UMBRA/frontend/src/components/projects/ProjectsPage.tsx
Kyle Pope d4ba98c6eb UI refresh: Projects pages (Phase 4)
- 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>
2026-02-22 02:56:07 +08:00

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>
);
}