INTRODUCTION
Over the past two weeks, I worked on developing a financial dashboard application using Next.js.
TABLE OF CONTENTS
- What to be Developed
- Overview of Application
- System Requirements
- Getting Started
WHAT TO BE DEVELOPED
A financial dashboard home page.
A login page.
Financial dashboard layout.
OVERVIEW OF APPLICATION
In the development of the financial dashboard application, there are some features that are going to be implemented like:
Styling: The different ways to style your application in Next.js.
Optimizations: How to optimize images, links, and fonts.
Routing: How to create nested layouts and pages using file-system routing.Data Fetching: How to set up a database on Vercel, and best practices for fetching and streaming.
SYSTEM REQUIREMENTS
This involves the system requirements:
Node.js 18.17.0 or later Download Node.js
Operating systems: macOS, Windows (including WSL), or Linux.
Visual Code editor.Download VisualStudio Code
GETTING STARTED
Creating The Financial Dashboard Application:
You need to install the pnpm as your package manager, and if you don't have pnpm install in your operating system, install by running this command on terminal:
npm install -g pnpm
Then to create a Next.js financial dashboard app, open your terminal, navigate to directory you want your app folder to be and then run the following command in the terminal:
npx create-next-app@latest nextjs-dashboard --example "https://github.com/vercel/next-learn/tree/main/dashboard/starter-example" --use-pnpm
Folder Structure:
You'll notice that the project has the following folder structure:
- /app: Contains all the routes, components, and logic for your application, this is where you'll be mostly working from.
- /app/lib: Contains functions used in your application, such as reusable utility functions and data fetching functions.
- /app/ui: Contains all the UI components for your application, such as cards, tables, and forms.
- /public: Contains all the static assets for your application, such as images.
- Config Files: You'll also notice config files such as next.config.js at the root of your application. Most of these files are created and pre-configured when you start a new project using create-next-app.
Running The Development Server:
Run pnpm i on your terminal to install the project's packages.
pnpm i
Followed by pnpm dev to start the development server.
pnpm dev
pnpm dev starts your Next.js development server. Check to see if it's working.
Open http://localhost:3000 on your browser.
CREATING A FINANCIAL DASHBOARD HOME PAGE
The home page will created by adding the images and fonts. To add the images, use the next/image component to automatically optimize your images.
Adding The Desktop Hero Image;
In your /app/page.tsx file, import the component from next/image. Then, add the image by using the following statements below:
import AcmeLogo from '@/app/ui/acme-logo';
import { ArrowRightIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { lusitana } from '@/app/ui/fonts';
import Image from 'next/image';
export default function Page() {
return (
// ...
<div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
{/* Add Hero Images Here */}
<Image
src="/hero-desktop.png"
width={1000}
height={760}
className="hidden md:block"
alt="Screenshots of the dashboard project showing desktop version"
/>
</div>
//...
);
}
Adding The Mobile Hero Image;
To add the Mobile Hero Image, in the same /app/page.tsx add the following statements below the Desktop Hero Image statements.
{/* Add Hero Images Here */}
<Image
src="/hero-desktop.png"
width={1000}
height={760}
className="hidden md:block"
alt="Screenshots of the dashboard project showing desktop version"
/>
<Image
src="/hero-mobile.png"
width={560}
height={620}
className="block md:hidden"
alt="Screenshot of the dashboard project showing mobile version"
/>
Now let's add some fonts to that home page. In your /app/ui folder, create a new file called fonts.ts. You'll use this file to keep the fonts that will be used throughout your application.
import { Inter } from 'next/font/google';
export const inter = Inter({ subsets: ['latin'] });
Finally, add the font to the
element in /app/layout.tsx:import '@/app/ui/global.css';
import { inter } from '@/app/ui/fonts';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={`${inter.className} antialiased`}>{children}</body>
</html>
);
}
So now the financial dashboard application home page will look like this below:
CREATING A LOGIN PAGE
Start by creating a new route called /login and paste the following code in your /app/login/page.tsx:
import AcmeLogo from '@/app/ui/acme-logo';
import LoginForm from '@/app/ui/login-form';
export default function LoginPage() {
return (
<main className="flex items-center justify-center md:h-screen">
<div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
<div className="flex h-20 w-full items-end rounded-lg bg-blue-500 p-3 md:h-36">
<div className="w-32 text-white md:w-36">
<AcmeLogo />
</div>
</div>
<LoginForm />
</div>
</main>
);
}
You'll notice the page imports . We will be using the NextAuth.js to add authentication to our application. NextAuth.js abstracts away much of the complexity involved in managing sessions, sign-in and sign-out, and other aspects of authentication.
Setting Up NextAuth.js;
Install NextAuth.js by running the following command in your terminal:
pnpm i next-auth@beta
Here, you're installing the beta version of NextAuth.js, which is compatible with Next.js 14.
Next, generate a secret key for your application. This key is used to encrypt cookies, ensuring the security of user sessions. You can do this by running the following command in your terminal:
openssl rand -base64 32
Then, in your .env file, add your generated key to the AUTH_SECRET variable:
AUTH_SECRET=your-secret-key
Adding The Page Options;
Create an auth.config.ts file at the root of our project that exports an authConfig object. This object will contain the configuration options for NextAuth.js. For now, it will only contain the pages option:
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
} satisfies NextAuthConfig;
You can use the pages option to specify the route for custom sign-in, sign-out, and error pages. This is not required, but by adding signIn: '/login' into our pages option, the user will be redirected to our custom login page, rather than the NextAuth.js default page.
Protecting Your Routes With Next.js Middleware;
Next, add the logic to protect your routes. This will prevent users from accessing the dashboard pages unless they are logged in.
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
if (isOnDashboard) {
if (isLoggedIn) return true;
return false; // Redirect unauthenticated users to login page
} else if (isLoggedIn) {
return Response.redirect(new URL('/dashboard', nextUrl));
}
return true;
},
},
providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;
The authorized callback is used to verify if the request is authorized to access a page via Next.js Middleware. It is called before a request is completed, and it receives an object with the auth and request properties. The auth property contains the user's session, and the request property contains the incoming request.
Next, you will need to import the authConfig object into a Middleware file. In the root of your project, create a file called middleware.ts and paste the following code:
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};
Here you're initializing NextAuth.js with the authConfig object and exporting the auth property. You're also using the matcher option from Middleware to specify that it should run on specific paths. Then create a new file called auth.ts that spreads your authConfig object:
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
});
Adding The Credentials Providers;
Next, you will need to add the providers option for NextAuth.js. providers is an array where you list different login options such as Google or GitHub. The Credentials provider allows users to log in with a username and a password. In your /auth.ts add the following code:
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [Credentials({})],
});
Adding The Sign In Functionality;
You can use the authorize function to handle the authentication logic. Similarly to Server Actions, you can use zod to validate the email and password before checking if the user exists in the database:
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
Credentials({
async authorize(credentials) {
const parsedCredentials = z
.object({ email: z.string().email(), password: z.string().min(6) })
.safeParse(credentials);
},
}),
],
});
After validating the credentials, create a new getUser function that queries the user from the database.
import { sql } from '@vercel/postgres';
import type { User } from '@/app/lib/definitions';
import bcrypt from 'bcrypt';
async function getUser(email: string): Promise<User | undefined> {
try {
const user = await sql<User>`SELECT * FROM users WHERE email=${email}`;
return user.rows[0];
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('Failed to fetch user.');
}
}
if (parsedCredentials.success) {
const { email, password } = parsedCredentials.data;
const user = await getUser(email);
if (!user) return null;
Updating The Login Form;
Now you need to connect the auth logic with your login form. In your /actions.ts file, create a new action called authenticate. This action should import the signIn function from /auth.ts:
'use server';
import { signIn } from '@/auth';
import { AuthError } from 'next-auth';
// ...
export async function authenticate(
prevState: string | undefined,
formData: FormData,
) {
try {
await signIn('credentials', formData);
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return 'Something went wrong.';
}
}
throw error;
}
}
Finally, in your login-form.tsx component, you can use React's useActionState to call the server action, handle form errors, and display the form's pending state:
import { useActionState } from 'react';
import { authenticate } from '@/app/lib/actions';
const [errorMessage, formAction, isPending] = useActionState(
authenticate,
undefined,
);
<form action={formAction} className="space-y-3">
<Button className="mt-4 w-full" aria-disabled={isPending}>
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
</Button>
{errorMessage && (
<>
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
<p className="text-sm text-red-500">{errorMessage}</p>
</>
)}
Adding Logout Functionality;
To add the logout functionality to , call the signOut function from auth.ts in your
element:import { signOut } from '@/auth';
action={async () => {
'use server';
await signOut();
}}
Now, try it out. You should be able to log in and out of your application. The login page should look like this:
CREATING DASHBOARD LAYOUT
Dashboards have some sort of navigation that is shared across multiple pages. In Next.js, you can use a special /layout.tsx file to create UI that is shared between multiple pages. inside the /dashboard folder, add a new file called /layout.tsx and paste the following code:
import SideNav from '@/app/ui/dashboard/sidenav';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
<div className="w-full flex-none md:w-64">
<SideNav />
</div>
<div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
</div>
);
}
You'll see the dashboard layout like this below:
Also set up pages for the /customers and /invoices.
Customers Page: /app/dashboard/customers/page.tsx
export default function Page() {
return <p>Customers Page</p>;
}
Invoices Page: /app/dashboard/invoices/page.tsx
export default function Page() {
return <p>Invoices Page</p>;
}
Navigation Between Pages:
In Next.js, you can use the Component to link between pages in your application. allows you to do client-side navigation with JavaScript. To use the component, open /app/ui/dashboard/nav-links.tsx, and import the Link component from next/link. Then, replace the tag with :
import {
UserGroupIcon,
HomeIcon,
DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
**import Link from 'next/link';**
// ...
export default function NavLinks() {
return (
<>
{links.map((link) => {
const LinkIcon = link.icon;
return (
<Link
key={link.name}
href={link.href}
className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
</>
);
}
CREATING THE DATABASE
- Create A Github Repository;
To start, let's push your repository to Github if you haven't done so already. This will make it easier to set up your database and deploy. If you need help setting up your repository, take a look at this guide on Github.
- Create A Vercel Account;
Visit Vercel.com to create an account. Choose the free "hobby" plan. Select Continue with GitHub to connect your GitHub and Vercel accounts.
- Connect And Deploy Your Project;
You'll be taken to this screen where you can select and import the GitHub repository you've just created:
Name your project and click Deploy.
- Create A Postgres Database;
To set up a database, click Continue to Dashboard and select the Storage tab from your project dashboard. Select Connect Store → Create New → Postgres → Continue.
Top comments (0)