tripoli.cloud/app/src/components/layout/TripoliSidebar.tsx
Adam Belkacemi 5dbd3b96f6 Import Tripoli Cloud frontend prototype
React/TypeScript/Vite/Tailwind prototype from the design session (login,
dashboard, photos, backups, settings views with mocked API layer).
Infra work (Rust services, Ansible, OpenTofu) follows in subsequent commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 21:21:43 +02:00

96 lines
3.9 KiB
TypeScript

import { DatabaseBackup, Image, LayoutGrid, LogOut, Settings, X } from 'lucide-react';
import { useNavigation } from '../../contexts/NavigationContext';
import { useSession } from '../../contexts/SessionContext';
import { useIsMobile } from '../../hooks/useIsMobile';
import { useStorageSummary } from '../../hooks/useStorageSummary';
import type { NavKey } from '../../types';
import { TripoliLogo, TripoliWordmark } from './TripoliLogo';
const NAV_ITEMS: Array<{ key: NavKey; label: string; icon: typeof LayoutGrid }> = [
{ key: 'dashboard', label: 'Tableau de bord', icon: LayoutGrid },
{ key: 'photos', label: 'Photos', icon: Image },
{ key: 'backups', label: 'Sauvegardes', icon: DatabaseBackup },
{ key: 'settings', label: 'Réglages', icon: Settings },
];
export function TripoliSidebar() {
const { nav, goTo, drawerOpen, closeDrawer } = useNavigation();
const { user, logout } = useSession();
const isMobile = useIsMobile();
const summary = useStorageSummary();
if (isMobile && !drawerOpen) return null;
return (
<aside
className={`z-40 flex w-64 flex-none flex-col gap-6 border-r border-line bg-panel2 p-[18px] pt-6 backdrop-blur-2xl ${
isMobile ? 'fixed inset-y-0 left-0 min-h-screen' : 'sticky top-0 min-h-screen'
}`}
>
<div className="flex items-center justify-between gap-2.5 px-1.5">
<div className="flex items-center gap-2.5">
<TripoliLogo size={28} />
<TripoliWordmark className="text-ink" />
</div>
{isMobile && (
<button
type="button"
onClick={closeDrawer}
aria-label="Fermer le menu"
className="flex h-[30px] w-[30px] items-center justify-center rounded-[9px] border border-line text-ink"
>
<X size={15} />
</button>
)}
</div>
<nav className="flex flex-col gap-[3px]">
{NAV_ITEMS.map(({ key, label, icon: Icon }) => {
const active = nav === key;
return (
<button
key={key}
type="button"
onClick={() => goTo(key)}
className={`flex items-center gap-2.5 rounded-[11px] px-3 py-2.5 text-left text-sm font-semibold transition-colors ${
active ? 'bg-selected text-ink' : 'bg-transparent text-ink-soft hover:bg-field'
}`}
>
<Icon size={17} strokeWidth={1.8} />
{label}
</button>
);
})}
</nav>
<div className="rounded-2xl border border-line bg-panel p-4">
<div className="flex justify-between gap-2 text-xs font-semibold">
<span className="text-ink">Stockage</span>
<span className="text-ink-soft">{summary?.label ?? '—'}</span>
</div>
<div className="my-2.5 h-[7px] overflow-hidden rounded-full bg-field">
<div className="h-[7px] rounded-full bg-brand-gradient" style={{ width: `${summary?.percent ?? 0}%` }} />
</div>
<p className="m-0 text-[11.5px] leading-relaxed text-ink-soft">{summary?.sealedNote ?? ''}</p>
</div>
<div className="mt-auto flex items-center gap-2.5 rounded-2xl border border-line bg-panel p-2.5">
<span className="flex h-[34px] w-[34px] flex-none items-center justify-center rounded-[11px] bg-brand-gradient text-[12.5px] font-bold text-white">
{user?.initials ?? '??'}
</span>
<div className="min-w-0 flex-1">
<p className="m-0 truncate text-[13px] font-semibold text-ink">{user?.name ?? 'Invité'}</p>
<p className="m-0 text-[11.5px] text-ink-soft">{user?.role ?? ''}</p>
</div>
<button
type="button"
onClick={logout}
aria-label="Se déconnecter"
className="flex h-7 w-7 flex-none items-center justify-center rounded-lg text-ink-soft"
>
<LogOut size={16} strokeWidth={1.8} />
</button>
</div>
</aside>
);
}