aethex-forge/client/pages/ProjectBoard.tsx
2025-10-18 03:49:38 +00:00

207 lines
6.8 KiB
TypeScript

import Layout from "@/components/Layout";
import { useAuth } from "@/contexts/AuthContext";
import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { aethexCollabService } from "@/lib/aethex-collab-service";
import LoadingScreen from "@/components/LoadingScreen";
const columns: {
key: "todo" | "doing" | "done" | "blocked";
title: string;
hint: string;
}[] = [
{ key: "todo", title: "To do", hint: "Planned" },
{ key: "doing", title: "In progress", hint: "Active" },
{ key: "done", title: "Done", hint: "Completed" },
{ key: "blocked", title: "Blocked", hint: "Needs attention" },
];
export default function ProjectBoard() {
const { user, loading } = useAuth();
const navigate = useNavigate();
const { projectId } = useParams<{ projectId: string }>();
const [isLoading, setIsLoading] = useState(true);
const [tasks, setTasks] = useState<any[]>([]);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [creating, setCreating] = useState(false);
useEffect(() => {
if (!loading && !user) navigate("/login", { replace: true });
}, [loading, user, navigate]);
const load = async () => {
if (!projectId) return;
setIsLoading(true);
try {
const rows = await aethexCollabService.listProjectTasks(projectId);
setTasks(rows);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
load();
}, [projectId]);
const grouped = useMemo(() => {
const map: Record<string, any[]> = {
todo: [],
doing: [],
done: [],
blocked: [],
};
for (const t of tasks) {
map[t.status || "todo"].push(t);
}
return map;
}, [tasks]);
const handleCreate = async () => {
if (!user?.id || !projectId) return;
if (!title.trim()) return;
setCreating(true);
try {
await aethexCollabService.createTask(
projectId,
title.trim(),
description.trim() || null,
null,
null,
);
setTitle("");
setDescription("");
await load();
} finally {
setCreating(false);
}
};
const move = async (
taskId: string,
status: "todo" | "doing" | "done" | "blocked",
) => {
await aethexCollabService.updateTaskStatus(taskId, status);
await load();
};
if (loading || isLoading)
return (
<LoadingScreen message="Loading project..." showProgress duration={800} />
);
if (!user) return null;
return (
<Layout>
<div className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(110,141,255,0.12),transparent_60%)] py-10">
<div className="mx-auto w-full max-w-6xl px-4 lg:px-6 space-y-6">
<section className="rounded-3xl border border-border/40 bg-background/80 p-6 shadow-2xl backdrop-blur">
<h1 className="text-3xl font-semibold text-foreground">
Project Board
</h1>
<p className="mt-1 text-sm text-muted-foreground">
Track tasks by status. Drag-and-drop coming next.
</p>
</section>
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)]">
{columns.map((col) => (
<Card
key={col.key}
className="rounded-3xl border-border/40 bg-background/70 shadow-xl backdrop-blur-lg"
>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
{col.title}
<Badge
variant="outline"
className="border-border/50 text-xs"
>
{grouped[col.key].length}
</Badge>
</CardTitle>
<CardDescription>{col.hint}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{grouped[col.key].length === 0 ? (
<p className="text-sm text-muted-foreground">No tasks.</p>
) : (
grouped[col.key].map((t) => (
<div
key={t.id}
className="rounded-2xl border border-border/30 bg-background/60 p-3"
>
<div className="font-medium text-foreground">
{t.title}
</div>
{t.description ? (
<p className="text-xs text-muted-foreground mt-1">
{t.description}
</p>
) : null}
<div className="mt-2 flex flex-wrap gap-2">
{columns.map((k) => (
<Button
key={`${t.id}-${k.key}`}
size="xs"
variant="outline"
onClick={() => move(t.id, k.key)}
>
{k.title}
</Button>
))}
</div>
</div>
))
)}
</CardContent>
</Card>
))}
</div>
<Card className="rounded-3xl border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
<CardHeader>
<CardTitle className="text-lg">Add task</CardTitle>
<CardDescription>
Keep titles concise; details optional.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Input
placeholder="Task title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Textarea
placeholder="Description (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<div className="flex justify-end">
<Button
onClick={handleCreate}
disabled={creating || !title.trim()}
className="rounded-full bg-gradient-to-r from-aethex-500 to-neon-blue text-white"
>
{creating ? "Creating..." : "Create task"}
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</Layout>
);
}