Implemented comprehensive Projects UI with full CRUD functionality following modern React best practices and using shadcn/ui components. Changes: - Created ProjectForm component with react-hook-form + zod validation - Auto-uppercase project key input - Comprehensive field validation (name, key, description) - Support for both create and edit modes - Toast notifications for success/error states - Enhanced Projects List Page (app/(dashboard)/projects/page.tsx) - Beautiful card-based grid layout with hover effects - Skeleton loading states for better UX - Empty state with call-to-action - Project metadata display (key badge, created date) - Integrated ProjectForm in Dialog for creation - Enhanced Project Detail Page (app/(dashboard)/projects/[id]/page.tsx) - Comprehensive project information display - Edit functionality with dialog form - Delete functionality with confirmation AlertDialog - Epics preview section with stats - Quick actions sidebar (Kanban, Epics) - Statistics card (Total/Active/Completed epics) - Skeleton loading states - Error handling with retry capability - Added toast notifications (Sonner) - Installed and configured sonner package - Added Toaster component to root layout - Success/error notifications for all CRUD operations - Installed required dependencies - date-fns for date formatting - sonner for toast notifications - shadcn/ui alert-dialog component Technical highlights: - TypeScript with strict type checking - React Query for data fetching and caching - Optimistic updates with automatic rollback - Responsive design (mobile-friendly) - Accessibility-focused components - Clean error handling throughout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
155 lines
5.6 KiB
TypeScript
155 lines
5.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { Plus, FolderKanban, Calendar } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { useProjects } from '@/lib/hooks/use-projects';
|
|
import { ProjectForm } from '@/components/projects/project-form';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
|
|
export default function ProjectsPage() {
|
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
const { data: projects, isLoading, error } = useProjects();
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<Skeleton className="h-9 w-48" />
|
|
<Skeleton className="h-5 w-64 mt-2" />
|
|
</div>
|
|
<Skeleton className="h-10 w-32" />
|
|
</div>
|
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<Card key={i}>
|
|
<CardHeader>
|
|
<Skeleton className="h-6 w-3/4" />
|
|
<Skeleton className="h-4 w-1/2 mt-2" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Skeleton className="h-16 w-full" />
|
|
<Skeleton className="h-4 w-32 mt-4" />
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
<Card className="w-full max-w-md">
|
|
<CardHeader>
|
|
<CardTitle className="text-destructive">Error Loading Projects</CardTitle>
|
|
<CardDescription>
|
|
{error instanceof Error ? error.message : 'Failed to load projects'}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Button onClick={() => window.location.reload()}>Retry</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Projects</h1>
|
|
<p className="text-muted-foreground mt-1">
|
|
Manage your projects and track progress
|
|
</p>
|
|
</div>
|
|
<Button onClick={() => setIsCreateDialogOpen(true)}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
New Project
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Projects Grid */}
|
|
{projects && projects.length > 0 ? (
|
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
{projects.map((project) => (
|
|
<Link key={project.id} href={`/projects/${project.id}`}>
|
|
<Card className="h-full transition-all hover:shadow-lg hover:border-primary cursor-pointer">
|
|
<CardHeader>
|
|
<div className="flex items-start justify-between">
|
|
<div className="space-y-1 flex-1">
|
|
<CardTitle className="line-clamp-1">{project.name}</CardTitle>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="secondary">{project.key}</Badge>
|
|
</div>
|
|
</div>
|
|
<FolderKanban className="h-5 w-5 text-muted-foreground flex-shrink-0 ml-2" />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{project.description ? (
|
|
<p className="text-sm text-muted-foreground line-clamp-3">
|
|
{project.description}
|
|
</p>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground italic">
|
|
No description
|
|
</p>
|
|
)}
|
|
<div className="flex items-center text-xs text-muted-foreground">
|
|
<Calendar className="mr-1 h-3 w-3" />
|
|
Created {formatDistanceToNow(new Date(project.createdAt), { addSuffix: true })}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Card className="flex flex-col items-center justify-center py-16">
|
|
<FolderKanban className="h-12 w-12 text-muted-foreground mb-4" />
|
|
<CardTitle className="mb-2">No projects yet</CardTitle>
|
|
<CardDescription className="mb-4">
|
|
Get started by creating your first project
|
|
</CardDescription>
|
|
<Button onClick={() => setIsCreateDialogOpen(true)}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Create Project
|
|
</Button>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Create Project Dialog */}
|
|
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Project</DialogTitle>
|
|
<DialogDescription>
|
|
Add a new project to organize your work and track progress
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<ProjectForm
|
|
onSuccess={() => setIsCreateDialogOpen(false)}
|
|
onCancel={() => setIsCreateDialogOpen(false)}
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|