Starting an admin dashboard with Typescript, React and Tailwind

Table of contents

Introduction

Today we will start building a dashboard from scratch, not only using frontend tools but also building the api in the backend. Last post we start a simple crud application with bun, typescript and hono, we’re gonna work using that as a base to make our dashboard. I’m not entirely sure what the business logic will be, but I want it to have the following:

We’re gonna make this like it’s a real life job where we get tickets and make tasks out of them to prepare you better.

Setup

Would you like to use TypeScript? yes
Which style would you like to use? Default
Which color would you like to use as base color? Slate
Where is your global CSS file? src/main.css
Do you want to use CSS variables for colors? yes
Where is your tailwind.config.js located? tailwind.config.js
Configure the import alias for components: @/components
Configure the import alias for utils: @/lib/utils
Are you using React Server Components? no
npx install react-router-dom

Auth task description

We’re gonna create a form component for our login and register forms.

Title: Create an auth form component Description:

Use shadcn to build the component

Add form with npx shadcn-ui@latest add form, button with npx shadcn-ui@latest add button, card with npx shadcn-ui@latest add card, input with npx shadcn-ui@latest add input.

Create a folder inside components called auth and inside add auth-form.tsx.

We’re gonna create a form inside a card, lets do the imports first:

import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
} from "@/components/ui/form";
import { z } from "zod";
import { useForm } from "react-hook-form";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "../ui/input";
import { Button } from "../ui/button";

Next let’s add the formSchema to validate our inputs:

const formSchema = z.object({
  username: z.string().trim().toLowerCase().email(),
  password: z.string().min(8).max(255),
});

export type FormValues = z.infer<typeof formSchema>;

The idea of the formSchema is to define the expected structure and data types for your form inputs. This ensures that only valid data gets submitted to your application, preventing unexpected errors or security vulnerabilities.

To get the type we use zod type inference capabilities in TypeScript to extract the type information from your formSchema.

Next we create our AuthForm component:

const AuthForm = () => {
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      password: "",
    },
  });
  return <Form {...form}></Form>;
};

The form object manages the state and behavior of your form, while the resolver is you’re essentially telling React Hook Form to use Zod to validate the form data based on the rules defined in your formSchema.

Now let’s add the rest of the components inside our form:

const AuthForm = (props: Props) => {
  const form =
    useForm <
    FormValues >
    {
      resolver: zodResolver(formSchema),
      defaultValues: {
        username: "",
        password: "",
      },
    };
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(props.onSubmit)}>
        <fieldset>
          <Card className="w-full max-w-sm">
            <CardHeader>
              <CardTitle className="text-2xl">Signup</CardTitle>
              <CardDescription>
                Enter your data below to signup.
              </CardDescription>
            </CardHeader>
            <CardContent className="grid gap-4">
              <FormField
                control={form.control}
                name="username"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Username</FormLabel>
                    <FormControl>
                      <Input
                        type="email"
                        placeholder="enter email"
                        {...field}
                      />
                    </FormControl>
                    <FormDescription>
                      We wont share your email with anybody.
                    </FormDescription>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="password"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Password</FormLabel>
                    <FormControl>
                      <Input
                        type="password"
                        placeholder="enter password"
                        {...field}
                      />
                    </FormControl>
                    <FormDescription>
                      Try to make it as secure as possible.
                    </FormDescription>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </CardContent>
            <CardFooter>
              <Button type="submit" className="w-full">
                Sign up
              </Button>
            </CardFooter>
          </Card>
        </fieldset>
      </form>
    </Form>
  );
};

We added some tailwind classnames to make it look better and a onSubmit prop.

The pattern is basically use the Form from our components folder which will wrap a common HTML form and inside we will show a card which will have a header and a body, where inside the body we put our form fields for username and password, where each will render an item which wraps a Label, the Input component and a description.

Routes

Create a folder called routes and inside add a file called signup.tsx.

import { z } from "zod";
import AuthForm from "@/components/auth/auth-form";

const formSchema = z.object({
  username: z.string().trim().toLowerCase().email(),
  password: z.string().min(8).max(255),
});

type FormValues = z.infer<typeof formSchema>;

export default function SignupRoute() {
  function onSubmit(values: FormValues) {
    console.log("Succesfully registed", values.username);
  }

  return (
    <div className="min-h-screen flex items-center justify-center px-4">
      <AuthForm onSubmit={onSubmit} />
    </div>
  );
}

We will add its respective functionalities later on. For now lets add the following inside our app.tsx file:

import { Route, Routes } from "react-router-dom";
import SignupRoute from "./routes/signup";

export default function App() {
  return (
    <Routes>
      <Route path="/signup" element={<SignupRoute />} />
      <Route path="/" element={<div>Hello</div>} />
    </Routes>
  );
}

And in main.tsx:

import { StrictMode, Suspense } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./main.css";
import App from "./app.tsx";

const rootNode = document.getElementById("root")!;

rootNode &&
  createRoot(rootNode).render(
    <StrictMode>
      <Suspense fallback="Loading...">
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </Suspense>
    </StrictMode>,
  );

Finally run npm run dev and go to /signup to see:

Signup

Conclusion

We succesfully set the project and made our first component! We need to make it better but its a good start and didn’t want to make it longer. We need to make it behave differently for when its login or register since its basically the same component, but we will do that on the next one.

See you on the next post.

Sincerely,

Eng. Adrian Beria.