fs3/frontend/src/app/page.tsx
rdev-worker 296401abbc
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
build: Build a full-stack task management application with the following str...
2026-01-31 07:44:20 +00:00

336 lines
12 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { Plus, Trash2, Eye, CheckCircle2, Circle } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { getTasks, createTask, deleteTask, getTask, Task } from "@/lib/api";
export default function Home() {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Add task form state
const [newTitle, setNewTitle] = useState("");
const [newDescription, setNewDescription] = useState("");
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [adding, setAdding] = useState(false);
// View task state
const [viewTask, setViewTask] = useState<Task | null>(null);
const [viewDialogOpen, setViewDialogOpen] = useState(false);
const [viewLoading, setViewLoading] = useState(false);
// Delete state
const [deletingId, setDeletingId] = useState<number | null>(null);
useEffect(() => {
loadTasks();
}, []);
async function loadTasks() {
try {
setLoading(true);
const data = await getTasks();
setTasks(data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load tasks");
} finally {
setLoading(false);
}
}
async function handleAddTask(e: React.FormEvent) {
e.preventDefault();
if (!newTitle.trim()) return;
try {
setAdding(true);
const task = await createTask(newTitle.trim(), newDescription.trim());
setTasks((prev) => [...prev, task]);
setNewTitle("");
setNewDescription("");
setAddDialogOpen(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to add task");
} finally {
setAdding(false);
}
}
async function handleDeleteTask(id: number) {
try {
setDeletingId(id);
await deleteTask(id);
setTasks((prev) => prev.filter((t) => t.id !== id));
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete task");
} finally {
setDeletingId(null);
}
}
async function handleViewTask(id: number) {
try {
setViewLoading(true);
setViewDialogOpen(true);
const task = await getTask(id);
setViewTask(task);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load task");
setViewDialogOpen(false);
} finally {
setViewLoading(false);
}
}
return (
<main className="min-h-screen bg-gradient-to-b from-background to-background/95">
<div className="container mx-auto py-10 px-4">
{/* Header */}
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-4xl font-bold tracking-tight">Task Manager</h1>
<p className="text-muted-foreground mt-1">
Organize and track your tasks efficiently
</p>
</div>
{/* Add Task Dialog */}
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}>
<DialogTrigger asChild>
<Button size="lg" className="gap-2">
<Plus className="h-5 w-5" />
Add Task
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<form onSubmit={handleAddTask}>
<DialogHeader>
<DialogTitle>Add New Task</DialogTitle>
<DialogDescription>
Create a new task to track. Fill in the details below.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<label htmlFor="title" className="text-sm font-medium">
Title
</label>
<Input
id="title"
placeholder="Enter task title..."
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
required
/>
</div>
<div className="grid gap-2">
<label
htmlFor="description"
className="text-sm font-medium"
>
Description
</label>
<Textarea
id="description"
placeholder="Enter task description..."
value={newDescription}
onChange={(e) => setNewDescription(e.target.value)}
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={adding || !newTitle.trim()}>
{adding ? "Adding..." : "Add Task"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
{/* Error Alert */}
{error && (
<div className="bg-destructive/15 border border-destructive text-destructive px-4 py-3 rounded-lg mb-6">
{error}
<button
className="float-right font-bold"
onClick={() => setError(null)}
>
&times;
</button>
</div>
)}
{/* Loading State */}
{loading && (
<div className="flex justify-center py-20">
<div className="animate-pulse text-muted-foreground">
Loading tasks...
</div>
</div>
)}
{/* Empty State */}
{!loading && tasks.length === 0 && (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16">
<Circle className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium">No tasks yet</h3>
<p className="text-muted-foreground text-center mt-1">
Get started by adding your first task
</p>
</CardContent>
</Card>
)}
{/* Task Grid */}
{!loading && tasks.length > 0 && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{tasks.map((task) => (
<Card
key={task.id}
className="group hover:shadow-lg transition-all duration-200 hover:border-primary/50"
>
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3 flex-1">
{task.completed ? (
<CheckCircle2 className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" />
) : (
<Circle className="h-5 w-5 text-muted-foreground mt-0.5 flex-shrink-0" />
)}
<CardTitle className="text-lg leading-tight">
{task.title}
</CardTitle>
</div>
</div>
<CardDescription className="pl-8 line-clamp-2">
{task.description || "No description"}
</CardDescription>
</CardHeader>
<CardContent className="pt-2">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">
{new Date(task.createdAt).toLocaleDateString()}
</span>
<div className="flex gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => handleViewTask(task.id)}
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => handleDeleteTask(task.id)}
disabled={deletingId === task.id}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* View Task Dialog */}
<Dialog open={viewDialogOpen} onOpenChange={setViewDialogOpen}>
<DialogContent className="sm:max-w-[525px]">
{viewLoading ? (
<div className="py-8 text-center">
<div className="animate-pulse text-muted-foreground">
Loading task...
</div>
</div>
) : viewTask ? (
<>
<DialogHeader>
<div className="flex items-center gap-3">
{viewTask.completed ? (
<CheckCircle2 className="h-6 w-6 text-green-500 flex-shrink-0" />
) : (
<Circle className="h-6 w-6 text-muted-foreground flex-shrink-0" />
)}
<DialogTitle className="text-xl">
{viewTask.title}
</DialogTitle>
</div>
</DialogHeader>
<div className="mt-4 space-y-4">
<div>
<h4 className="text-sm font-medium text-muted-foreground mb-1">
Description
</h4>
<p className="text-sm">
{viewTask.description || "No description provided"}
</p>
</div>
<div className="flex gap-6 text-sm">
<div>
<span className="text-muted-foreground">Status: </span>
<span
className={
viewTask.completed
? "text-green-500"
: "text-yellow-500"
}
>
{viewTask.completed ? "Completed" : "Pending"}
</span>
</div>
<div>
<span className="text-muted-foreground">Created: </span>
<span>
{new Date(viewTask.createdAt).toLocaleString()}
</span>
</div>
</div>
</div>
<DialogFooter className="mt-6">
<Button
variant="destructive"
onClick={() => {
handleDeleteTask(viewTask.id);
setViewDialogOpen(false);
}}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Task
</Button>
</DialogFooter>
</>
) : null}
</DialogContent>
</Dialog>
</div>
</main>
);
}