tahamajs/IE / Projects /front /src /components /project /ProjectHeader.tsx
tahamajs's picture
download
raw
8.22 kB
// src/components/project/ProjectHeader.tsx
import { ArrowLeft, RefreshCw, Archive, Trash2, Edit, MoreVertical, Globe, Lock, Users, Activity, Star, Share2 } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
interface ProjectHeaderProps {
project: {
id: number;
name: string;
description: string;
status: string;
visibility: string;
memberCount?: number;
taskCount?: number;
completedTaskCount?: number;
tags?: string[];
};
isOwner: boolean;
onRefresh: () => void;
onArchive: () => void;
onDelete: () => void;
onEdit?: () => void;
isRefreshing: boolean;
progress?: number;
}
const STATUS_STYLES: Record<string, string> = {
active: "bg-emerald-500/10 text-emerald-600 border-emerald-500/20 dark:bg-emerald-500/20 dark:text-emerald-400",
completed: "bg-blue-500/10 text-blue-600 border-blue-500/20 dark:bg-blue-500/20 dark:text-blue-400",
archived: "bg-gray-500/10 text-gray-600 border-gray-500/20 dark:bg-gray-500/20 dark:text-gray-400",
};
export function ProjectHeader({ project, isOwner, onRefresh, onArchive, onDelete, onEdit, isRefreshing, progress = 0 }: ProjectHeaderProps) {
const [showMenu, setShowMenu] = useState(false);
const navigate = useNavigate();
const visibilityIcon = project.visibility === "public"
? <Globe className="w-3.5 h-3.5" />
: project.visibility === "private"
? <Lock className="w-3.5 h-3.5" />
: <Users className="w-3.5 h-3.5" />;
return (
<div className="bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl border-b border-white/20 dark:border-gray-800/50 sticky top-0 z-20 shadow-sm">
<div className="container mx-auto px-4">
{/* Top Row */}
<div className="flex items-start justify-between gap-4 pt-4 pb-2 flex-wrap">
<div className="flex items-center gap-4">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => navigate("/projects")}
className="p-2 rounded-xl bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all"
>
<ArrowLeft className="w-5 h-5 text-gray-600 dark:text-gray-300" />
</motion.button>
<div>
<div className="flex items-center gap-2.5 flex-wrap">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white tracking-tight">{project.name}</h1>
<span className={`px-2.5 py-0.5 text-xs font-semibold rounded-full border capitalize ${STATUS_STYLES[project.status] ?? STATUS_STYLES.active}`}>
{project.status}
</span>
<span className="flex items-center gap-1 px-2.5 py-0.5 text-xs font-semibold rounded-full border bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-700 capitalize">
{visibilityIcon} {project.visibility}
</span>
</div>
{project.description && (
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5 max-w-2xl line-clamp-1">{project.description}</p>
)}
{project.tags && project.tags.length > 0 && (
<div className="flex gap-1.5 mt-1.5 flex-wrap">
{project.tags.slice(0, 5).map(tag => (
<span key={tag} className="px-2 py-0.5 text-xs bg-primary/10 text-primary rounded-full border border-primary/20">
#{tag}
</span>
))}
</div>
)}
</div>
</div>
<div className="flex items-center gap-2">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={onRefresh}
disabled={isRefreshing}
className="p-2.5 rounded-xl bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all disabled:opacity-50"
title="Refresh"
>
<RefreshCw className={`w-4 h-4 text-gray-600 dark:text-gray-300 ${isRefreshing ? "animate-spin text-primary" : ""}`} />
</motion.button>
{onEdit && (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={onEdit}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-primary text-white text-sm font-medium hover:bg-primary/90 shadow-sm shadow-primary/20 transition-all"
>
<Edit className="w-4 h-4" />
Edit
</motion.button>
)}
{isOwner && (
<div className="relative">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setShowMenu(!showMenu)}
className="p-2.5 rounded-xl bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all"
>
<MoreVertical className="w-4 h-4 text-gray-600 dark:text-gray-300" />
</motion.button>
<AnimatePresence>
{showMenu && (
<>
<div className="fixed inset-0 z-10" onClick={() => setShowMenu(false)} />
<motion.div
initial={{ opacity: 0, scale: 0.95, y: -5 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: -5 }}
className="absolute right-0 mt-1 w-52 bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200/70 dark:border-gray-700/70 z-20 overflow-hidden py-1"
>
<button
onClick={() => { onArchive(); setShowMenu(false); }}
className="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 text-gray-700 dark:text-gray-300 transition"
>
<Archive className="w-4 h-4 text-amber-500" />
Archive Project
</button>
<div className="border-t border-gray-100 dark:border-gray-700 my-1" />
<button
onClick={() => { onDelete(); setShowMenu(false); }}
className="w-full px-4 py-2.5 text-left text-sm hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-3 text-red-600 dark:text-red-400 transition"
>
<Trash2 className="w-4 h-4" />
Delete Forever
</button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
)}
</div>
</div>
{/* Progress Row */}
{project.taskCount && project.taskCount > 0 ? (
<div className="pb-3 mt-2">
<div className="flex justify-between text-xs font-medium text-gray-500 dark:text-gray-400 mb-1.5">
<span className="flex items-center gap-1">
<Activity className="w-3 h-3" />
Progress — {project.completedTaskCount ?? 0}/{project.taskCount} tasks
</span>
<span className="text-primary font-semibold">{Math.round(progress)}%</span>
</div>
<div className="h-1.5 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="h-full bg-gradient-to-r from-primary to-indigo-500 rounded-full"
/>
</div>
</div>
) : <div className="pb-2" />}
</div>
</div>
);
}

Xet Storage Details

Size:
8.22 kB
·
Xet hash:
a6dafa0184403b4a119f5242dd3512c455f7b9017726c31371e8c2c5d246635b

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