Lesson 3.7: UI Implementation
Let's create the UI components for our Task Manager using the design system components following Atomic Design principles.
Adding shadcn/ui Components
Before implementing our components, we need to add the required shadcn/ui components to our design system. Follow these steps:
- Visit shadcn/ui Components
- Find the component you need (e.g., Alert, Skeleton)
- Use the CLI to add the component:
# Add Alert component
npx shadcn@latest add alert
# Add Skeleton component
npx shadcn@latest add skeleton
The CLI will:
- Add the component to
ds/atoms/
- Add necessary dependencies
- Update your
components.json
If the CLI installation fails, you can manually install components:
- Create the component file in
ds/atoms/
: - Add the component form their website manually
- make sure to change the
@/lib/utils
to@/_core/utils
when adding manually.
For example, to add the Alert component:
- Go to Alert Documentation
- Run
npx shadcn@latest add alert
- Import and use in your components:
import { Alert, AlertDescription } from "@/ds/atoms/alert";
Utility Functions
Before creating components, let's set up utility functions that can be used across the project.
Core Utilities
For utilities that are used across multiple modules, add them to the core utils:
// @/_core/utils/date.ts
export function formatDate(date: string | Date): string {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
export function isOverdue(date: string | Date): boolean {
return new Date(date) < new Date();
}
Module-Specific Utilities
For utilities specific to a module, keep them in the module's utils folder:
// modules/tasks/utils/index.ts
import { TaskNode } from '@/nodes/task-node';
export function getTaskStatus(task: TaskNode): 'completed' | 'pending' | 'overdue' {
if (task.completed) return 'completed';
if (task.dueDate && new Date(task.dueDate) < new Date()) return 'overdue';
return 'pending';
}
export function getPriorityColor(priority: string): string {
switch (priority) {
case 'high': return 'text-red-500';
case 'medium': return 'text-yellow-500';
case 'low': return 'text-green-500';
default: return 'text-gray-500';
}
}
Create Task Components
Task Card (Organism)
// ds/organisms/post-card.tsx
import { Card, CardContent, CardHeader, CardFooter } from "@/ds/atoms/card";
import { Badge } from "@/ds/atoms/badge";
import { Button } from "@/ds/atoms/button";
import { Checkbox } from "@/ds/atoms/checkbox";
import { formatDate } from "@/_core/utils/date";
import { PostNode } from "@/nodes/post-node";
interface PostCardProps {
post: PostNode;
onToggleComplete: (id: number) => void;
onDelete: (id: number) => void;
}
export function PostCard({ post, onToggleComplete, onDelete }: PostCardProps) {
return (
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between">
<div className="flex items-center gap-2">
<Checkbox
checked={post.completed}
onCheckedChange={() => onToggleComplete(post.id)}
/>
<h3
className={`text-lg font-semibold ${
post.completed ? "line-through" : ""
}`}
>
{post.title}
</h3>
</div>
<Badge variant={post.completed === true ? "destructive" : "default"}>
{post.completed ? "Completed" : "Pending"}
</Badge>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600">{post.description}</p>
</CardContent>
<CardFooter className="flex justify-end">
<Button
variant="destructive"
size="sm"
onClick={() => onDelete(post.id)}
>
Delete
</Button>
</CardFooter>
</Card>
);
}
Task List (Organism)
// ds/organisms/post-list.tsx
"use client";
import { PostCard } from "@/ds/organisms/post-card";
import { Skeleton } from "@/ds/atoms/skeleton";
import { usePosts } from "@/modules/post";
import { Alert, AlertDescription } from "../atoms/alert";
import { useEffect, useState } from "react";
export function PostList() {
const { posts, isLoading, error, actions } = usePosts();
const [formattedPosts, setFormattedPosts] = useState(posts);
useEffect(() => {
setFormattedPosts(
posts.map((post) => ({
...post,
createdAt: new Date(post.createdAt).toLocaleDateString(),
updatedAt: new Date(post.updatedAt).toLocaleDateString(),
}))
);
}, [posts]);
if (isLoading) {
return (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-32 w-full" />
))}
</div>
);
}
if (error) {
return (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
);
}
return (
<div className="space-y-4">
{formattedPosts.map((post) => (
<PostCard
key={post.id}
post={post}
onToggleComplete={actions.toggleComplete}
onDelete={actions.deletePost}
/>
))}
</div>
);
}
Task Form (Organism)
// ds/organisms/post-form.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { usePosts } from "@/modules/post";
import { PostFormData, postSchema } from "@/modules/post/schemas";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "../atoms/form";
import { Input } from "../atoms/input";
import { Textarea } from "../atoms/textarea";
import { Select } from "../atoms/select";
import { Button } from "../atoms/button";
export function PostForm() {
const { actions } = usePosts();
const form = useForm<PostFormData>({
resolver: zodResolver(postSchema),
defaultValues: {
title: "",
description: "",
priority: "medium",
tags: [],
},
});
const onSubmit = async (data: PostFormData) => {
await actions.addPost(data);
form.reset();
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>Priority</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Add Post</Button>
</form>
</Form>
);
}
Using and Creating Templates
Templates provide a consistent layout structure for your pages. You can either use existing templates from our design system or create custom ones.
Using Existing Templates
Our design system comes with several pre-built templates that you can usef rom ds/templates
. For our tutorial, let's build a custom template PostManagerTemplate
Creating Custom Templates
You can create custom templates by extending the base template structure. Here's an example of a custom post manager template:
// ds/templates/post-manager-template.tsx
import React, { ReactNode } from "react";
interface PostManagerTemplateProps {
children: ReactNode;
description: string;
actions?: ReactNode;
}
function PostManagerTemplate({
children,
description,
actions,
}: PostManagerTemplateProps) {
return (
<div className="min-h-screen flex flex-col">
<header className="sticky top-0 z-10 bg-background border-b p-4">
<div className="flex justify-between items-center">
<p className="text-muted-foreground">{description}</p>
{actions}
</div>
</header>
<main className="flex-1 p-6">{children}</main>
</div>
);
}
export default PostManagerTemplate;
Key features of this template:
- Sticky header with description and optional actions
- Flexible main content area
- Responsive layout
- Consistent spacing and styling
You can customize this template by:
- Adding more sections (sidebar, footer, etc.)
- Modifying the styling
- Including additional props for more flexibility
- Adding template-specific features
Update Task Manager Page
Now we have created the template and necessary organisms, its time to plug them in to template and build our module.
// modules/post/pages/index.tsx
i// modules/post/pages/index.tsx
import { Button } from "@/ds/atoms/button";
import { PostForm } from "@/ds/organisms/post-form";
import { PostList } from "@/ds/organisms/post-list";
import PostManagerTemplate from "@/ds/templates/post-manager-template";
export default function PostManagerPage() {
return (
<PostManagerTemplate
description="Manage your tasks efficiently"
actions={<Button variant="outline">Export Tasks</Button>}
>
<div className="grid gap-6 md:grid-cols-2">
<div>
<h2 className="text-xl font-semibold mb-4">Add New Task</h2>
<PostForm />
</div>
<div>
<h2 className="text-xl font-semibold mb-4">Tasks</h2>
<PostList />
</div>
</div>
</PostManagerTemplate>
);
}
Verify UI Implementation
- Check that all components render correctly
- Test form validation
- Verify task creation and updates
- Confirm stats update in real-time
- Test responsive layout
Next Steps
In the next step, we'll:
- Create and customize templates
- Implement proper layout structure
- Add template-specific features