Files
ColaFlow-Web/app/(dashboard)/projects/page.tsx
Yaojia Wang 99ba4c4b1a feat(frontend): improve error handling and UX - Sprint 3 Story 4
Add comprehensive error handling with Error Boundary and improve user feedback.

Changes:
- Created global ErrorBoundary component with fallback UI using react-error-boundary
- Integrated ErrorBoundary in root layout to catch all errors
- Created Loading component with variants (sm, md, lg) for consistent loading states
- Created EmptyState component for better empty data display with CTAs
- Improved form error messages in login and register pages (consistent destructive styling)
- Updated projects page to use EmptyState component
- Added better error handling with retry actions

UX improvements:
- Better error messages and recovery options with clear action buttons
- Consistent loading indicators across all pages
- Helpful empty states with clear descriptions and CTAs
- Graceful error handling without crashes
- Consistent destructive color theme for all error messages

Technical:
- Installed react-error-boundary package (v5)
- All TypeScript types are properly defined
- Build and type checking pass successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 20:04:00 +01:00

150 lines
5.2 KiB
TypeScript

'use client';
import { useState } from 'react';
import Link from 'next/link';
import { Plus, FolderKanban, Calendar, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, 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';
import { EmptyState } from '@/components/ui/empty-state';
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 (
<EmptyState
icon={AlertCircle}
title="Failed to load projects"
description={error instanceof Error ? error.message : 'An error occurred while loading projects. Please try again.'}
action={{
label: 'Retry',
onClick: () => window.location.reload(),
}}
/>
);
}
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>
) : (
<EmptyState
icon={FolderKanban}
title="No projects yet"
description="Get started by creating your first project to organize your work and track progress."
action={{
label: 'Create Project',
onClick: () => setIsCreateDialogOpen(true),
}}
/>
)}
{/* 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>
);
}