In this part of the tutorial, we will create the pages and components UI using the shadcn/ui library.
In this lesson, we will create the visual structure of our application’s authentication page. We will start by defining some styling classes, such as the min-h-screen class to take up the entire height of the screen.
Then, we will divide the screen into two columns using a grid. We will create a div for the project logo, with a border on the right to separate it from the form content.
We will use colors from the shadcn/ui design system and add a footer with the copyright. We will center the login content and create a form with inputs and a button to access the dashboard.
1. Install new components from shadcn/ui, such as Input and Label:
pnpm dlx shadcn-ui@latest add input label
2. Add the following code into sign-in.tsx and auth.tsx (_layouts):
// sign-in.tsx
import { Label } from "@radix-ui/react-label"
import { Helmet } from "react-helmet-async"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function SignIn() {
return (
<>
<Helmet title="Login" />
<div className="p-8">
<div className="flex w-[350px] flex-col justify-center gap-6">
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tighter">
Access Dashboard
</h1>
<p className="text-sm text-muted-foreground">
Track your sales through the partner dashboard!
</p>
</div>
<form
action=""
className="flex flex-col gap-4"
>
<div className="space-y-2">
<Label htmlFor="email">Your E-mail</Label>
<Input
id="email"
type="email"
></Input>
</div>
<Button className="w-full">Access Panel</Button>
</form>
</div>
</div>
</>
)
}
// auth.tsx (_layouts)
import { Pizza } from "lucide-react"
import { Outlet } from "react-router-dom"
export function AuthLayout() {
return (
<div className="grid min-h-screen grid-cols-2">
<div className="flex h-full flex-col justify-between border-r border-foreground/5 bg-muted p-10 text-muted-foreground">
<div className="flex items-center gap-3 text-lg font-medium text-foreground">
<Pizza className="h-5 w-5"></Pizza>
<span className="font-semibold">Pizza Shop</span>
</div>
<footer className="text-sm">
Dashboard client © Pizza Shop - {new Date().getFullYear()}
</footer>
</div>
<div className="flex flex-col items-center justify-center">
<Outlet />
</div>
</div>
)
}
In this lesson, we will set up the React Hook Form library to handle forms. We will install the necessary dependencies, such as react-hook-form, Zod for data validation, and hookform resolvers for integration with Zod.
Next, we will import useForm from the react hook form, which returns important information, such as register and handleSubmit.
To validate the form data, we will create a validation object using Zod. To test it, we will transform the handleSignin function into an asynchronous function and add a timeout to simulate a two-second delay. We will also use the formState property to disable the submit button while the form is being submitted.
1. Install the package react-hook-form zod and @hookform/resolvers
pnpm i react-hook-form zod @hookform/resolvers
2. Add the following code into the sign-in.tsx
// sign-in.tsx file
import { Label } from "@radix-ui/react-label"
import { Helmet } from "react-helmet-async"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
const signInForm = z.object({
email: z.string().email(),
})
type signInForm = z.infer<typeof signInForm>
export function SignIn() {
const {
register,
handleSubmit,
formState: { isSubmitting },
} = useForm<signInForm>()
async function handleSignIn(data: signInForm) {
console.log(data)
await new Promise(resolve => setTimeout(resolve, 2000))
}
return (
<>
<Helmet title="Login" />
<div className="p-8">
<div className="flex w-[350px] flex-col justify-center gap-6">
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tighter">
Access Dashboard
</h1>
<p className="text-sm text-muted-foreground">
Track your sales through the partner dashboard!
</p>
</div>
<form
action=""
className="flex flex-col gap-4"
onSubmit={handleSubmit(handleSignIn)}
>
<div className="space-y-2">
<Label htmlFor="email">Your E-mail</Label>
<Input
id="email"
type="email"
{...register("email")}
></Input>
</div>
<Button
className="w-full"
disabled={isSubmitting}
>
Access Panel
</Button>
</form>
</div>
</div>
</>
)
}
In this lesson, we will learn about the Sonner library, which is a pre-styled toast library. Sonner makes it easy to display success or error messages to the user.
We will install Sonner and configure it in our project.
pnpm install sonner
Next, we will use Sonner to display success and error toasts during the user authentication process. We will use the success method to display a success message when the authentication link is sent to the user’s email.
We will also explore some of Sonner’s properties, such as custom colors and adding action buttons to toasts.
In the sign-in component modify the function handleSubmit.
// App.tsx
import "./global.css"
import { Helmet, HelmetProvider } from "react-helmet-async"
import { RouterProvider } from "react-router-dom"
import { Toaster } from "sonner"
import { router } from "./routes"
export function App() {
return (
<HelmetProvider>
<Helmet titleTemplate="%s | Pizza Web Shop" />
<Toaster richColors />
<RouterProvider router={router} />
</HelmetProvider>
)
}
// sign-in.tsx
import { toast } from "sonner"
async function handleSignIn(data: signInForm) {
try {
console.log(data)
await new Promise(resolve => setTimeout(resolve, 2000))
toast.success("Sent an authentication link to your e-mail.", {
action: {
label: "Send again",
onClick: () => handleSignIn(data),
},
})
} catch {
toast.error("Try again, unexpected error occured.")
}
}
In this lesson, we created the registration page, which is similar to the login page. We added additional fields to the registration form, such as restaurant name, username, and phone number. We also added a paragraph with text about the terms of service and privacy policies.
We used the Link component from react-router-dom to redirect the user to the login page after registration. We also made adjustments to the Toast messages to display specific error and success messages.
Finally, we used the radix slot to style the “new restaurant” button as a button.
1. Create the sign-up page the same as sign-in:
import { Label } from "@radix-ui/react-label"
import { Helmet } from "react-helmet-async"
import { useForm } from "react-hook-form"
import { Link, useNavigate } from "react-router-dom"
import { toast } from "sonner"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
const signUpForm = z.object({
restaurantName: z.string(),
managerName: z.string(),
phone: z.string(),
email: z.string().email(),
})
type signUpForm = z.infer<typeof signUpForm>
export function SignUp() {
const navigate = useNavigate()
const {
register,
handleSubmit,
formState: { isSubmitting },
} = useForm<signUpForm>()
async function handleSignUp(data: signUpForm) {
try {
console.log(data)
await new Promise(resolve => setTimeout(resolve, 2000))
toast.success("Restaurant registered successfully.", {
action: {
label: "Login",
onClick: () => navigate("/sign-in"),
},
})
} catch {
toast.error(
"Error when registering you as our partner. Try again, unexpected error occured.",
)
}
}
return (
<>
<Helmet title="Register" />
<div className="p-8">
<Button
asChild
variant={"secondary"}
className="absolute right-8 top-8"
>
<Link to="/sign-in">Sign In</Link>
</Button>
<div className="flex w-[350px] flex-col justify-center gap-6">
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tighter">
Create free account
</h1>
<p className="text-sm text-muted-foreground">
Be our partner and start your sales!
</p>
</div>
<form
action=""
className="flex flex-col gap-4"
onSubmit={handleSubmit(handleSignUp)}
>
<div className="space-y-2">
<Label htmlFor="restaurantName">Restaurant Name</Label>
<Input
id="restaurantName"
type="text"
{...register("restaurantName")}
></Input>
</div>
<div className="space-y-2">
<Label htmlFor="managerName">Your Name</Label>
<Input
id="managerName"
type="text"
{...register("managerName")}
></Input>
</div>
<div className="space-y-2">
<Label htmlFor="email">Your E-mail</Label>
<Input
id="email"
type="email"
{...register("email")}
></Input>
</div>
<div className="space-y-2">
<Label htmlFor="phone">Your Phone</Label>
<Input
id="phone"
type="tel"
{...register("phone")}
></Input>
</div>
<Button
className="w-full"
disabled={isSubmitting}
>
Complete Registration
</Button>
<p className="px-6 text-center text-sm leading-relaxed text-muted-foreground">
By continuing, you agree to our{" "}
<a
href=""
className="underline underline-offset-4"
>
Terms of Service
</a>{" "}
and{" "}
<a
href=""
className="underline underline-offset-4"
>
Privacy Policy
</a>
.
</p>
</form>
</div>
</div>
</>
)
}
Explanation of the button properties:
// Note:
// asChild: all properties/styles of a button and pass into the link
// variant: color from shadcn/ui
<Button
asChild
variant={"secondary"}
className="absolute right-8 top-8"
>
<Link to="/sign-up">New Shop</Link>
</Button>
If you need to send the user to another page/component without clicking button you can use navigate react-router DOM
import { Link, useNavigate} from "react-router-dom"
const navigate = useNavigate()
toast.success("Restaurant registered successfully.", {
action: {
label: "Login",
onClick: () => navigate("/sign-in"),
},
})
The final step is to modify the routes.tsx so you can access the sign-up page properly:
import { createBrowserRouter } from "react-router-dom"
import { AppLayout } from "./pages/_layouts/app"
import { AuthLayout } from "./pages/_layouts/auth"
import { Dashboard } from "./pages/app/dashboard"
import { SignIn } from "./pages/auth/sign-in"
import { SignUp } from "./pages/auth/sign-up"
export const router = createBrowserRouter([
{
path: "/",
element: <AppLayout />,
children: [{ path: "/", element: <Dashboard /> }],
},
{
path: "/",
element: <AuthLayout />,
children: [
{ path: "/sign-in", element: <SignIn /> },
{ path: "/sign-up", element: <SignUp /> },
],
},
])
In this lesson, we start customizing the header of a food delivery app. We create a component called NavLink to style the header links in a specific way.
We use react-router-dom to get the current route and add a data attribute to indicate whether the link is active or not. Based on this attribute, we style the link text according to the current route. We also add a separator between the logo and the navigation menu.
Add the component Separator:
pnpm dlx shadcn-ui@latest add separator
Component Header:
// header.tsx
import { Home, Pizza, UtensilsCrossed } from "lucide-react"
import { Separator } from "@/components/ui/separator"
import { NavLink } from "./nav-link"
export function Header() {
return (
<div className="border-b">
<div className="flex h-16 items-center gap-6 px-6">
<Pizza className="h-6 w-6" />
<Separator
orientation="vertical"
className="h-6"
/>
<nav className="flex items-center space-x-4 lg:space-x-6">
<NavLink to="/">
<Home className="h-4 w-4" />
Home
</NavLink>
<NavLink to="/orders">
<UtensilsCrossed className="h-4 w-4" />
Orders
</NavLink>
</nav>
</div>
</div>
)
}
The component NavLink contains two important concepts:
// nav-link.tsx
import { Link, LinkProps, useLocation } from "react-router-dom"
export type NavLinkProps = LinkProps
export function NavLink(props: NavLinkProps) {
const { pathname } = useLocation() // returns info about current route
return (
<Link
data-current={pathname === props.to} // if route is equal to where the link goes -> active
className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground data-[current=true]:text-foreground"
{...props}
></Link>
)
}
In this lesson, we learned how to implement a dark theme in an application using shadcn. We started by copying the ThemeProvider component and adding it to a folder called theme. We then added the ThemeProvider around our app and set some properties, such as the store key and the default theme.
We also added a button to toggle the theme and created the ThemeToggle component. Next, we installed DropDownMenu and added an account menu with options such as store profile and log out. Finally, we styled the menu and added icons and text.
Now we can move on to creating the rest of the look and feel of our application.
Add the component DrodpdownMenu:
pnpm dlx shadcn-ui@latest add dropdown-menu
Go to the shadcn/ui website, Dark mode section, and select Vite to follow the steps.
1. Create a folder called theme, and add two files, one theme-provider.tsx and another theme-toggle.tsx.
2. Add the ThemeToggle in the Header.tsx.
Create another component for the Menu Dropdown:
import { Building, ChevronDown, LogOut } from "lucide-react"
import { Button } from "./ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "./ui/dropdown-menu"
export function AccountMenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="flex select-none items-center gap-2"
>
Pizza Shop
<ChevronDown size={16}></ChevronDown>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-56"
>
<DropdownMenuLabel className="flex flex-col">
<span>Rui Neto</span>
<span className="text-xs font-normal text-muted-foreground">
ruiverganineto@gmail.com
</span>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Building className="mr-2 h-4 w-4"></Building>
<span>Shop Profile</span>
</DropdownMenuItem>
<DropdownMenuItem className="text-rose-500 dark:text-rose-400">
<LogOut className="mr-2 h-4 w-4"></LogOut>
<span>Exit</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
In this lesson, we will understand how changing themes works in an application. The ThemeProvider is a context that provides information about which theme is selected, as well as a function to change the theme.
ThemeToggle is responsible for changing the theme between Light, Dark and System, and adds the corresponding class to the HTML element. Tailwind uses this class to determine whether the application is in Light Mode or Dark Mode.
In this lesson, we will start building the more complex pages of the application. We will start with the order listing page, which has several elements. We will create a folder called orders and inside it a file called orders.tsx for the order page.
// orders.tsx
import { ArrowRight, Search, X } from "lucide-react"
import { Helmet } from "react-helmet-async"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
export function Orders() {
return (
<>
<Helmet title="Orders" />
<div className="flex flex-col gap-4">
<h1 className="text-3xl font-bold tracking-tight">Orders</h1>
<div className="space-y-2.5">
<form
action=""
className="flex items-center gap-2"
>
<span className="text-sm font-semibold">Filters:</span>
<Input
placeholder="Client name"
className="h-8 w-[320px]"
></Input>
</form>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[64px]"></TableHead>
<TableHead className="w-[140px]">Identificator</TableHead>
<TableHead className="w-[180px]">Made in</TableHead>
<TableHead className="w-[140px]">Status</TableHead>
<TableHead>Client</TableHead>
<TableHead className="w-[140px]">Total of Order</TableHead>
<TableHead className="w-[164px]"></TableHead>
<TableHead className="w-[132px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: 10 }).map((_, indice) => {
return (
<TableRow key={indice}>
<TableCell>
<Button
variant="outline"
size="xs"
>
<Search className="h-3 w-3"></Search>
<span className="sr-only">Details of order</span>
</Button>
</TableCell>
<TableCell className="font-mono text-xs font-medium">
03454545
</TableCell>
<TableCell className="text-muted-foreground">
15 minutes ago
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-slate-400"></span>
<span className="font-medium text-muted-foreground">
Pending
</span>
</div>
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-green-600"></span>
<span className="font-medium text-muted-foreground">
Completed
</span>
</div>
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-red-600"></span>
<span className="font-medium text-muted-foreground">
Cancelled
</span>
</div>
</TableCell>
<TableCell className="font-medium">
Rui Vergani Neto
</TableCell>
<TableCell className="font-medium">$250.00</TableCell>
<TableCell>
<Button
variant="outline"
size="xs"
>
<ArrowRight className="mr-2 h-3 w-3" />
Approve
</Button>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="xs"
>
<X className="mr-2 h-3 w-3"></X>
Cancel
</Button>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
</div>
</div>
</>
)
}
We will divide the page into several components for better organization. Next, we will add a title using the Helmet component from React Helmet. We will create a div that will contain the title and the order table. The table will have filters so the user can filter by order status and customer name.
We will adjust the size of the table columns for better organization. We will add buttons to open the order details and to approve or cancel the order, depending on the current status. Finally, we will add styles to the table using the Table component from shadcn/ui.
Thinking about accessibility, the property sr-only makes it accessible only to screen-readers.
<Button
variant="outline"
size="xs"
>
<Search className="h-3 w-3"></Search>
<span className="sr-only">Details of order</span>
</Button>
Also, it is important to adjust the routes file:
// routes.tsx
import { createBrowserRouter } from "react-router-dom"
import { AppLayout } from "./pages/_layouts/app"
import { AuthLayout } from "./pages/_layouts/auth"
import { Dashboard } from "./pages/app/dashboard"
import { Orders } from "./pages/app/orders/orders"
import { SignIn } from "./pages/auth/sign-in"
import { SignUp } from "./pages/auth/sign-up"
export const router = createBrowserRouter([
{
path: "/",
element: <AppLayout />,
children: [
{ path: "/", element: <Dashboard /> },
{ path: "/orders", element: <Orders /> },
],
},
{
path: "/",
element: <AuthLayout />,
children: [
{ path: "/sign-in", element: <SignIn /> },
{ path: "/sign-up", element: <SignUp /> },
],
},
])
In this lesson, we learned how to implement filters and pagination in an orders table. We started by creating a status filter, where the user could select a specific status or all statuses. Next, we added a search button to filter the results based on the selected filters.
We also added a button to remove the applied filters. Next, we implemented pagination of the results, allowing the user to navigate between pages of orders.
Finally, we created the OrderTableRow component to represent each row in the orders table.
// order-table-filters.tsx
import { Search, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
export function OrderTableFilter() {
return (
<form
action=""
className="flex items-center gap-2"
>
<span className="text-sm font-semibold">Filters:</span>
<Input
placeholder="ID of order"
className="h-8 w-auto"
></Input>
<Input
placeholder="Client name"
className="h-8 w-[320px]"
></Input>
<Select>
<SelectTrigger className="h-8 w-[180px]">
<SelectValue placeholder="Filter by Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="cancelled">Cancelled</SelectItem>
<SelectItem value="processing">Preparing</SelectItem>
<SelectItem value="delivering">Out for delivery</SelectItem>
<SelectItem value="delivered">Delivered</SelectItem>
</SelectContent>
</Select>
<Button
type="submit"
variant="secondary"
size="xs"
>
<Search
size="xs"
className="mr-2 h-4 w-4"
></Search>
Filter results
</Button>
<Button
type="button"
variant="outline"
size="xs"
>
<X
size="xs"
className="mr-2 h-4 w-4"
></X>
Remove filters
</Button>
</form>
)
}
// order-table-row.tsx
import { ArrowRight, Search, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { TableCell, TableRow } from "@/components/ui/table"
export function OrderTableRowCustom() {
return (
<>
<TableRow>
<TableCell>
<Button
variant="outline"
size="xs"
>
<Search className="h-3 w-3"></Search>
<span className="sr-only">Details of order</span>
</Button>
</TableCell>
<TableCell className="font-mono text-xs font-medium">
03454545
</TableCell>
<TableCell className="text-muted-foreground">15 minutes ago</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-slate-400"></span>
<span className="font-medium text-muted-foreground">Pending</span>
</div>
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-green-600"></span>
<span className="font-medium text-muted-foreground">Completed</span>
</div>
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-red-600"></span>
<span className="font-medium text-muted-foreground">Cancelled</span>
</div>
</TableCell>
<TableCell className="font-medium">Rui Vergani Neto</TableCell>
<TableCell className="font-medium">$250.00</TableCell>
<TableCell>
<Button
variant="outline"
size="xs"
>
<ArrowRight className="mr-2 h-3 w-3" />
Approve
</Button>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="xs"
>
<X className="mr-2 h-3 w-3"></X>
Cancel
</Button>
</TableCell>
</TableRow>
</>
)
}
In this lesson, we will work on implementing pagination in our application. Pagination is a generic functionality that can be used in any listing.
We will create the pagination component inside the components folder.
The pagination component will receive three main properties: pageIndex (which represents the current page), totalCount (which is the total number of records) and perPage (which is the number of records per page). We will calculate the total number of pages by dividing totalCount by perPage.
We will use the Math.ceil method to always round up, ensuring that we have the correct number of pages. If an error occurs in the division, we will return 1 page.
Next, we will create the basic structure of the component, displaying the total number of records and the navigation buttons. The buttons will be styled with icons and will have functionality added in the next lessons.
// pagination.tsx
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from "lucide-react"
import { Button } from "@/components/ui/button"
export interface PaginationProps {
pageIndex: number
totalCount: number
perPage: number
}
export function Pagination({
pageIndex,
totalCount,
perPage,
}: PaginationProps) {
const pages = Math.ceil(totalCount / perPage) || 1 // round up
return (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
Total of {totalCount} items(s)
</span>
<div className="flex items-center gap-6 lg:gap-8">
<div className="text-sm font-medium">
Page {pageIndex + 1} of {pages}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
className="h-8 w-8 p-0"
>
<ChevronsLeft className="h-4 w-4" />
<span className="sr-only">First Page</span>
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
>
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">Previous Page</span>
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
>
<ChevronRight className="h-4 w-4" />
<span className="sr-only">Next Page</span>
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
>
<ChevronsRight className="h-4 w-4" />
<span className="sr-only">Last Page</span>
</Button>
</div>
</div>
</div>
)
}
So you can test the component feel free to modify the orders.tsx
// orders.tsx
<Pagination
pageIndex={0}
totalCount={105}
perPage={10}
/>
In this lesson, we’ll create the visual structure for displaying order details in a modal. We’ll use the shadcn/ui Dialog component to build the modal. We’ll use DialogHeader for the header, DialogTitle for the title, and DialogDescription for the description.
We’ll then add a table to display the order information, with each row containing two cells: the name of the information and the corresponding value.
This is the component we’ll be using: https://ui.shadcn.com/docs/components/dialog
pnpm dlx shadcn-ui@latest add dialog
Create the trigger first:
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="xs"
>
<Search className="h-3 w-3"></Search>
<span className="sr-only">Details of order</span>
</Button>
</DialogTrigger>
<OrderDetails />
</Dialog>
Create the dialog file:
// order-details.tsx
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
export function OrderDetails() {
return (
<DialogContent>
<DialogHeader>
<DialogTitle>Order: 12345</DialogTitle>
<DialogDescription>Details or your Order</DialogDescription>
</DialogHeader>
<div className="space-y-6">
<Table>
<TableBody>
<TableRow>
<TableCell className="text-muted-foreground">Status</TableCell>
<TableCell className="flex justify-end">
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-red-600"></span>
<span className="font-medium text-muted-foreground">
Cancelled
</span>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground">Client</TableCell>
<TableCell className="flex justify-end">
Rui Vergani Neto
</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground">Phone</TableCell>
<TableCell className="flex justify-end">
+44 07543281851
</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground">E-mail</TableCell>
<TableCell className="flex justify-end">
ruiverganineto@gmail.com
</TableCell>
</TableRow>
<TableRow>
<TableCell className="text-muted-foreground">Made in</TableCell>
<TableCell className="flex justify-end">3 minutes ago</TableCell>
</TableRow>
</TableBody>
</Table>
<Table>
<TableHeader>
<TableRow>
<TableHead>Product</TableHead>
<TableHead className="text-right">Quantity</TableHead>
<TableHead className="text-right">Price</TableHead>
<TableHead className="text-right">Subtotal</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Pizza Pepperoni Family</TableCell>
<TableCell className="text-right">2</TableCell>
<TableCell className="text-right">R$ 69,90</TableCell>
<TableCell className="text-right">R$ 139,80</TableCell>
</TableRow>
<TableRow>
<TableCell>Pizza Mussarela Family</TableCell>
<TableCell className="text-right">2</TableCell>
<TableCell className="text-right">R$ 19,90</TableCell>
<TableCell className="text-right">R$ 49,80</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total of order</TableCell>
<TableCell className="text-right font-medium">
R$ 167,90
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
</DialogContent>
)
}
In this lesson, we’ll build the visual structure of the Dashboard page. We’ll start by creating a folder called “Dashboard” and within it a file called “Dashboard.tsx”.
We’ll then move the content into this folder and update the routes to import the Dashboard component from the correct location. From here, we’ll add a div with a title of “Dashboard” and separate the rest of the content into another div.
On the Dashboard page, we’ll have 4 top-level Cards that will display key business information, such as the number of orders for the month and the day. Below these Cards, we’ll have two larger charts, one for revenue over the period and one for historically top-selling products. Each Card will be a separate component, with its own file and import.
Install the card component:
pnpm dlx shadcn-ui@latest add card
// dashboard.tsx
import { Helmet } from "react-helmet-async"
import { DayOrdersAmountCard } from "./day-orders-amount-card"
import { CancelOrderAmountCard } from "./month-canceled-orders-amount-card"
import { MonthOrderAmount } from "./month-orders-amout-card"
import { MonthRevenueCard } from "./month-revenue-card"
export function Dashboard() {
return (
<>
<Helmet title="Dashboard" />
<div className="flex flex-col gap-4">
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
<div className="grid grid-cols-4 gap-4">
<MonthRevenueCard />
<MonthOrderAmount />
<DayOrdersAmountCard />
<CancelOrderAmountCard />
</div>
</div>
</>
)
}
// month-revenue-card.tsx
import { DollarSign } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export function MonthRevenueCard() {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base">Total Income (mes)</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground"></DollarSign>
</CardHeader>
<CardContent className="space-y-1">
<span className="text-2xl font-bold tracking-tight">R$ 1248,60</span>
<p className="text-xs text-muted-foreground">
<span className="text-emerald-500 dark:text-emerald-400">+2%</span>{" "}
relation with last month
</p>
</CardContent>
</Card>
)
}
// month-orders-amount.tsx
import { Utensils } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export function MonthOrderAmount() {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base">Orders (mes)</CardTitle>
<Utensils className="h-4 w-4 text-muted-foreground"></Utensils>
</CardHeader>
<CardContent className="space-y-1">
<span className="text-2xl font-bold tracking-tight">246</span>
<p className="text-xs text-muted-foreground">
<span className="text-emerald-500 dark:text-emerald-400">+6%</span>{" "}
relation with last month
</p>
</CardContent>
</Card>
)
}
// day-orders-amount-card.tsx
import { Utensils } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export function DayOrdersAmountCard() {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base">Orders (day)</CardTitle>
<Utensils className="h-4 w-4 text-muted-foreground"></Utensils>
</CardHeader>
<CardContent className="space-y-1">
<span className="text-2xl font-bold tracking-tight">12</span>
<p className="text-xs text-muted-foreground">
<span className="text-rose-500 dark:text-rose-400">-4%</span> relation
to yesterday
</p>
</CardContent>
</Card>
)
}
// month-canceled-orders-amount.tsx
import { Ban } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export function CancelOrderAmountCard() {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base">Cancelled (mes)</CardTitle>
<Ban className="h-4 w-4 text-muted-foreground"></Ban>
</CardHeader>
<CardContent className="space-y-1">
<span className="text-2xl font-bold tracking-tight">18</span>
<p className="text-xs text-muted-foreground">
<span className="text-emerald-500 dark:text-emerald-400">-2%</span>{" "}
relation to yesterday
</p>
</CardContent>
</Card>
)
}
In this lesson, we will create a revenue chart using the Recharts library. I will show you a similar example of what we will build and explain how we can customize it. We will create a component called RevenueChart.tsx and add it to our dashboard.
I will define the layout of the component and import the necessary components from Recharts, such as ResponsiveContainer, LineChart, XAxis, YAxis, CartesianGrid, Line, and Tooltip. I will define the size of the chart and style the elements using inline styles.
Line Chart example that will be used: https://recharts.org/en-US/examples/SimpleLineChart
I will create dummy data for the chart and pass it to the LineChart component. I will customize the appearance of the chart, such as the line color and axis labels. In the end, we will have a chart of daily revenue over the period.
Install Recharts library:
// Install library
pnpm i recharts
// revenue-chart.tsx
import {
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts"
import colors from "tailwindcss/colors"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
const data = [
{
date: "10/12",
revenue: 1200,
},
{
date: "11/12",
revenue: 1000,
},
{
date: "12/12",
revenue: 1700,
},
{
date: "13/12",
revenue: 900,
},
{
date: "14/12",
revenue: 800,
},
{
date: "15/12",
revenue: 2300,
},
{
date: "16/12",
revenue: 1600,
},
]
export function RevenueChart() {
return (
<Card className="col-span-6">
<CardHeader className="flex flex-row items-center justify-between pb-8">
<div className="space-y-1">
<CardTitle className="text-base font-medium">
Income in the period
</CardTitle>
<CardDescription>Daily Income in the period</CardDescription>
</div>
</CardHeader>
<CardContent>
<ResponsiveContainer
width="100%"
height={240}
>
<LineChart
style={{ fontSize: 12 }}
data={data}
>
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
dy={16}
/>
<YAxis
stroke="#888"
axisLine={false}
tickLine={false}
width={70}
tickFormatter={(value: number) =>
value.toLocaleString("en-GB", {
style: "currency",
currency: "GBP",
})
}
/>
<Line
type="linear"
strokeWidth={2}
dataKey="revenue"
stroke={colors.violet["500"]}
></Line>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
)
}
In this lesson, we will build the next chart, which will be a pie chart.
First, we will add the CartesianGrid component to the line chart we made earlier. Next, we will import the Pie and PieChart components from ReCharts to create the pie chart.
We will define the data that will be displayed in the chart, such as the product name and the total sold. We will also customize the colors and size of the pie slices.
Finally, we will add a legend to the chart. By the end of the lesson, we will have both charts ready to continue development.
// popular-products-chart.tsx
import { BarChart } from "lucide-react"
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"
import colors from "tailwindcss/colors"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
const data = [
{ product: "Pepperoni", amount: 50 },
{ product: "Mussarela", amount: 100 },
{ product: "Marguerita", amount: 10 },
{ product: "4 Cheese", amount: 26 },
{ product: "Chicken", amount: 40 },
]
const COLORS = [
colors.sky[500],
colors.amber[500],
colors.violet[500],
colors.emerald[500],
colors.rose[500],
]
export function PopularProductsChart() {
return (
<Card className="col-span-3">
<CardHeader className="flex flex-row items-center justify-between pb-8">
<div className="flex items-center justify-between space-y-1">
<CardTitle className="pr-2 text-base font-medium">
Popular Products
</CardTitle>
<CardDescription>
<BarChart className="h-4 w-4 text-muted-foreground" />
</CardDescription>
</div>
</CardHeader>
<CardContent>
<ResponsiveContainer
width="100%"
height={240}
>
<PieChart style={{ fontSize: 12 }}>
<Pie
dataKey="amount"
data={data}
nameKey="product"
cx="50%"
cy="50%"
outerRadius={86}
innerRadius={64}
strokeWidth={8}
labelLine={false}
label={({
cx,
cy,
midAngle,
innerRadius,
outerRadius,
value,
index,
}) => {
const RADIAN = Math.PI / 180
const radius = 12 + innerRadius + (outerRadius - innerRadius)
const x = cx + radius * Math.cos(-midAngle * RADIAN)
const y = cy + radius * Math.sin(-midAngle * RADIAN)
return (
<text
x={x}
y={y}
className="fill-muted-foreground text-xs"
textAnchor={x > cx ? 'start' : 'end'}
dominantBaseline="central"
>
{data[index].product.length > 12
? data[index].product.substring(0, 12).concat('...')
: data[index].product}{' '}
({value})
</text>
)
}}
>
{data.map((_, index) => {
return (
<Cell
key={`cell-${index}`}
fill={COLORS[index]}
className="stroke-background"
/>
)
})}
</Pie>
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
)
}
// label component code
label={({
cx,
cy,
midAngle,
innerRadius,
outerRadius,
value,
index,
}) => {
const RADIAN = Math.PI / 180
const radius = 12 + innerRadius + (outerRadius - innerRadius)
const x = cx + radius * Math.cos(-midAngle * RADIAN)
const y = cy + radius * Math.sin(-midAngle * RADIAN)
return (
<text
x={x}
y={y}
className="fill-muted-foreground text-xs"
textAnchor={x > cx ? 'start' : 'end'}
dominantBaseline="central"
>
{data[index].product.length > 12
? data[index].product.substring(0, 12).concat('...')
: data[index].product}{' '}
({value})
</text>
)
}}
In this lesson, before we start integrating our project with an API, we will make the last visual change. We will create a file called 404.tsx inside the Pages folder. In this file, we will export a page called “Not Found”.
It will have a div with some classes to center the content on the screen. We will have a title “Page not found” and a paragraph with a link to return to the dashboard. We will make the connection between this page and the errorElement component in our AppLayout. Now, when we try to access a non-existent page, we will be redirected to the 404 page.
// pages > 404.tsx
import { Link } from "react-router-dom"
export function NotFound() {
return (
<div className="flex h-screen flex-col items-center justify-center gap-2">
<h1 className="text-4xl font-bold">Page Not Found</h1>
<p className="text-accent-foreground">
Return to the{" "}
<Link
to="/"
className="text-sky-600 dark:text-sky-400"
>
Dashboard
</Link>
</p>
</div>
)
}
Change the routes.tsx file:
// routes.tsx
import { createBrowserRouter } from "react-router-dom"
import { AppLayout } from "./pages/_layouts/app"
import { AuthLayout } from "./pages/_layouts/auth"
import { Dashboard } from "./pages/app/dashboard/dashboard"
import { Orders } from "./pages/app/orders/orders"
import { SignIn } from "./pages/auth/sign-in"
import { SignUp } from "./pages/auth/sign-up"
import { NotFound } from "./pages/404"
export const router = createBrowserRouter([
{
path: "/",
element: <AppLayout />,
errorElement: <NotFound/>,
children: [
{ path: "/", element: <Dashboard /> },
{ path: "/orders", element: <Orders /> },
],
},
{
path: "/",
element: <AuthLayout />,
children: [
{ path: "/sign-in", element: <SignIn /> },
{ path: "/sign-up", element: <SignUp /> },
],
},
])
Check similiar articles below 🚀