- ProjectsPage: h-16 header, segmented pill filter, summary stat cards - ProjectCard: sentence case, stylesheet status colors, hover glow, overdue dates - ProjectDetail: summary progress card, compact task rows, overdue highlighting - ProjectForm: Dialog → Sheet migration, 2-column layout, delete in footer - TaskForm: Dialog → Sheet migration, 2-column grid layout, delete in footer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
5.6 KiB
TypeScript
147 lines
5.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { Plus, FolderKanban, Layers, PlayCircle, CheckCircle2 } from 'lucide-react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import api from '@/lib/api';
|
|
import type { Project } from '@/types';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { GridSkeleton } from '@/components/ui/skeleton';
|
|
import { EmptyState } from '@/components/ui/empty-state';
|
|
import ProjectCard from './ProjectCard';
|
|
import ProjectForm from './ProjectForm';
|
|
|
|
const statusFilters = [
|
|
{ value: '', label: 'All' },
|
|
{ value: 'not_started', label: 'Not Started' },
|
|
{ value: 'in_progress', label: 'In Progress' },
|
|
{ value: 'completed', label: 'Completed' },
|
|
] as const;
|
|
|
|
export default function ProjectsPage() {
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
|
const [statusFilter, setStatusFilter] = useState('');
|
|
|
|
const { data: projects = [], isLoading } = useQuery({
|
|
queryKey: ['projects'],
|
|
queryFn: async () => {
|
|
const { data } = await api.get<Project[]>('/projects');
|
|
return data;
|
|
},
|
|
});
|
|
|
|
const filteredProjects = statusFilter
|
|
? projects.filter((p) => p.status === statusFilter)
|
|
: projects;
|
|
|
|
const inProgressCount = projects.filter((p) => p.status === 'in_progress').length;
|
|
const completedCount = projects.filter((p) => p.status === 'completed').length;
|
|
|
|
const handleEdit = (project: Project) => {
|
|
setEditingProject(project);
|
|
setShowForm(true);
|
|
};
|
|
|
|
const handleCloseForm = () => {
|
|
setShowForm(false);
|
|
setEditingProject(null);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{/* Header */}
|
|
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
|
|
<h1 className="font-heading text-2xl font-bold tracking-tight">Projects</h1>
|
|
|
|
<div className="flex items-center rounded-md border border-border overflow-hidden ml-4">
|
|
{statusFilters.map((sf) => (
|
|
<button
|
|
key={sf.value}
|
|
onClick={() => setStatusFilter(sf.value)}
|
|
className={`px-3 py-1.5 text-sm font-medium transition-colors duration-150 ${
|
|
statusFilter === sf.value
|
|
? 'bg-accent/15 text-accent'
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-card-elevated'
|
|
}`}
|
|
style={{
|
|
backgroundColor: statusFilter === sf.value ? 'hsl(var(--accent-color) / 0.15)' : undefined,
|
|
color: statusFilter === sf.value ? 'hsl(var(--accent-color))' : undefined,
|
|
}}
|
|
>
|
|
{sf.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex-1" />
|
|
|
|
<Button onClick={() => setShowForm(true)} size="sm">
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
New Project
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto px-6 py-5">
|
|
{/* Summary stats */}
|
|
{!isLoading && projects.length > 0 && (
|
|
<div className="grid gap-2.5 grid-cols-3 mb-5">
|
|
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
|
|
<CardContent className="p-4 flex items-center gap-3">
|
|
<div className="p-1.5 rounded-md bg-blue-500/10">
|
|
<Layers className="h-4 w-4 text-blue-400" />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] tracking-wider uppercase text-muted-foreground">Total</p>
|
|
<p className="font-heading text-xl font-bold tabular-nums">{projects.length}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
|
|
<CardContent className="p-4 flex items-center gap-3">
|
|
<div className="p-1.5 rounded-md bg-purple-500/10">
|
|
<PlayCircle className="h-4 w-4 text-purple-400" />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] tracking-wider uppercase text-muted-foreground">In Progress</p>
|
|
<p className="font-heading text-xl font-bold tabular-nums">{inProgressCount}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
|
|
<CardContent className="p-4 flex items-center gap-3">
|
|
<div className="p-1.5 rounded-md bg-green-500/10">
|
|
<CheckCircle2 className="h-4 w-4 text-green-400" />
|
|
</div>
|
|
<div>
|
|
<p className="text-[10px] tracking-wider uppercase text-muted-foreground">Completed</p>
|
|
<p className="font-heading text-xl font-bold tabular-nums">{completedCount}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{isLoading ? (
|
|
<GridSkeleton cards={6} />
|
|
) : filteredProjects.length === 0 ? (
|
|
<EmptyState
|
|
icon={FolderKanban}
|
|
title="No projects yet"
|
|
description="Create your first project to start tracking tasks and progress."
|
|
actionLabel="New Project"
|
|
onAction={() => setShowForm(true)}
|
|
/>
|
|
) : (
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{filteredProjects.map((project) => (
|
|
<ProjectCard key={project.id} project={project} onEdit={handleEdit} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{showForm && <ProjectForm project={editingProject} onClose={handleCloseForm} />}
|
|
</div>
|
|
);
|
|
}
|