231 lines
7.7 KiB
TypeScript
231 lines
7.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { AddTaskDialog } from "@/components/add-task-dialog";
|
|
import { TaskDetailDialog } from "@/components/task-detail-dialog";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import { Plus, CheckCircle2, Clock, AlertCircle, Trash2 } from "lucide-react";
|
|
|
|
interface Task {
|
|
id: number;
|
|
title: string;
|
|
description: string;
|
|
status: string;
|
|
priority: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
const statusConfig: Record<string, { label: string; icon: React.ReactNode; variant: "default" | "secondary" | "destructive" | "outline" }> = {
|
|
pending: { label: "Pending", icon: <Clock className="w-3 h-3" />, variant: "secondary" },
|
|
in_progress: { label: "In Progress", icon: <AlertCircle className="w-3 h-3" />, variant: "default" },
|
|
completed: { label: "Completed", icon: <CheckCircle2 className="w-3 h-3" />, variant: "outline" },
|
|
};
|
|
|
|
const priorityColors: Record<string, string> = {
|
|
low: "bg-green-500/20 text-green-400 border-green-500/30",
|
|
medium: "bg-yellow-500/20 text-yellow-400 border-yellow-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 [addDialogOpen, setAddDialogOpen] = useState(false);
|
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
|
const { toast } = useToast();
|
|
|
|
const fetchTasks = async () => {
|
|
try {
|
|
const res = await fetch("/api/tasks");
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
setTasks(data.data || []);
|
|
}
|
|
} catch (error) {
|
|
toast({
|
|
title: "Error",
|
|
description: "Failed to fetch tasks",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchTasks();
|
|
}, []);
|
|
|
|
const handleAddTask = async (task: { title: string; description: string; priority: string }) => {
|
|
try {
|
|
const res = await fetch("/api/tasks", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(task),
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
toast({
|
|
title: "Success",
|
|
description: "Task created successfully",
|
|
});
|
|
fetchTasks();
|
|
setAddDialogOpen(false);
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
toast({
|
|
title: "Error",
|
|
description: "Failed to create task",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDeleteTask = async (id: number, e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
try {
|
|
const res = await fetch(`/api/tasks/${id}`, { method: "DELETE" });
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
toast({
|
|
title: "Success",
|
|
description: "Task deleted successfully",
|
|
});
|
|
fetchTasks();
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
toast({
|
|
title: "Error",
|
|
description: "Failed to delete task",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
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,
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto py-8 px-4">
|
|
<div className="flex justify-between items-center mb-8">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Task Manager</h1>
|
|
<p className="text-muted-foreground mt-1">Manage and track your tasks efficiently</p>
|
|
</div>
|
|
<Button onClick={() => setAddDialogOpen(true)}>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Add Task
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid gap-4 md:grid-cols-4 mb-8">
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardDescription>Total Tasks</CardDescription>
|
|
<CardTitle className="text-3xl">{stats.total}</CardTitle>
|
|
</CardHeader>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardDescription>Pending</CardDescription>
|
|
<CardTitle className="text-3xl text-muted-foreground">{stats.pending}</CardTitle>
|
|
</CardHeader>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardDescription>In Progress</CardDescription>
|
|
<CardTitle className="text-3xl text-blue-400">{stats.inProgress}</CardTitle>
|
|
</CardHeader>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardDescription>Completed</CardDescription>
|
|
<CardTitle className="text-3xl text-green-400">{stats.completed}</CardTitle>
|
|
</CardHeader>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Task List */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>All Tasks</CardTitle>
|
|
<CardDescription>Click on a task to view details</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
<div className="text-center py-8 text-muted-foreground">Loading tasks...</div>
|
|
) : tasks.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
No tasks yet. Create your first task to get started.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{tasks.map((task) => {
|
|
const status = statusConfig[task.status] || statusConfig.pending;
|
|
return (
|
|
<div
|
|
key={task.id}
|
|
onClick={() => setSelectedTask(task)}
|
|
className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/50 cursor-pointer transition-colors"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex flex-col gap-1">
|
|
<span className="font-medium">{task.title}</span>
|
|
<span className="text-sm text-muted-foreground line-clamp-1">
|
|
{task.description || "No description"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Badge className={priorityColors[task.priority] || priorityColors.medium}>
|
|
{task.priority}
|
|
</Badge>
|
|
<Badge variant={status.variant} className="flex items-center gap-1">
|
|
{status.icon}
|
|
{status.label}
|
|
</Badge>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={(e) => handleDeleteTask(task.id, e)}
|
|
className="text-muted-foreground hover:text-destructive"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<AddTaskDialog
|
|
open={addDialogOpen}
|
|
onOpenChange={setAddDialogOpen}
|
|
onSubmit={handleAddTask}
|
|
/>
|
|
|
|
<TaskDetailDialog
|
|
task={selectedTask}
|
|
open={!!selectedTask}
|
|
onOpenChange={(open) => !open && setSelectedTask(null)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|