Construire une plateforme de gestion de fichiers complète — upload, authentification, analytics, sécurité fail2ban et déploiement Docker.
Tout le dashboard d'OpenFiler est un seul Client Component — app/dashboard.tsx. C'est un choix délibéré : l'état de sélection, les filtres, les modals et les toasts sont tous locaux. Pas besoin de server components ici.
'use client'
import { useState, useEffect, useCallback } from 'react'
import { Sidebar } from '@/components/Sidebar'
import { Toolbar } from '@/components/Toolbar'
import { Table } from '@/components/table/Table'
import { BatchBar } from '@/components/BatchBar'
import { Modals } from '@/components/Modals'
import { Toasts } from '@/components/Toasts'
export default function Dashboard() {
const [files, setFiles] = useState<FileMetadata[]>([])
const [activeView, setActiveView] = useState<'all' | 'image' | 'video' | 'document' | 'trash' | 'tracking' | 'security'>('all')
const [selected, setSelected] = useState<Set<number>>(new Set())
const [search, setSearch] = useState('')
const [modal, setModal] = useState<ModalState>(null)
const [toasts, setToasts] = useState<Toast[]>([])
const fetchFiles = useCallback(async () => {
const folder = activeView === 'all' ? '' : activeView
const res = await fetch(`/api/files?folder=${folder}`)
const data = await res.json()
setFiles(data.files)
}, [activeView])
useEffect(() => { fetchFiles() }, [fetchFiles])
return (
<div className="flex h-screen overflow-hidden">
<Sidebar activeView={activeView} onViewChange={setActiveView} />
<div className="flex-1 flex flex-col overflow-hidden">
<Toolbar search={search} onSearch={setSearch} onUpload={() => setModal({ type: 'upload' })} />
{selected.size > 0 && <BatchBar selected={selected} files={files} onAction={handleBatchAction} />}
<Table files={filteredFiles} selected={selected} onSelect={setSelected} onAction={handleAction} />
</div>
<Modals modal={modal} onClose={() => setModal(null)} onSuccess={fetchFiles} />
<Toasts toasts={toasts} />
</div>
)
}const VIEWS = [
{ id: 'all', label: 'Tous les fichiers', icon: Folder },
{ id: 'image', label: 'Images', icon: Image },
{ id: 'video', label: 'Vidéos', icon: Video },
{ id: 'document', label: 'Documents', icon: FileText },
{ id: 'trash', label: 'Corbeille', icon: Trash2 },
{ id: 'tracking', label: 'Analytics', icon: BarChart2 },
{ id: 'security', label: 'Sécurité', icon: Shield },
]Apparaît quand au moins un fichier est sélectionné via les checkboxes du tableau :
export function BatchBar({ selected, files, onAction }) {
const selectedFiles = files.filter(f => selected.has(f.id))
return (
<div className="flex items-center gap-4 px-6 py-3 bg-card border-b border-line">
<span className="text-sm text-mute">{selected.size} sélectionné(s)</span>
<button onClick={() => onAction('download-batch', selectedFiles)}>
Télécharger (ZIP)
</button>
<button onClick={() => onAction('delete-batch', selectedFiles)}>
Supprimer
</button>
</div>
)
}export function Toasts({ toasts }: { toasts: Toast[] }) {
return (
<div className="fixed bottom-6 right-6 flex flex-col gap-2 z-50">
{toasts.map(toast => (
<div
key={toast.id}
className={`px-4 py-3 text-sm rounded border ${
toast.type === 'success' ? 'bg-green-950 border-green-700 text-green-300' :
toast.type === 'error' ? 'bg-red-950 border-red-700 text-red-300' :
'bg-card border-line text-ink'
}`}
>
{toast.message}
</div>
))}
</div>
)
}Côté client, pas de re-fetch à chaque keystroke — on filtre le state local :
const filteredFiles = files.filter(f =>
f.original_name.toLowerCase().includes(search.toLowerCase())
)Simple, immédiat, sans requête réseau.