iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
TypeScript · Avancé

OpenFiler

Construire une plateforme de gestion de fichiers complète — upload, authentification, analytics, sécurité fail2ban et déploiement Docker.

Next.js 15TypeScriptSQLiteBetter AuthTailwind CSS v4
GitHub
01Setup et configuration02Authentification avec Better Auth03Base de données SQLite04Système d'upload de fichiers05Dashboard et interface06Corbeille et liens de partage07Déploiement Docker
Chapitre 5·35 min

Dashboard et interface

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.

Architecture du dashboard

app/dashboard.tsx
'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>
  )
}

Sidebar — navigation

components/Sidebar.tsx
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 },
]

Barre d'actions groupées

Apparaît quand au moins un fichier est sélectionné via les checkboxes du tableau :

components/BatchBar.tsx
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>
  )
}

Système de toasts

components/Toasts.tsx
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>
  )
}

Filtrage en temps réel

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.

Précédent
Système d'upload de fichiers
Suivant
Corbeille et liens de partage

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main