This Part 3 is a continuation of Part 2 of our blog series. If you haven't read Part 1 and Part 2 of this series, it's advisable that you do so to understand how we got to this stage.
For reference, here are the previous parts of this blog series:
We'll add some protection for Part 3 of this blog series to prevent unauthorized access to our application.
Strapi provides built-in authentication and user management features, including roles and permissions. Follow these steps to configure these settings and set roles and permissions.
We'll start by creating our folder structure layout for these authentication pages. We need two pages for authentication: the signup page and the login page.
Remember, in Part 1 of this blog series, we made the 'Overview' page the first page displayed to the user when they enter the application. We'll change it to be the page they're navigated to after a successful signup and login, meaning we'll make the sign-up/log-in page the main page of the application.
Let's create our components first.
In our app
folder, we'll create a new folder and name it auth
. We'll create two sub-folders in this newly created folder - 'signup' and 'signin'.
In the 'signup' folder, we'll also create a component called SignUp.tsx
In the 'signin' folder, we'll create two components - page.tsx
and SignIn.tsx
.
These will be the signup and sign-in form pages, which will contain forms for signing up and logging in to the application.
1The layout will look like this:
app/
┣ auth/
┃ ┣ signin/
┃ ┃ ┣ page.tsx
┃ ┃ ┗ SignIn.tsx
┃ ┗ signup/
┃ ┗ SignUp.tsx
┣ dashboard/
┣ globals.css
┣ head. tsx
┣ layout. tsx
┗ page.tsx
page.tsx
component inside the app
folder, the first page of the application will be changed from the overview dashboard page to the signup page.To do this, we'll change this page.tsx
component from this:
1import SideNav from "@/components/SideNav";
2import Overview from "./dashboard/Overview";
3
4const page = () => {
5 return (
6 <>
7 <div>
8 <SideNav />
9 <div>
10 <Overview />
11 </div>
12 </div>
13 </>
14 );
15};
16export default page;
To this:
1"use client";
2import SignUp from "./auth/signup/SignUp";
3
4function App() {
5 return (
6 <div>
7 <SignUp />
8 </div>
9 );
10}
11export default App;
1'use client'
2import React, { useState, useEffect } from 'react';
3import axios from 'axios';
4import BarChart from './budget/BarChart';
5import PieChart from './budget/PieChart';
6
7const page: React.FC = () => {
8 const [budgets, setBudgets] = useState<{ category: string; amount: number; }[]>([]);
9 const [chartType, setChartType] = useState<'bar' | 'pie'>('bar');
10
11 useEffect(() => {
12 const fetchBudgets = async () => {
13 try {
14 const res = await axios.get('http://localhost:1337/api/budgets?populate=budget');
15 const data = res.data.data.map((budget: any) => ({
16 category: budget.attributes.category,
17 amount: budget.attributes.amount,
18 }));
19 setBudgets(data);
20 } catch (error) {
21 console.error('Error fetching budgets:', error);
22 }
23 };
24
25 fetchBudgets();
26 }, []);
27
28 const categories = budgets.map(budget => budget.category);
29 const amounts = budgets.map(budget => budget.amount);
30
31 return (
32 <main>
33 <div>
34 <h2>OVERVIEW</h2>
35 <div>
36 <button onClick={() => setChartType('bar')} className={`mx-2 py-2 px-3 ${chartType === 'bar' ? 'bg-teal-500 text-white' : 'bg-gray-200 text-gray-700'} rounded-lg`}>
37 Bar Chart
38 </button>
39 <button onClick={() => setChartType('pie')} className={`mx-2 py-2 px-3 ${chartType === 'pie' ? 'bg-teal-500 text-white' : 'bg-gray-200 text-gray-700'} rounded-lg`}>
40 Pie Chart
41 </button>
42 </div>
43 </section>
44 <section className="mt-">
45 {chartType === 'bar' ? (
46 <BarChart categories={categories} amounts={amounts} />
47 ) : (
48 <PieChart categories={categories} amounts={amounts} />
49 )}
50 </div>
51 </main>
52 );
53};
54
55export default page;
Note: This overview page was worked on in Part 2 of our series.
Let's create the forms that will be used to authenticate users.
Create the sign-up form page
Let's go to our SignUp.tsx
component and paste these lines of code to build the sign-up form.
1"use client";
2import React, { useState } from "react";
3import { FaEye, FaEyeSlash } from "react-icons/fa";
4import Link from "next/link";
5import axios from "axios";
6import { useRouter } from "next/navigation";
7
8const SignUp = () => {
9 const [username, setUsername] = useState("");
10 const [email, setEmail] = useState("");
11 const [password, setPassword] = useState("");
12 const [showPassword, setShowPassword] = useState(false);
13 const handleTogglePassword = () => {
14 setShowPassword((prevShowPassword) => !prevShowPassword);
15 };
16
17 const router = useRouter();
18
19 const handleSubmit = async (event: { preventDefault: () => void }) => {
20 event.preventDefault();
21 try {
22 const response = await axios.post(
23 "http://localhost:1337/api/auth/local/register",
24 {
25 username,
26 email,
27 password,
28 },
29 );
30 console.log(response.data);
31 alert("Registration successful!");
32
33 // Clear the form inputs
34 setUsername("");
35 setEmail("");
36 setPassword("");
37
38 // Redirect to the login page
39 if (typeof window !== "undefined") {
40 router.push("/auth/signin");
41 }
42 } catch (error) {
43 console.error(
44 "Registration failed:",
45 error.response?.data || error.message,
46 );
47 alert("Registration failed. Username or email invalid or already taken");
48 }
49 };
50
51 return (
52 <main>
53 <h1>Sign up</h1>
54 <form onSubmit={handleSubmit}>
55 <div>
56 <label htmlFor="username">Username</label>
57 <input
58 type="text"
59 placeholder="Enter your username"
60 value={username}
61 onChange={(e) => setUsername(e.target.value)}
62 />
63 </div>
64
65 <div>
66 <label htmlFor="email">Email</label>
67 <input
68 type="email"
69 placeholder="Enter your email address"
70 value={email}
71 onChange={(e) => setEmail(e.target.value)}
72 />
73 </div>
74
75 <div>
76 <label htmlFor="password">Password</label>
77 <input
78 type={showPassword ? "text" : "password"}
79 placeholder="*******"
80 value={password}
81 onChange={(e) => setPassword(e.target.value)}
82 />
83 <div onClick={handleTogglePassword}>
84 {showPassword ? <FaEyeSlash /> : <FaEye />}
85 </div>
86 </div>
87
88 <button type="submit">Sign Up</button>
89 </form>
90
91 <p>
92 {" "}
93 Already have an account?
94 <Link href="/auth/signin">
95 <span>Login</span>
96 </Link>
97 </p>
98 </main>
99 );
100};
101
102export default SignUp;
Note: I don't want the code to be too long, so I've omitted the styling for all the components in this article. You can check out the full TailwindCSS code later.
Code explanation:
username
input, email
input, password
input, and showPassword
for password visibility using the useState
hook.handleTogglePassword
function to handle the password visibility toggle.useRouter
hook from Next.js, we handled page routing after a successful signup.handleSubmit
function to send a POST request to the Strapi registration endpoint (http://localhost:1337/api/auth/local/register).
This function logs the response data to the console and displays an alert for successful signup. It then clears the form inputs and redirects us to the sign-in page (/auth/signin) upon a successful registration.Here's what our signup page looks like after styling:
Create the sign-in form page
Inside our SignIn.tsx
component that's located inside the 'signin' folder we created, we'll paste these lines of code:
1"use client";
2import React, { useState } from "react";
3import { FaEye, FaEyeSlash } from "react-icons/fa";
4import Link from "next/link";
5import axios from "axios";
6import { useRouter } from "next/navigation";
7
8const SignIn = () => {
9 const [email, setEmail] = useState("");
10 const [password, setPassword] = useState("");
11 const [showPassword, setShowPassword] = useState(false);
12
13 const router = useRouter();
14
15 const handleTogglePassword = () => {
16 setShowPassword((prevShowPassword) => !prevShowPassword);
17 };
18
19 const handleSubmit = async (event: { preventDefault: () => void }) => {
20 event.preventDefault();
21 try {
22 if (!email || !password) {
23 throw new Error("Please fill in all fields.");
24 }
25 const response = await axios.post(
26 "http://localhost:1337/api/auth/local",
27 {
28 identifier: email,
29 password,
30 },
31 );
32 console.log(response.data);
33 alert("Login successful!");
34
35 // Save user information in local storage
36 localStorage.setItem("user", JSON.stringify(response.data.user));
37
38 // Redirect to the dashboard page
39 if (typeof window !== "undefined") {
40 router.push("/dashboard");
41 }
42 } catch (error: any) {
43 console.error("Login failed:", error.response?.data || error.message);
44 alert("Invalid login details");
45 }
46 };
47
48 return (
49 <main>
50 <h1>Login</h1>
51 <form onSubmit={handleSubmit}>
52 <div>
53 <label htmlFor="email">Email</label>
54 <input
55 type="email"
56 placeholder="Enter your email address"
57 value={email}
58 onChange={(e) => setEmail(e.target.value)}
59 />
60 </div>
61
62 <div>
63 <label htmlFor="password">Password</label>
64 <input
65 type={showPassword ? "text" : "password"}
66 placeholder="*******"
67 value={password}
68 onChange={(e) => setPassword(e.target.value)}
69 />
70 <div onClick={handleTogglePassword}>
71 {showPassword ? <FaEyeSlash /> : <FaEye />}
72 </div>
73 </div>
74
75 <button type="submit">Sign in</button>
76 </form>
77
78 <p>
79 {" "}
80 Don't have an account?
81 <Link href="/">
82 <span>Sign Up</span>
83 </Link>
84 </p>
85 </main>
86 );
87};
88
89export default SignIn;
Code explanation:
email
input, password
input, and showPassword
for password visibility using the useState hook.useRouter
hook from Next.js, we handled page routing to the dashboard.handleSubmit
function that sends a POST request to the Strapi authentication endpoint (http://localhost:1337/api/auth/local).
This function logs the response data to the console, displays an alert for successful sign-in, and saves our info in localStorage
. It then clears the form inputs and redirects us to the dashboard page (/dashboard) upon successful sign-in.NOTE: Both sign-in and signup pages use similar functionality but handle different authentication endpoints (sign-in uses
/api/auth/local
, and signup uses/api/auth/local/register
).
Here's what our sign-in form looks like after styling:
You can include a more advanced form validation and error handling method like Formik, Zod, or any other library.
Set the route for the login page
In the page.tsx
component located inside the 'signin' folder, we'll paste these lines of code:
1import React from "react";
2import SignIn from "./SignIn";
3
4const page = () => {
5 return (
6 <>
7 <SignIn />
8 </>
9 );
10};
11
12export default page;
Add a 'Log out' button in your sidebar navigation that will navigate users back to the signup or sign-in page.
1<Link href="/auth/signin">
2 <section>
3 <FaSignOutAlt />
4 <span>Log out</span>
5 </section>
6</Link>;
We can test out this functionality by inputting our registration details, logging in, and we'll be directed to the dashboard page. If we go to the 'Content-Type Builder' page of our Strapi admin panel, we'll to see the details we signed up with in the 'User' collection type.
We didn't need to create a collection type for the user's details like we did with the budget, income, and expense collections. That's because 'User' is a collection type that's available for use by default whenever we create a new Strapi project.
Update the dashboard main page Let's go to to the 'page. tsx' file located in our 'dashboard' folder and update the code to this:
1"use client";
2import SideNav from "@/components/SideNav";
3import React, { useEffect, useState } from "react";
4import { useRouter } from "next/navigation";
5import axios from "axios";
6import BarChart from "./budget/BarChart";
7import PieChart from "./budget/PieChart";
8
9const page = () => {
10 const [budgets, setBudgets] = useState<
11 { category: string; amount: number }[]
12 >([]);
13 const [chartType, setChartType] = useState<"bar" | "pie">("bar");
14
15 const [user, setUser] = useState<{ username: string; email: string } | null>(
16 null,
17 );
18 const router = useRouter();
19
20 useEffect(() => {
21 // Retrieve user data from local storage
22 const storedUser = localStorage.getItem("user");
23 if (storedUser) {
24 setUser(JSON.parse(storedUser));
25 } else {
26 // Redirect to login page if no user data is found
27 router.push("/auth/signin");
28 }
29 }, [router]);
30
31 useEffect(() => {
32 const fetchBudgets = async () => {
33 try {
34 const res = await axios.get(
35 "http://localhost:1337/api/budgets?populate=budget",
36 );
37 const data = res.data.data.map((budget: any) => ({
38 category: budget.attributes.category,
39 amount: budget.attributes.amount,
40 }));
41 setBudgets(data);
42 } catch (error) {
43 console.error("Error fetching budgets:", error);
44 }
45 };
46
47 fetchBudgets();
48 }, []);
49
50 const categories = budgets.map((budget) => budget.category);
51 const amounts = budgets.map((budget) => budget.amount);
52
53 return (
54 <>
55 <main>
56 <SideNav />
57
58 <div>
59 <section>
60 <h2>OVERVIEW</h2>
61 {user && (
62 <p className="mt-4 text-lg">
63 Welcome {user.username ? user.username : user.email}!
64 </p>
65 )}
66 <div>
67 <button
68 onClick={() => setChartType("bar")}
69 className={`mx-2 py-2 px-3 ${chartType === "bar" ? "bg-teal-500 text-white" : "bg-gray-200 text-gray-700"} rounded-lg`}
70 >
71 Bar Chart
72 </button>
73 <button
74 onClick={() => setChartType("pie")}
75 className={`mx-2 py-2 px-3 ${chartType === "pie" ? "bg-teal-500 text-white" : "bg-gray-200 text-gray-700"} rounded-lg`}
76 >
77 Pie Chart
78 </button>
79 </div>
80 </section>
81
82 {budgets.length === 0 ? (
83 <div>
84 <p>No budget has been added, so no visuals yet.</p>
85 </div>
86 ) : (
87 <section className="mt-5">
88 {chartType === "bar" ? (
89 <BarChart categories={categories} amounts={amounts} />
90 ) : (
91 <PieChart categories={categories} amounts={amounts} />
92 )}
93 </section>
94 )}
95 </div>
96 </main>
97 </>
98 );
99};
100
101export default page;
The things we added to this component include:
SideNav
component, along with the useRouter
method from Next.js.const [user, setUser] = useState<{ username: string, email: string } | null>(null);
.useRouter
method and implemented the useEffect
hook to retrieve user data from local storage where it was stored in the 'Signin' page.Our app is almost ready at this point.
When we inspect our console in the browser, we should see the client-side code typically interacting with JWT tokens provided by the Strapi server during signup and sign-in.
That's because Strapi's server-side endpoints (/api/auth/local
and /api/auth/local/register
) handle JWT authentication internally.
For the Signup: When we sign up, our credentials are usually sent to the server. After successful authentication, the server generates a JWT token containing our information and signs it with a secret key. The JWT token is then returned to the client and stored securely, usually in local storage or a secure cookie.
For the Signin: When we sign in, our credentials are sent to the server. After a successful authentication, the server generates a new JWT token or refreshes the existing token and returns it to the client.
To get a good understanding of this authentication method used in Strapi, take a look at Beginners Guide to Authentication and Authorization in Strapi article.
Now, let's move on to a more advanced feature: personalized financial reports in our finance tracker application.
We want to implement a feature that will enable us to generate a personalized report based on our financial data.
Let's build!
We'll need to configure the Strapi backend to enable this functionality. So how do we go about this?
Let's navigate to the Strapi backend folder of our project and follow the steps below:
1"use strict";
2
3/**
4 * report controller
5 */
6
7// path: src/api/report/controllers/report.js
8
9module.exports = {
10 async generate(ctx) {
11 const { budgets, incomes, expenses } = ctx.request.body;
12
13 if (!budgets.length && !incomes.length && !expenses.length) {
14 return ctx.badRequest("No data available to generate report.");
15 }
16
17 // Calculate totals and find max expense
18 const totalExpenses = expenses.reduce(
19 (sum, expense) => sum + expense.amount,
20 0,
21 );
22 const totalIncomes = incomes.reduce(
23 (sum, income) => sum + income.amount,
24 0,
25 );
26
27 let maxExpenseCategory = "N/A";
28 let maxExpenseAmount = 0;
29
30 if (expenses.length > 0) {
31 const maxExpense = expenses.reduce(
32 (max, expense) => (expense.amount > max.amount ? expense : max),
33 expenses[0],
34 );
35 maxExpenseCategory = maxExpense.description;
36 maxExpenseAmount = maxExpense.amount;
37 }
38
39 // Analyze and generate personalized report
40 let report = " ";
41
42 // budget report logic
43 if (budgets.length > 0) {
44 report += "Based on your data: ";
45 budgets.forEach((budget) => {
46 report += `Your budget for '${budget.category}' is '${budget.amount}', `;
47 });
48 report += "<br>";
49 }
50
51 // income and expenses report logic
52 if (expenses.length > 0) {
53 report += `You are spending more on ${maxExpenseCategory} than other expenses. <br>`;
54 if (totalExpenses >= totalIncomes) {
55 report += `You've spent a total of <strong>$${totalExpenses}</strong> on expenses while having an inflow/income of <strong>$${totalIncomes}</strong>, meaning you've spent more than you earned. Oops!🙁. <br>`;
56 } else {
57 report += `You've spent a total of <strong>$${totalExpenses}</strong> on expenses while having an inflow/income of <strong>$${totalIncomes}</strong>, meaning you managed to spend less than you earned. Kudos 🎉. <br>`;
58 }
59 }
60
61 const createdReport = await strapi.query("api::report.report").create({
62 data: { report },
63 });
64
65 return ctx.send({ report: createdReport.report });
66 },
67};
Controller code explanation:
budgets
, incomes
, and expenses
arrays from the request body.1if (!budgets.length && !incomes.length && !expenses.length) {
2 return ctx.badRequest('No data available to generate report.');
3}
This controller file contains the logic for generating personalized financial reports based on user data.
This is just a basic report that we generated. Feel free to change the content of the report as best you see fit.
1"use strict";
2
3/**
4 * report routes
5 */
6
7module.exports = {
8 routes: [
9 {
10 method: "POST",
11 path: "/generate-report",
12 handler: "report.generate",
13 config: {
14 policies: [],
15 middlewares: [],
16 },
17 },
18 ],
19};
1{
2 "kind": "collectionType",
3 "collectionName": "reports",
4 "info": {
5 "singularName": "report",
6 "pluralName": "reports",
7 "displayName": "Reports",
8 "description":" "
9 },
10 "options": {
11 "draftAndPublish": true
12 },
13 "pluginOptions": {},
14 "attributes": {
15 "report": {
16 "type": "text"
17 }
18 }
19}
Here's how our folder structure for these Strapi configuration files will look like now:
report/
┣ content-types/
┃ ┗ report/
┃ ┗ schema.json
┣ controllers/
┃ ┗ report.js
┗ routes/
┗ report.js
We'll need to stop our panel server from running in the terminal using the 'cls' command for windows powershell. Then we'll run the server again with the npm run develop
command to make sure Strapi is aware of our new controller, route, and schema.
Open up the server in the browser.
NOTE: Anytime you make changes to the Strapi backend, the server reloads.
What is left to do is to update the frontend code to implement the personalized report logic and display the report.
Let's navigate to our 'page. tsx' file inside our "/src/app/dashboard/page.tsx" and include these lines of code:
1const [incomes, setIncomes] = useState<
2 { description: string; amount: number }[]
3>([]);
4const [expenses, setExpenses] = useState<
5 { description: string; amount: number }[]
6>([]);
7const [report, setReport] = useState<string | null>(null);
fetchIncomesAndExpenses
function inside the useEffect
hook that's handling the budget fetching. This function will retrieve income and expense data from the Strapi API and set the state for these data in the component:1// Fetch incomes and expenses for the report
2const fetchIncomesAndExpenses = async () => {
3 try {
4 const incomeRes = await axios.get("http://localhost:1337/api/incomes");
5 const expenseRes = await axios.get("http://localhost:1337/api/expenses");
6
7 const incomeData = incomeRes.data.data.map((income: any) => ({
8 description: income.attributes.description,
9 amount: income.attributes.amount,
10 }));
11
12 const expenseData = expenseRes.data.data.map((expense: any) => ({
13 description: expense.attributes.description,
14 amount: expense.attributes.amount,
15 }));
16
17 setIncomes(incomeData);
18 setExpenses(expenseData);
19 } catch (error) {
20 console.error("Error fetching incomes and expenses:", error);
21 }
22};
We'll need to call the function in the useEffect
hook like this: fetchIncomesAndExpenses();
Then, we'll create a separate generateReport
function to send a POST request to the new Strapi API to generate personalized financial reports based on the current budget, income, and expense data.
If the request is successful, the generated report from the response is set in the component state using setReport
. If the request fails, an error message is logged to the console, and a message alert is displayed on the app.
1const generateReport = async () => {
2 try {
3 const res = await axios.post("http://localhost:1337/api/generate-report", {
4 budgets,
5 incomes,
6 expenses,
7 });
8
9 setReport(res.data.report);
10 } catch (error) {
11 console.error("Failed to generate report:", error);
12 alert("Failed to generate report");
13 }
14};
dangerouslySetInnerHTML
property to render the HTML string.1<div className="container mx-auto py-6 flex justify-center">
2 <button onClick={generateReport}>Generate Report</button>
3</div>;
4
5{
6 report && (
7 <div>
8 <h3>Financial report</h3>
9 <div dangerouslySetInnerHTML={{ __html: report }}></div>
10 </div>
11 );
12}
NOTE: You can view the full complete code for the now updated 'page.tsx' component here.
We'll go to our browser, refresh the web app to ensure changes are reflected. In our overview page, at the bottom, click the "Generate report" button. We'll get a simplified report of our financial data.
When we go to the 'Content Manager' section in your Strapi admin panel, we'll see that the generated report was created as an entry in the 'Report' collection.
There you have it. Our finance tracker application is functional and ready for use. You can generate a more comprehensive report using this technique.
Here's a quick demo of the report generation feature.
If you make any changes to your financial data (budget, income, expenses), the page refreshes and you can generate a new report to reflect the changes made.
If we want someone else to be able to view this report, we can add a functionality that allows us to export the report as PDF.
For example, if we want to export only the budget data report, including either of the charts, we can omit the part that analyzes the income and expenses in the report.js
file in the controllers.
Let's only generate a report for the budget data so we can export the budget report.
So how do we print and export this data report along with the chart? Here's how we'll do it.
We'll use three libraries - html-to-pdfmake, pdfmake and html2canvas for the printing functionality.
The html-to-pdfmake
library is for generating PDFs from HTML content. The pdfmake
library is used to generate PDFs for both client-side and server-side, allowing us to create PDFs from structured data. The html2canvas
library will be used to capture the chart as an image, and then the image will be included in the PDF document generated by the pdfmake
library.
We'll have to install these libraries using this command:
npm install html-to-pdfmake pdfmake html2canvas
Update our overview component, like this:
1import htmlToPdfmake from 'html-to-pdfmake';
2import pdfMake from 'pdfmake/build/pdfmake';
3import pdfFonts from 'pdfmake/build/vfs_fonts';
4import html2canvas from 'html2canvas';
Next, we'll add this under the imports: pdfMake.vfs = pdfFonts.pdfMake.vfs;
We'll create a printReport
function that will convert the HTML content of the report to a PDF and trigger a download. This function will capture the chart as an image using the html2canvas
package. It will then convert the chart image and the HTML content of the report to a PDF document, including the captured chart image.
1const printReport = async () => {
2 try {
3 const printContent = document.getElementById("report-content")?.innerHTML;
4 const chartCanvas = chartRef.current;
5
6 if (printContent && chartCanvas) {
7 const canvas = await html2canvas(chartCanvas);
8 const chartImage = canvas.toDataURL("image/png");
9
10 const docDefinition = {
11 content: [
12 { text: "Financial Report", style: "header" },
13 { image: chartImage, width: 500 },
14 htmlToPdfmake(printContent),
15 ],
16 styles: {
17 header: {
18 fontSize: 18,
19 bold: true,
20 margin: [0, 0, 0, 10],
21 },
22 },
23 };
24
25 pdfMake.createPdf(docDefinition).download("financial_report.pdf");
26 }
27 } catch (error) {
28 console.error("Failed to generate PDF:", error);
29 }
30};
We'll also import a useRef
hook at the top of our file and use this hook to get a reference to the chart section in the DOM, like this:
const chartRef = useRef(null);
Here's how we'll add the ref
to the 'Chart' Section:
1<section ref={chartRef}>
2 {chartType === "bar" ? (
3 <BarChart categories={categories} amounts={amounts} />
4 ) : (
5 <PieChart categories={categories} amounts={amounts} />
6 )}
7</section>;
printReport
function:1<button onClick={printReport}>
2 Export as PDF
3</button>
Now, when we generate a report by clicking the "Generate report" button, our budget report is generated along with an "Export as PDF" button. When we click this button, our report will be exported as PDF, and it will automatically download the report as PDF to your system.
Feel free to style your report in any way you want it to appear in the PDF inside the printReport
function.
If we want to download a copy of the report with the pie chart instead of the bar chart, we'll switch the chart type to 'pie chart' on the web page. Click the "Generate report" button again and export.
If we want to enable cashflow statement report generation for the income and expenses, we'll follow the same steps and export it the same way.
This is the GitHub repo for this article.
In this "Build a Finance Tracker with Next.js, Strapi, and Chart.js" blog series, here's a summary of what we learned:
This app can be used by individuals hoping to manage their finances, observe their spending habits, track their financial data, and get a simplified report of their finances.
I hope you enjoyed this series and were able to code along with me and build your own finance tracker application.
Juliet is a developer and technical writer whose work aims to break down complex technical concepts and empower developers of all levels.