1import { useEffect, useState } from 'react';2import { Link, useNavigate, useParams } from 'react-router-dom';3import { Archive, BarChart3, LogOut, MoreHorizontal, Pencil, Pin, PinOff, Plus, Search, Settings, ShieldCheck, Trash2, X } from 'lucide-react';4import * as DM from '@radix-ui/react-dropdown-menu';5import { useChat } from '@/stores/chat';6import { useAuth } from '@/stores/auth';7import { useUI } from '@/stores/ui';8import { Logo } from '@/components/ui/logo';9import { cn } from '@/lib/cn';10import { fmtDate } from '@/lib/format';1112export function Sidebar({ onClose }: { onClose?: () => void }) {13 const { id } = useParams();14 const nav = useNavigate();15 const { conversations, loadConversations, updateConversation, deleteConversation } = useChat();16 const user = useAuth((s) => s.user);17 const logout = useAuth((s) => s.logout);18 const { course, setSettings } = useUI();19 const [q, setQ] = useState('');20 const [renaming, setRenaming] = useState<string | null>(null);21 const [draft, setDraft] = useState('');2223 useEffect(() => {24 const t = setTimeout(() => loadConversations(q || undefined).catch(() => undefined), q ? 250 : 0);25 return () => clearTimeout(t);26 }, [q, loadConversations]);2728 const go = (path: string) => {29 nav(path);30 onClose?.();31 };3233 const rename = async (cid: string) => {34 const title = draft.trim();35 setRenaming(null);36 if (title) await updateConversation(cid, { title });37 };3839 const remove = async (cid: string) => {40 if (!confirm('Supprimer définitivement cette conversation ?')) return;41 await deleteConversation(cid);42 if (id === cid) go('/');43 };4445 return (46 <aside className="flex h-full w-[300px] max-w-[85vw] flex-col bg-neutral-surface border-r border-neutral-line">47 <div className="flex items-center justify-between px-4 pt-[calc(12px+var(--safe-top))] pb-3">48 <Link to="/" onClick={onClose}><Logo /></Link>49 {onClose && <button onClick={onClose} className="h-11 w-11 inline-flex items-center justify-center rounded-xl hover:bg-white" aria-label="Fermer le menu"><X size={20} /></button>}50 </div>51 <div className="px-3 space-y-2">52 <button onClick={() => go('/')} className="w-full h-11 rounded-xl bg-uqo-blue text-white font-medium inline-flex items-center justify-center gap-2 hover:bg-uqo-blue-dark">53 <Plus size={18} /> Nouvelle conversation <span className="text-white/70 text-xs">· {course}</span>54 </button>55 <label className="relative block">56 <Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-muted" />57 <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Rechercher…" className="w-full h-11 rounded-xl border border-neutral-line bg-white pl-9 pr-3 text-sm" aria-label="Rechercher dans l'historique" />58 </label>59 </div>60 <nav className="mt-3 flex-1 overflow-y-auto scroll-thin px-2" aria-label="Conversations">61 {conversations.length === 0 && <p className="px-3 py-6 text-center text-sm text-neutral-muted">Aucune conversation pour l'instant.</p>}62 <ul className="space-y-0.5">63 {conversations.map((c) => (64 <li key={c.id} className={cn('group flex items-center rounded-xl', id === c.id ? 'bg-white shadow-card' : 'hover:bg-white/70')}>65 {renaming === c.id ? (66 <input autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onBlur={() => rename(c.id)} onKeyDown={(e) => e.key === 'Enter' && rename(c.id)} className="flex-1 h-11 bg-transparent px-3 text-sm outline-none" />67 ) : (68 <button onClick={() => go(`/c/${c.id}`)} className="flex-1 min-w-0 text-left px-3 py-2.5">69 <span className="flex items-center gap-1.5 text-sm font-medium truncate">{c.pinned && <Pin size={12} className="text-uqo-blue shrink-0" />}<span className="truncate">{c.title}</span></span>70 <span className="block text-[11px] text-neutral-muted">{c.course} · {fmtDate(c.updated_at)}</span>71 </button>72 )}73 <DM.Root>74 <DM.Trigger asChild>75 <button className="h-11 w-10 inline-flex items-center justify-center rounded-lg text-neutral-muted opacity-70 sm:opacity-0 group-hover:opacity-100 focus:opacity-100 data-[state=open]:opacity-100" aria-label="Options"><MoreHorizontal size={16} /></button>76 </DM.Trigger>77 <DM.Portal>78 <DM.Content align="end" className="z-50 min-w-[190px] rounded-xl border border-neutral-line bg-white p-1 shadow-card text-sm">79 <DM.Item onSelect={() => { setDraft(c.title); setRenaming(c.id); }} className="flex items-center gap-2 rounded-lg px-3 py-2.5 outline-none hover:bg-neutral-surface cursor-pointer"><Pencil size={14} /> Renommer</DM.Item>80 <DM.Item onSelect={() => updateConversation(c.id, { pinned: !c.pinned })} className="flex items-center gap-2 rounded-lg px-3 py-2.5 outline-none hover:bg-neutral-surface cursor-pointer">{c.pinned ? <PinOff size={14} /> : <Pin size={14} />} {c.pinned ? 'Désépingler' : 'Épingler'}</DM.Item>81 <DM.Item onSelect={() => updateConversation(c.id, { archived: true })} className="flex items-center gap-2 rounded-lg px-3 py-2.5 outline-none hover:bg-neutral-surface cursor-pointer"><Archive size={14} /> Archiver</DM.Item>82 <DM.Separator className="my-1 h-px bg-neutral-line" />83 <DM.Item onSelect={() => remove(c.id)} className="flex items-center gap-2 rounded-lg px-3 py-2.5 outline-none text-semantic-error hover:bg-red-50 cursor-pointer"><Trash2 size={14} /> Supprimer</DM.Item>84 </DM.Content>85 </DM.Portal>86 </DM.Root>87 </li>88 ))}89 </ul>90 </nav>91 <div className="border-t border-neutral-line p-3 pb-[calc(12px+var(--safe-bottom))] space-y-1">92 {user && (user.role === 'professor' || user.role === 'admin') && (93 <button onClick={() => go('/professeur')} className="w-full h-11 rounded-xl px-3 inline-flex items-center gap-2 text-sm hover:bg-white"><BarChart3 size={16} className="text-uqo-blue" /> Tableau de bord professeur</button>94 )}95 {user?.role === 'admin' && (96 <button onClick={() => go('/admin')} className="w-full h-11 rounded-xl px-3 inline-flex items-center gap-2 text-sm hover:bg-white"><ShieldCheck size={16} className="text-uqo-blue" /> Coûts et système</button>97 )}98 <div className="flex items-center gap-2 pt-1">99 <span className="h-9 w-9 rounded-full bg-uqo-blue-light text-uqo-blue-dark inline-flex items-center justify-center text-xs font-semibold">{(user?.display_name || user?.email || '?').slice(0, 2).toUpperCase()}</span>100 <span className="min-w-0 flex-1"><span className="block text-sm font-medium truncate">{user?.display_name || user?.email}</span><span className="block text-[11px] text-neutral-muted capitalize">{user?.role === 'professor' ? 'professeur' : user?.role === 'admin' ? 'administrateur' : 'étudiant·e'}</span></span>101 <button onClick={() => setSettings(true)} className="h-11 w-11 inline-flex items-center justify-center rounded-xl hover:bg-white" aria-label="Préférences"><Settings size={18} /></button>102 <button onClick={() => logout().then(() => go('/connexion'))} className="h-11 w-11 inline-flex items-center justify-center rounded-xl hover:bg-white" aria-label="Se déconnecter"><LogOut size={18} /></button>103 </div>104 </div>105 </aside>106 );107}108