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

@@ -0,0 +1,166 @@
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { useCreateProject, useUpdateProject } from '@/lib/hooks/use-projects';
import type { Project } from '@/types/project';
import { toast } from 'sonner';
import { Loader2 } from 'lucide-react';
const projectSchema = z.object({
name: z
.string()
.min(1, 'Name is required')
.max(100, 'Name must be less than 100 characters'),
key: z
.string()
.min(3, 'Key must be at least 3 characters')
.max(10, 'Key must be less than 10 characters')
.regex(/^[A-Z]+$/, 'Key must be uppercase letters only'),
description: z
.string()
.max(500, 'Description must be less than 500 characters')
.optional(),
});
type ProjectFormValues = z.infer<typeof projectSchema>;
interface ProjectFormProps {
project?: Project;
onSuccess?: () => void;
onCancel?: () => void;
}
export function ProjectForm({ project, onSuccess, onCancel }: ProjectFormProps) {
const isEditing = !!project;
const createProject = useCreateProject();
const updateProject = useUpdateProject(project?.id || '');
const form = useForm<ProjectFormValues>({
resolver: zodResolver(projectSchema),
defaultValues: {
name: project?.name || '',
key: project?.key || '',
description: project?.description || '',
},
});
async function onSubmit(data: ProjectFormValues) {
try {
if (isEditing) {
await updateProject.mutateAsync(data);
toast.success('Project updated successfully');
} else {
await createProject.mutateAsync(data);
toast.success('Project created successfully');
}
onSuccess?.();
} catch (error) {
const message = error instanceof Error ? error.message : 'Operation failed';
toast.error(message);
}
}
const isLoading = createProject.isPending || updateProject.isPending;
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Project Name *</FormLabel>
<FormControl>
<Input placeholder="e.g., ColaFlow" {...field} />
</FormControl>
<FormDescription>
The display name for your project
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="key"
render={({ field }) => (
<FormItem>
<FormLabel>Project Key *</FormLabel>
<FormControl>
<Input
placeholder="e.g., COLA"
{...field}
onChange={(e) => {
// Auto-uppercase
const value = e.target.value.toUpperCase();
field.onChange(value);
}}
maxLength={10}
/>
</FormControl>
<FormDescription>
3-10 uppercase letters (used in issue IDs like COLA-123)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Brief description of the project..."
className="resize-none"
rows={4}
{...field}
/>
</FormControl>
<FormDescription>
Optional description of your project (max 500 characters)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-3">
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isLoading}
>
Cancel
</Button>
)}
<Button type="submit" disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isEditing ? 'Update Project' : 'Create Project'}
</Button>
</div>
</form>
</Form>
);
}