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>
150 lines
5.2 KiB
TypeScript
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>
|
|
);
|
|
}
|