217 lines
6.9 KiB
TypeScript
217 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { Plus, Trash2, CheckCircle, Clock, Circle } 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 { Task, fetchTasks, deleteTask } from "@/lib/api";
|
|
|
|
const statusIcons = {
|
|
pending: Circle,
|
|
in_progress: Clock,
|
|
completed: CheckCircle,
|
|
};
|
|
|
|
const statusColors = {
|
|
pending: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
|
|
in_progress: "bg-blue-500/20 text-blue-400 border-blue-500/30",
|
|
completed: "bg-green-500/20 text-green-400 border-green-500/30",
|
|
};
|
|
|
|
const priorityColors = {
|
|
low: "bg-slate-500/20 text-slate-400 border-slate-500/30",
|
|
medium: "bg-orange-500/20 text-orange-400 border-orange-500/30",
|
|
high: "bg-red-500/20 text-red-400 border-red-500/30",
|
|
};
|
|
|
|
export default function Dashboard() {
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const loadTasks = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await fetchTasks();
|
|
setTasks(data);
|
|
setError(null);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to load tasks");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadTasks();
|
|
}, []);
|
|
|
|
const handleDelete = async (id: number) => {
|
|
try {
|
|
await deleteTask(id);
|
|
setTasks(tasks.filter((t) => t.id !== id));
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to delete task");
|
|
}
|
|
};
|
|
|
|
const stats = {
|
|
total: tasks.length,
|
|
pending: tasks.filter((t) => t.status === "pending").length,
|
|
inProgress: tasks.filter((t) => t.status === "in_progress").length,
|
|
completed: tasks.filter((t) => t.status === "completed").length,
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
<div className="text-muted-foreground">Loading tasks...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Stats Overview */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Total Tasks
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">{stats.total}</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Pending
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold text-yellow-400">
|
|
{stats.pending}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
In Progress
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold text-blue-400">
|
|
{stats.inProgress}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Completed
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold text-green-400">
|
|
{stats.completed}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-xl font-semibold">Tasks</h2>
|
|
<Link href="/tasks/new">
|
|
<Button>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Add Task
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
{error && (
|
|
<div className="bg-destructive/20 text-destructive-foreground border border-destructive/30 rounded-lg p-4">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Task List */}
|
|
{tasks.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
<p className="text-muted-foreground mb-4">No tasks yet</p>
|
|
<Link href="/tasks/new">
|
|
<Button>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Create your first task
|
|
</Button>
|
|
</Link>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="grid gap-4">
|
|
{tasks.map((task) => {
|
|
const StatusIcon = statusIcons[task.status];
|
|
return (
|
|
<Card
|
|
key={task.id}
|
|
className="hover:border-primary/50 transition-colors"
|
|
>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex items-start gap-3 flex-1">
|
|
<StatusIcon className="h-5 w-5 mt-0.5 text-muted-foreground" />
|
|
<div className="flex-1 min-w-0">
|
|
<Link
|
|
href={`/tasks/${task.id}`}
|
|
className="font-medium hover:text-primary transition-colors"
|
|
>
|
|
{task.title}
|
|
</Link>
|
|
{task.description && (
|
|
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
|
{task.description}
|
|
</p>
|
|
)}
|
|
<div className="flex gap-2 mt-2">
|
|
<Badge
|
|
variant="outline"
|
|
className={statusColors[task.status]}
|
|
>
|
|
{task.status.replace("_", " ")}
|
|
</Badge>
|
|
<Badge
|
|
variant="outline"
|
|
className={priorityColors[task.priority]}
|
|
>
|
|
{task.priority}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="text-muted-foreground hover:text-destructive"
|
|
onClick={() => handleDelete(task.id)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|