Go Back to Home
image featured post
React JS

Dashboard Pizza Shop using React.js. Part 2 – Pages and Components (UI)

  • Rui Vergani Neto
  • 6, August, 2024
  • 17 min read
Search through topics

In this part of the tutorial, we will create the pages and components UI using the shadcn/ui library.

Page: Login

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>
  )
}

Using React Hook Form

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.

  • register is used to register fields in the form, while handleSubmit handles the submission of the form, avoiding the default redirect.

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>
    </>
  )
}

Notifications Toast (Sooner)

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.")
    }
  }

Page: Register the restaurant

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 /> },
    ],
  },
])

Layout of app with header

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:

  • using JavaScript attributes such as data-current, you can style these components in the TailwindCSS, for example, the pathname (from the route) is compared to the props.to (the link that will direct) so let’s say the pathname is /orders and the link when this component is called is /orders, the ‘active’ style will be needed.
// 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>
  )
}

ThemeToggle and menu of account

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>
  )
}

Explanation of Theme Toggle

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.

Page Listing of orders

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 /> },
    ],
  },
])

Component: Filtering of order

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>
    </>
  )
}

Component: Pagination

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}
/>

Component: Details of order

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>
  )
}

Page: Dashboard

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>
  )
}

Graphic of income during the period

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

  • ResponsiveContainer = Adds the responsive container around the graph
  • LineChart = the line of the graph, and you can customise the line width, height, data, margin, etc..
  • XAxis = the XAxis can be customizable.
  • YAxis = the YAxis can be customizable.
  • CartesianGrid =
  • Line = configure the type of line you want to display.
  • Tooltip =

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>
  )
}

Graphic of popular product

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>
  )
}}

Page 404 (Not Found)

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 /> },
    ],
  },
])

Similar Articles

Check similiar articles below 🚀