GooDrive/frontend/src/components/drive/DriveExplorer.jsx
Louis Mazin a9f9f70f84 ok
2026-10-14 18:26:03 +02:00

139 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
ChevronRight, Clock3, FolderOpen, Grid2X2, List as ListIcon,
LoaderCircle, Search, Upload,
} from 'lucide-react'
import FileList from './FileList'
import FileGrid from './FileGrid'
import Banner from './StatusBanner'
import { headerNames } from '@/config/navigation'
import ContextActions, {fileOptions} from './ContextActions'
export default function DriveExplorer({ controller, user }) {
const {
view, setParent, trail, items, stats, query, setQuery, search, loading,
error, setError, note, setNote, layout, setLayout, sort, setSort,
display, goRoot, openEntry, startAction, performItemAction, filesInput, isTrash, canWrite, activeSpace,
} = controller
const [context, setContext] = useState(null)
const closeContext = useCallback(() => setContext(null), [])
const openContext = (item,x,y) => {
const options=fileOptions(item,isTrash,view==='shared')
if (options.length) setContext({item,x,y,options})
}
useEffect(() => {
if (context && !display.some(item => item.id === context.item.id && item.space_id === context.item.space_id)) closeContext()
}, [display, context, closeContext])
useEffect(() => { closeContext() }, [view, activeSpace?.id, closeContext])
// In "Partagés avec moi", space statistics belong to the last selected
// space. Count the displayed shared files and folders instead.
const visibleCounts = loading ? null : view === 'shared'
? { folders: display.filter(item => item.is_folder).length, files: display.filter(item => !item.is_folder).length }
: view === 'files' ? stats : null
const title = search
? `« ${search} »`
: trail.length && !search ? trail.at(-1)?.name : view === 'files' ? (activeSpace?.display_name || headerNames[view]) : (view === 'shared' ? 'Partagés avec moi' : headerNames[view])
return (
<section className="flex min-h-0 flex-1 flex-col overflow-hidden px-4 pb-4 pt-6 sm:px-7 sm:pb-7">
<div className="mb-4 flex items-center gap-2 lg:hidden">
<Search className="size-4 text-muted-foreground" />
<Input value={query} onChange={event => setQuery(event.target.value)} placeholder="Rechercher dans mes fichiers…" aria-label="Rechercher" />
</div>
<div className="mb-5 flex flex-wrap items-end justify-between gap-3">
<div className="min-w-0">
{!isTrash && <div className="mb-2 flex items-center gap-1.5 text-xs text-muted-foreground">
<button onClick={goRoot} className="rounded px-1 py-1 hover:bg-muted">{view === 'shared' ? 'Partagés avec moi' : activeSpace?.is_private ? 'Mes fichiers' : activeSpace?.display_name || 'Mes fichiers'}</button>
{(view === 'files' || view === 'shared') && !search && trail.map(folder => (
<React.Fragment key={folder.id}>
<ChevronRight className="size-3.5" />
<button className="max-w-28 truncate rounded px-1 py-1 hover:bg-muted" onClick={() => setParent(folder.id)}>{folder.name}</button>
</React.Fragment>
))}
{view !== 'files' && view !== 'shared' && <>
<ChevronRight className="size-3.5" />
<span>{headerNames[view]}</span>
</>}
</div>}
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{isTrash && (
<p className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
<Clock3 className="size-3.5 shrink-0" aria-hidden="true" />
Les éléments de la corbeille sont supprimés définitivement après 5 jours.
</p>
)}
<p className="mt-1 text-sm text-muted-foreground">
{loading ? 'Chargement…' : `${items.length} élément${items.length > 1 ? 's' : ''}`}
{!canWrite ? ' · lecture seule' : ''}
</p>
</div>
<div className="flex items-center gap-1.5">
<span className="mr-1 hidden text-xs text-muted-foreground sm:inline">Trier :</span>
<select value={sort} onChange={event => setSort(event.target.value)} className="h-9 rounded-lg border bg-card px-2 text-xs outline-none sm:text-sm">
<option value="name">Nom</option>
<option value="date">Date</option>
<option value="size">Taille</option>
</select>
<div className="ml-1 flex rounded-lg border bg-card p-0.5">
<Button size="icon-sm" variant={layout === 'list' ? 'secondary' : 'ghost'} aria-label="Liste" onClick={() => setLayout('list')}>
<ListIcon />
</Button>
<Button size="icon-sm" variant={layout === 'grid' ? 'secondary' : 'ghost'} aria-label="Grille" onClick={() => setLayout('grid')}>
<Grid2X2 />
</Button>
</div>
</div>
</div>
{error && <div className="mb-3"><Banner text={error} close={() => setError('')} /></div>}
{note && <div className="mb-3"><Banner text={note} close={() => setNote('')} /></div>}
<div className="min-h-0 flex-1 overflow-y-auto rounded-2xl border bg-card shadow-sm">
{loading ? (
<div className="flex h-full min-h-72 items-center justify-center gap-3 text-sm text-muted-foreground">
<LoaderCircle className="size-5 animate-spin" /> Chargement des fichiers…
</div>
) : !display.length ? (
<div className="flex min-h-80 flex-col items-center justify-center px-5 text-center">
<div className="mb-4 grid size-20 place-items-center rounded-3xl bg-muted">
<FolderOpen className="size-9 text-primary" />
</div>
<h3 className="text-lg font-semibold">
{isTrash ? 'La corbeille est vide' : search ? 'Aucun résultat' : view === 'shared' ? 'Aucun fichier partagé' : 'Cet espace est vide'}
</h3>
<p className="mt-2 max-w-sm text-sm leading-relaxed text-muted-foreground">
{isTrash
? 'Les fichiers et dossiers supprimés apparaîtront ici.'
: search ? 'Essaie un autre nom de fichier.'
: view === 'shared' ? 'Les éléments que d’autres utilisateurs partagent avec toi apparaîtront ici.' : 'Importe tes premiers fichiers ou crée un dossier pour commencer à organiser ton espace.'}
</p>
{!isTrash && !search && canWrite && (
<Button className="mt-5" onClick={() => filesInput.current?.click()}>
<Upload /> Importer des fichiers
</Button>
)}
</div>
) : layout === 'list' ? (
<FileList items={display} user={user} onOpen={isTrash ? () => {} : openEntry} onActions={startAction} onContext={openContext} />
) : (
<FileGrid items={display} onOpen={isTrash ? () => {} : openEntry} onActions={startAction} onContext={openContext} />
)}
</div>
<ContextActions menu={context && {...context, options:fileOptions(context.item,isTrash,view==='shared')}} onClose={closeContext}
onChoose={type => performItemAction(context.item,type)} />
<footer className="flex justify-between gap-2 px-1 pt-3 text-[11px] text-muted-foreground">
<span className="hidden md:inline">Clic droit sur un élément pour les actions.</span>
<span>{visibleCounts ? `${visibleCounts.folders} dossier${visibleCounts.folders === 1 ? '' : 's'} · ${visibleCounts.files} fichier${visibleCounts.files === 1 ? '' : 's'}` : ''}</span>
</footer>
</section>
)
}