JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { motion } from "framer-motion";4import {5 Upload, Download, Trash2, Pencil, FolderInput, Share2, LogIn, LogOut, FolderPlus, FolderMinus, Link2Off,6 Star, Tag, Edit3, Users, RotateCcw, Eye, Activity as ActivityIcon, type LucideIcon,7} from "lucide-react";8import { EmptyState } from "@/components/ui/PageHeader";910export interface ActivityItem {11 id: string;12 action: string;13 details: string | null;14 createdAt: string;15 file: { name: string } | null;16}1718interface Meta { label: string; icon: LucideIcon; tone: string }1920/** Actions réelles observées en base : UPLOAD, LOGIN, file_renamed, DOWNLOAD, LOGOUT, DELETE, CREATE_FOLDER, DELETE_FOLDER, shared_space_renamed, SHARE, MOVE, FAVORITE, EDIT, RENAME, TAG_ADD, TAG_REMOVE… */21const META: Record<string, Meta> = {22 UPLOAD: { label: "a téléversé", icon: Upload, tone: "text-emerald-600 bg-emerald-500/10 dark:text-emerald-300" },23 DOWNLOAD: { label: "a téléchargé", icon: Download, tone: "text-blue-600 bg-blue-500/10 dark:text-blue-300" },24 DELETE: { label: "a supprimé", icon: Trash2, tone: "text-rose-600 bg-rose-500/10 dark:text-rose-300" },25 RESTORE: { label: "a restauré", icon: RotateCcw, tone: "text-emerald-600 bg-emerald-500/10 dark:text-emerald-300" },26 RENAME: { label: "a renommé", icon: Pencil, tone: "text-amber-600 bg-amber-500/10 dark:text-amber-300" },27 FILE_RENAMED: { label: "a renommé", icon: Pencil, tone: "text-amber-600 bg-amber-500/10 dark:text-amber-300" },28 MOVE: { label: "a déplacé", icon: FolderInput, tone: "text-blue-600 bg-blue-500/10 dark:text-blue-300" },29 SHARE: { label: "a partagé", icon: Share2, tone: "text-purple-600 bg-purple-500/10 dark:text-purple-300" },30 UNSHARE: { label: "a retiré le partage de", icon: Link2Off, tone: "text-gray-600 bg-gray-500/10 dark:text-gray-300" },31 LOGIN: { label: "connexion", icon: LogIn, tone: "text-gray-600 bg-gray-500/10 dark:text-gray-300" },32 LOGOUT: { label: "déconnexion", icon: LogOut, tone: "text-gray-600 bg-gray-500/10 dark:text-gray-300" },33 CREATE_FOLDER: { label: "a créé un dossier", icon: FolderPlus, tone: "text-blue-600 bg-blue-500/10 dark:text-blue-300" },34 DELETE_FOLDER: { label: "a supprimé un dossier", icon: FolderMinus, tone: "text-rose-600 bg-rose-500/10 dark:text-rose-300" },35 SHARED_SPACE_RENAMED: { label: "a renommé un espace partagé", icon: Users, tone: "text-purple-600 bg-purple-500/10 dark:text-purple-300" },36 FAVORITE: { label: "a mis en favori", icon: Star, tone: "text-amber-600 bg-amber-500/10 dark:text-amber-300" },37 UNFAVORITE: { label: "a retiré des favoris", icon: Star, tone: "text-gray-600 bg-gray-500/10 dark:text-gray-300" },38 EDIT: { label: "a modifié", icon: Edit3, tone: "text-blue-600 bg-blue-500/10 dark:text-blue-300" },39 PREVIEW: { label: "a consulté", icon: Eye, tone: "text-gray-600 bg-gray-500/10 dark:text-gray-300" },40 TAG_ADD: { label: "a étiqueté", icon: Tag, tone: "text-purple-600 bg-purple-500/10 dark:text-purple-300" },41 TAG_REMOVE: { label: "a retiré une étiquette de", icon: Tag, tone: "text-gray-600 bg-gray-500/10 dark:text-gray-300" },42};4344export function activityMeta(action: string): Meta {45 const key = (action || "").toUpperCase();46 return META[key] || { label: action.toLowerCase().replace(/_/g, " "), icon: ActivityIcon, tone: "text-gray-600 bg-gray-500/10 dark:text-gray-300" };47}4849export function timeAgo(dateStr: string): string {50 const diff = Date.now() - new Date(dateStr).getTime();51 const m = Math.floor(diff / 60000);52 if (m < 1) return "à l'instant";53 if (m < 60) return `il y a ${m} min`;54 const h = Math.floor(m / 60);55 if (h < 24) return `il y a ${h} h`;56 const d = Math.floor(h / 24);57 if (d < 30) return `il y a ${d} j`;58 return new Intl.DateTimeFormat("fr-CA", { day: "numeric", month: "short" }).format(new Date(dateStr));59}6061/** Nettoie les détails techniques (« Uploaded X », « Login from ... ») pour un libellé lisible. */62function prettyDetails(a: ActivityItem): string | null {63 if (!a.details) return null;64 let d = a.details;65 if (a.file?.name && d.includes(a.file.name)) return null; // redondant avec le nom66 d = d.replace(/^Uploaded\s+/i, "").replace(/^Created folder\s+/i, "").replace(/^Deleted\s+/i, "").replace(/^Login from\s+/i, "depuis ");67 return d;68}6970interface ActivityTimelineProps {71 activities: ActivityItem[];72 /** Compact : sans ligne verticale, padding réduit */73 compact?: boolean;74 limit?: number;75 emptyTitle?: string;76}7778export default function ActivityTimeline({ activities, compact, limit, emptyTitle = "Aucune activité récente" }: ActivityTimelineProps) {79 const list = limit ? activities.slice(0, limit) : activities;80 if (list.length === 0) {81 return <EmptyState icon={ActivityIcon} title={emptyTitle} description="Vos actions (téléversements, partages, modifications) apparaîtront ici." />;82 }83 return (84 <ol className="relative" role="list">85 {!compact && <span className="absolute left-[19px] top-4 bottom-4 w-px bg-line dark:bg-white/[0.08]" aria-hidden />}86 {list.map((a, i) => {87 const meta = activityMeta(a.action);88 const Icon = meta.icon;89 const details = prettyDetails(a);90 return (91 <motion.li92 key={a.id}93 initial={{ opacity: 0, x: -8 }}94 animate={{ opacity: 1, x: 0 }}95 transition={{ delay: Math.min(i, 12) * 0.03, duration: 0.25 }}96 className={`relative flex items-start gap-3 ${compact ? "py-2" : "py-2.5"} px-1 rounded-xl hover:bg-gray-50 dark:hover:bg-white/[0.03] transition-colors`}97 >98 <span className={`relative z-10 w-9 h-9 rounded-xl flex items-center justify-center shrink-0 ring-4 ring-surface dark:ring-[#12131a] ${meta.tone}`}>99 <Icon className="w-4 h-4" />100 </span>101 <div className="flex-1 min-w-0 pt-0.5">102 <p className="text-[13.5px] text-gray-700 dark:text-gray-200 leading-snug">103 <span className="text-gray-500 dark:text-gray-400">{meta.label}</span>104 {a.file?.name && <> <span className="font-medium text-gray-900 dark:text-white break-all">{a.file.name}</span></>}105 </p>106 {details && <p className="text-[12px] text-gray-400 dark:text-gray-500 truncate mt-0.5">{details}</p>}107 </div>108 <time dateTime={a.createdAt} className="text-[11.5px] text-gray-400 dark:text-gray-500 shrink-0 whitespace-nowrap pt-1 tabular-nums" title={new Date(a.createdAt).toLocaleString("fr-CA")}>109 {timeAgo(a.createdAt)}110 </time>111 </motion.li>112 );113 })}114 </ol>115 );116}117