Day three - The Front End
For the front-end, let's keep it simple, because the main goal of this project is the CI/CD pipeline.
Let's install some dependencies:
pnpm add dayjs next-themes ky cookies-next pnpm add -D tailwind-variants @faker-js/faker @next/eslint-plugin-next @types/react @testing-library/jest-dom @testing-library/react @testing-library/user-event happy-dom @playwright/test @vitejs/plugin-react npx shadcn-ui@latest add avatar button dropdown-menu label input checkbox separator
Creating the a simple and responsive task page and components:
I've chosen to use server components with server actions, with useTransition hooks, because in the next React19 version it will be more common to use them, so, why not practice in the current version?
For example, to work with the create task use case:
The form component
// ./apps/web/src/components/new-task-form.tsx 'use client' import { Plus } from 'lucide-react' import { useRouter } from 'next/navigation' import { useTransition } from 'react' import { createTaskAction } from '../app/tasks/actions' import { Button } from './ui/button' import { Input } from './ui/input' const NewTaskForm = () => { const [isPending, startTransition] = useTransition() const router = useRouter() const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault() const formData = new FormData(e.currentTarget) e.currentTarget.reset() startTransition(async () => { await createTaskAction(formData) router.refresh() }) } return ( <form onSubmit={handleSubmit}> <div className="mx-auto flex w-full max-w-xl items-center space-x-2 rounded-lg border-2 border-muted p-4 has-[:focus-visible]:ring-2"> <Input type="text" name="title" placeholder="Create a new task" className="focus-visible: rounded-none border-0 outline-none focus-visible:ring-0" /> <Button type="submit" disabled={isPending}> <Plus className="size-4" /> <span className="sr-only">Create new task</span> </Button> </div> </form> ) } NewTaskForm.displayName = 'NewTaskForm' export { NewTaskForm }
The server action
// ./apps/web/src/app/tasks/actions.ts 'use server' import { HTTPError } from 'ky' import { createTask } from '../../http/create-task' export const createTaskAction = async (data: FormData) => { const formDataValidationResult = createTaskSchema.safeParse( Object.fromEntries(data), ) if (!formDataValidationResult.success) { return false } const { title } = formDataValidationResult.data try { await createTask({ title }) return true } catch (error) { if (error instanceof HTTPError) { const { message } = await error.response.json() console.log(message) // Todo: To be implemented } return false } }
And finally, the http request, using the ky
library:
// ./apps/web/src/http/create-task.ts import { api } from '../lib/ky' type CreateTaskRequest = { title: string } export async function createTask({ title }: CreateTaskRequest): Promise<void> { await api.post(`tasks`, { json: { title, }, }) }
Contribute to the Project
If you found this post helpful or have suggestions for improvement, feel free to check out the project repository on GitHub. You are welcome to fork the repository and submit a pull request. If you have any questions or want to discuss a topic, please open an issue. We appreciate your contributions!