139 lines
7.5 KiB
JavaScript
139 lines
7.5 KiB
JavaScript
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>
|
||
)
|
||
}
|