feat(frontend): Implement Phase 2 - Complete Projects UI with CRUD operations

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>
This commit is contained in:
Yaojia Wang
2025-11-04 21:26:02 +01:00
parent e52c8300de
commit 2b134b0d6f
11 changed files with 975 additions and 217 deletions

View File

@@ -2,69 +2,66 @@
import { useState } from 'react';
import Link from 'next/link';
import { Plus, Loader2 } from 'lucide-react';
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 { CreateProjectDialog } from '@/components/features/projects/CreateProjectDialog';
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();
// Log state for debugging
console.log('[ProjectsPage] State:', {
isLoading,
error,
projects,
apiUrl: process.env.NEXT_PUBLIC_API_URL,
});
if (isLoading) {
return (
<div className="flex h-[50vh] items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<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) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api/v1';
console.error('[ProjectsPage] Error loading projects:', error);
return (
<div className="flex h-[50vh] items-center justify-center">
<div className="flex items-center justify-center min-h-[400px]">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-red-600">Failed to Load Projects</CardTitle>
<CardDescription>Unable to connect to the backend API</CardDescription>
<CardTitle className="text-destructive">Error Loading Projects</CardTitle>
<CardDescription>
{error instanceof Error ? error.message : 'Failed to load projects'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<p className="text-sm font-medium">Error Details:</p>
<p className="text-sm text-muted-foreground">{errorMessage}</p>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">API URL:</p>
<p className="text-sm font-mono text-muted-foreground">{apiUrl}</p>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Troubleshooting Steps:</p>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
<li>Check if the backend server is running</li>
<li>Verify the API URL in .env.local</li>
<li>Check browser console (F12) for detailed errors</li>
<li>Check network tab (F12) for failed requests</li>
</ul>
</div>
<Button
onClick={() => window.location.reload()}
className="w-full"
>
Retry
</Button>
<CardContent>
<Button onClick={() => window.location.reload()}>Retry</Button>
</CardContent>
</Card>
</div>
@@ -73,10 +70,11 @@ export default function ProjectsPage() {
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">
<p className="text-muted-foreground mt-1">
Manage your projects and track progress
</p>
</div>
@@ -86,51 +84,71 @@ export default function ProjectsPage() {
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{projects?.map((project) => (
<Link key={project.id} href={`/projects/${project.id}`}>
<Card className="transition-colors hover:bg-accent">
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-1">
<CardTitle>{project.name}</CardTitle>
<CardDescription>{project.key}</CardDescription>
{/* 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>
<span
className={`rounded-full px-2 py-1 text-xs font-medium ${
project.status === 'Active'
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-700'
}`}
>
{project.status}
</span>
</div>
</CardHeader>
<CardContent>
<p className="line-clamp-2 text-sm text-muted-foreground">
{project.description}
</p>
</CardContent>
</Card>
</Link>
))}
</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>
)}
{!projects || projects.length === 0 ? (
<Card className="col-span-full">
<CardContent className="flex h-40 items-center justify-center">
<p className="text-sm text-muted-foreground">
No projects yet. Create your first project to get started.
</p>
</CardContent>
</Card>
) : null}
</div>
<CreateProjectDialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
/>
{/* 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>
);
}