SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
2 h agolast push
JavaScript 56.7% TypeScript 42.6%
6.5 KB · 117 lines tsx
Raw Blame History
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