tahamajs/IE / Projects /front /src /components /project /ProjectTimeline.tsx
tahamajs's picture
download
raw
5.51 kB
// src/components/project/ProjectTimeline.tsx
import { useState, useEffect, useCallback } from "react";
import { CheckCircle, Clock, Circle, Calendar, Loader2, Activity } from "lucide-react";
import { motion } from "framer-motion";
import { formatDistanceToNow } from "@/lib/date";
import api from "@/lib/api";
import { toast } from "react-toastify";
interface Task {
id: number;
title: string;
status: string;
priority: string;
dueDate?: string;
createdAt: string;
assigneeUsername?: string;
}
interface ProjectTimelineProps {
projectId: number;
}
export function ProjectTimeline({ projectId }: ProjectTimelineProps) {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const fetchTasks = useCallback(async () => {
setLoading(true);
try {
const res = await api.get(`/projects/${projectId}/tasks`);
const all = res.data?.tasks ?? res.data?.items ?? res.data ?? [];
// Sort by createdAt descending
setTasks([...all].sort((a: Task, b: Task) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()));
} catch {
toast.error("Failed to load timeline");
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => { fetchTasks(); }, [fetchTasks]);
const getStatusIcon = (status: string) => {
if (status.includes("complet")) return <CheckCircle className="w-4 h-4 text-emerald-500" />;
if (status.includes("progress")) return <Clock className="w-4 h-4 text-amber-500" />;
return <Circle className="w-4 h-4 text-gray-400" />;
};
const getStatusColor = (status: string) => {
if (status.includes("complet")) return "border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20";
if (status.includes("progress")) return "border-amber-500 bg-amber-50 dark:bg-amber-900/20";
return "border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900";
};
const getPriorityDot = (priority: string) => {
if (priority === "high") return "bg-red-400";
if (priority === "medium") return "bg-amber-400";
return "bg-emerald-400";
};
if (loading) {
return (
<div className="flex items-center justify-center py-16">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
);
}
if (tasks.length === 0) {
return (
<div className="text-center py-16 text-gray-500">
<Activity className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p>No timeline events yet</p>
<p className="text-sm mt-1">Create tasks to see them in the timeline</p>
</div>
);
}
return (
<div className="space-y-4">
<h3 className="text-base font-bold text-gray-900 dark:text-white">Project Timeline</h3>
<p className="text-sm text-gray-500">{tasks.length} events</p>
<div className="relative">
{/* Vertical line */}
<div className="absolute left-5 top-0 bottom-0 w-0.5 bg-gradient-to-b from-primary/30 via-gray-200 to-transparent dark:via-gray-700" />
<div className="space-y-4">
{tasks.map((task, i) => (
<motion.div
key={task.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.06 }}
className="relative flex gap-5"
>
{/* Icon */}
<div className={`relative z-10 w-10 h-10 rounded-full border-2 flex items-center justify-center flex-shrink-0 shadow-sm ${getStatusColor(task.status)}`}>
{getStatusIcon(task.status)}
</div>
{/* Content */}
<div className="flex-1 bg-white/70 dark:bg-gray-900/70 backdrop-blur-md rounded-2xl border border-white/50 dark:border-gray-700/50 p-4 hover:shadow-sm transition-all">
<div className="flex items-start justify-between gap-2 flex-wrap">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${getPriorityDot(task.priority)}`} />
<p className={`font-semibold text-sm ${task.status.includes("complet") ? "line-through text-gray-400" : "text-gray-900 dark:text-white"}`}>
{task.title}
</p>
</div>
<span className="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">
{formatDistanceToNow(new Date(task.createdAt), { addSuffix: true })}
</span>
</div>
<div className="flex items-center gap-4 mt-2 text-xs text-gray-400 flex-wrap">
{task.dueDate && (
<span className={`flex items-center gap-1 ${new Date(task.dueDate) < new Date() && !task.status.includes("complet") ? "text-red-500" : ""}`}>
<Calendar className="w-3 h-3" />
Due {formatDistanceToNow(new Date(task.dueDate), { addSuffix: true })}
</span>
)}
{task.assigneeUsername && (
<span>@{task.assigneeUsername}</span>
)}
<span className="capitalize px-2 py-0.5 rounded-full text-xs bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400">
{task.status.replace("_", " ")}
</span>
</div>
</div>
</motion.div>
))}
</div>
</div>
</div>
);
}

Xet Storage Details

Size:
5.51 kB
·
Xet hash:
03c7cfb4bccf701853521f477b6065c12db3af4a50aef033067e44ab5229b618

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.