tahamajs/IE / Projects /front /src /components /project /ProjectOverview.tsx
tahamajs's picture
download
raw
9.6 kB
// src/components/project/ProjectOverview.tsx
import { useState } from "react";
import { Edit2, Check, X, Calendar, Users, CheckSquare, FileText, TrendingUp, Award, Clock, Target } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { formatDistanceToNow } from "@/lib/date";
import api from "@/lib/api";
import { toast } from "react-toastify";
interface Project {
id: number;
name: string;
description: string;
status: string;
visibility: string;
memberCount: number;
taskCount: number;
completedTaskCount: number;
fileCount: number;
createdAt: string;
updatedAt: string;
tags?: string[];
ownerUsername?: string;
}
interface ProjectOverviewProps {
project: Project;
isOwner: boolean;
onUpdate: (updates: any) => void;
}
export function ProjectOverview({ project, isOwner, onUpdate }: ProjectOverviewProps) {
const [editing, setEditing] = useState(false);
const [name, setName] = useState(project.name);
const [description, setDescription] = useState(project.description || "");
const [saving, setSaving] = useState(false);
const progress = project.taskCount > 0 ? Math.round((project.completedTaskCount / project.taskCount) * 100) : 0;
const handleSave = async () => {
if (!name.trim()) return;
setSaving(true);
try {
await onUpdate({ name: name.trim(), description: description.trim() });
setEditing(false);
} catch {
toast.error("Failed to save");
} finally {
setSaving(false);
}
};
const stats = [
{ label: "Members", value: project.memberCount ?? 0, icon: Users, color: "text-blue-500 bg-blue-500/10" },
{ label: "Tasks", value: project.taskCount ?? 0, icon: CheckSquare, color: "text-emerald-500 bg-emerald-500/10" },
{ label: "Completed", value: project.completedTaskCount ?? 0, icon: Award, color: "text-purple-500 bg-purple-500/10" },
{ label: "Files", value: project.fileCount ?? 0, icon: FileText, color: "text-orange-500 bg-orange-500/10" },
];
return (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, i) => (
<motion.div
key={stat.label}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.07 }}
className="bg-white/60 dark:bg-gray-900/60 backdrop-blur-md rounded-2xl border border-white/30 dark:border-gray-700/50 p-4 shadow-sm"
>
<div className={`w-10 h-10 rounded-xl ${stat.color} flex items-center justify-center mb-3`}>
<stat.icon className="w-5 h-5" />
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">{stat.value}</div>
<div className="text-xs text-gray-500 dark:text-gray-400 font-medium mt-0.5">{stat.label}</div>
</motion.div>
))}
</div>
{/* Progress Card */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-gradient-to-br from-primary/5 via-transparent to-indigo-500/5 rounded-2xl border border-primary/10 dark:border-primary/20 p-6"
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-primary" />
<h3 className="font-semibold text-gray-900 dark:text-white">Overall Progress</h3>
</div>
<span className="text-2xl font-black text-primary">{progress}%</span>
</div>
<div className="h-3 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 1, ease: "easeOut" }}
className="h-full bg-gradient-to-r from-primary to-indigo-500 rounded-full"
/>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
{project.completedTaskCount} of {project.taskCount} tasks completed
</p>
</motion.div>
{/* Description Card */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-white/60 dark:bg-gray-900/60 backdrop-blur-md rounded-2xl border border-white/30 dark:border-gray-700/50 p-6 shadow-sm"
>
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Target className="w-4 h-4 text-primary" />
Project Description
</h3>
{isOwner && !editing && (
<button
onClick={() => setEditing(true)}
className="flex items-center gap-1 text-xs text-primary hover:underline transition"
>
<Edit2 className="w-3 h-3" /> Edit
</button>
)}
</div>
<AnimatePresence mode="wait">
{editing ? (
<motion.div key="edit" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="space-y-3">
<input
value={name}
onChange={e => setName(e.target.value)}
className="w-full px-3 py-2 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="Project name"
/>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
rows={4}
className="w-full px-3 py-2 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
placeholder="Describe your project..."
/>
<div className="flex gap-2">
<button
onClick={handleSave}
disabled={saving || !name.trim()}
className="flex items-center gap-1.5 px-4 py-2 bg-primary text-white rounded-xl text-sm font-medium hover:bg-primary/90 disabled:opacity-50 transition"
>
<Check className="w-4 h-4" /> {saving ? "Saving..." : "Save"}
</button>
<button
onClick={() => { setEditing(false); setName(project.name); setDescription(project.description || ""); }}
className="flex items-center gap-1.5 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl text-sm font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition"
>
<X className="w-4 h-4" /> Cancel
</button>
</div>
</motion.div>
) : (
<motion.p key="view" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
className="text-gray-600 dark:text-gray-400 leading-relaxed"
>
{project.description || <span className="text-gray-400 italic">No description provided. {isOwner ? "Click Edit to add one." : ""}</span>}
</motion.p>
)}
</AnimatePresence>
</motion.div>
{/* Meta Info */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="grid grid-cols-1 sm:grid-cols-2 gap-4"
>
<div className="bg-white/60 dark:bg-gray-900/60 backdrop-blur-md rounded-2xl border border-white/30 dark:border-gray-700/50 p-4 shadow-sm">
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-1">
<Clock className="w-4 h-4" />
Created
</div>
<p className="font-semibold text-gray-900 dark:text-white">
{formatDistanceToNow(new Date(project.createdAt), { addSuffix: true })}
</p>
</div>
<div className="bg-white/60 dark:bg-gray-900/60 backdrop-blur-md rounded-2xl border border-white/30 dark:border-gray-700/50 p-4 shadow-sm">
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-1">
<Calendar className="w-4 h-4" />
Last Updated
</div>
<p className="font-semibold text-gray-900 dark:text-white">
{formatDistanceToNow(new Date(project.updatedAt), { addSuffix: true })}
</p>
</div>
</motion.div>
{/* Tags */}
{project.tags && project.tags.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="bg-white/60 dark:bg-gray-900/60 backdrop-blur-md rounded-2xl border border-white/30 dark:border-gray-700/50 p-4 shadow-sm"
>
<h3 className="text-sm font-semibold text-gray-500 dark:text-gray-400 mb-3 uppercase tracking-wider">Tags</h3>
<div className="flex flex-wrap gap-2">
{project.tags.map(tag => (
<span key={tag} className="px-3 py-1 text-sm bg-primary/10 text-primary border border-primary/20 rounded-full">
#{tag}
</span>
))}
</div>
</motion.div>
)}
</div>
);
}

Xet Storage Details

Size:
9.6 kB
·
Xet hash:
7e8e34c8c4b359a5ab38f91db3065f6246fb2518a32a3cd58a088b3031065d8b

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