Skip to main content

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:

  1. Visit shadcn/ui Components
  2. Find the component you need (e.g., Alert, Skeleton)
  3. 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:

  1. Create the component file in ds/atoms/:
  2. Add the component form their website manually
  3. make sure to change the @/lib/utils to @/_core/utils when adding manually.

For example, to add the Alert component:

  1. Go to Alert Documentation
  2. Run npx shadcn@latest add alert
  3. 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:

  1. Adding more sections (sidebar, footer, etc.)
  2. Modifying the styling
  3. Including additional props for more flexibility
  4. 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

  1. Check that all components render correctly
  2. Test form validation
  3. Verify task creation and updates
  4. Confirm stats update in real-time
  5. Test responsive layout

Next Steps

In the next step, we'll:

  1. Create and customize templates
  2. Implement proper layout structure
  3. Add template-specific features

Continue to Summary and Best practices →