| // 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.