GooDrive/frontend/src/pages/PublicSharePage.jsx
Louis Mazin 17afb70810 super
2026-10-14 18:51:52 +02:00

102 lines
8.3 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 { useCallback, useEffect, useRef, useState } from 'react'
import { ArrowLeft, Download, File, Folder, FolderOpen, LoaderCircle, Eye, X, Image, FileText, ShieldCheck, Maximize2, Minimize2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { drive, message, publicFileUrl } from '@/api/client'
import { fmtSize } from '@/lib/drive-format'
function typeOf(item) {
const n = (item.name || '').toLowerCase(), m=(item.mime_type || '').toLowerCase()
if(m.startsWith('image/') && m!=='image/svg+xml' || /\.(png|jpe?g|gif|webp|bmp|avif)$/.test(n)) return 'image'
if(n.endsWith('.docx'))return 'word'
if(m==='application/pdf' || n.endsWith('.pdf'))return 'pdf'
if(m.startsWith('video/') || /\.(mp4|webm|mov|m4v|ogv)$/.test(n))return 'video'
if(m.startsWith('audio/') || /\.(mp3|ogg|wav|m4a|flac|aac)$/.test(n))return 'audio'
if(m.startsWith('text/') || /\.(txt|md|json|csv|xml|log|py|js|css)$/.test(n))return 'text'
return null
}
function Viewer({item,token,fullscreen}) {
const kind=typeOf(item)
const src=publicFileUrl(token,item.id,true)
const mediaHeight=fullscreen?'max-h-[calc(100dvh-5rem)]':'max-h-[72vh]'
const frameHeight=fullscreen?'h-[calc(100dvh-5rem)]':'h-[70vh]'
if(kind==='image')return <img className={`mx-auto ${mediaHeight} max-w-full rounded-lg object-contain`} alt={item.name} src={src}/>
if(kind==='word')return <iframe title={item.name} src={src} sandbox="" className={`${frameHeight} w-full rounded-xl bg-white`}/>
if(kind==='pdf')return <iframe title={item.name} src={src} className={`${frameHeight} w-full rounded-xl bg-white`}/>
if(kind==='video')return <video controls playsInline preload="metadata" src={src} className={`${mediaHeight} w-full rounded-xl`}/>
if(kind==='audio')return <audio controls src={src} className="w-full"/>
if(kind==='text')return <iframe sandbox="" title={item.name} src={src} className={`${frameHeight} w-full rounded-xl bg-white`}/>
return <div className="p-10 text-center text-muted-foreground">Aperçu indisponible pour ce format.</div>
}
export default function PublicSharePage({token}) {
const [parent,setParent]=useState(null)
const [data,setData]=useState(null)
const [selected,setSelected]=useState(null)
const [error,setError]=useState('')
const [loading,setLoading]=useState(true)
const viewerRef=useRef(null)
const [fullscreen,setFullscreen]=useState(false)
const [fullscreenSupported,setFullscreenSupported]=useState(false)
useEffect(()=>{
setFullscreenSupported(Boolean(document.fullscreenEnabled))
const update=()=>setFullscreen(document.fullscreenElement===viewerRef.current)
document.addEventListener('fullscreenchange', update)
return ()=>document.removeEventListener('fullscreenchange', update)
},[])
const closeViewer=()=>{
if(document.fullscreenElement===viewerRef.current) document.exitFullscreen?.().catch(()=>{})
setSelected(null)
}
const toggleFullscreen=async()=>{
try {
if(document.fullscreenElement===viewerRef.current) await document.exitFullscreen()
else await viewerRef.current?.requestFullscreen()
} catch {
// Browsers may deny fullscreen; leave the normal viewer usable.
}
}
const load=useCallback(async()=>{
setLoading(true)
try { setData(await drive.publicBrowse(token,parent));setError('') }
catch(e){setError(e?.response?.status===404?'Ce lien a expiré ou n’est plus disponible.':message(e))}
finally{setLoading(false)}
},[token,parent])
useEffect(()=>{load()},[load])
// A public file link first shows its details; preview starts only on "Ouvrir".
useEffect(()=>{setSelected(null)},[token])
const previous=()=>setParent(data?.breadcrumb?.length>1?data.breadcrumb.at(-2).id:null)
const items=data?.items || []
return <div className="min-h-dvh bg-muted/25 px-4 py-5 sm:py-12">
<div className="mx-auto max-w-5xl space-y-5">
<header className="flex items-center gap-3 px-1"><img src="/icon.png" alt="GoodDrive" className="size-12 rounded-xl"/><div><h1 className="text-xl font-bold tracking-tight">GoodDrive</h1><p className="text-sm text-muted-foreground">Un partage, tout simplement.</p></div></header>
<main className="overflow-hidden rounded-3xl border bg-card shadow-sm">
{loading ? <div className="flex min-h-72 items-center justify-center gap-3"><LoaderCircle className="size-5 animate-spin"/> Chargement…</div>
: error ? <div className="p-10 text-center text-destructive" role="alert">{error}</div>
: data && <>
<div className="border-b bg-muted/20 px-5 py-6 sm:px-8">
<div className="mb-3 flex items-center gap-2 text-xs font-medium text-muted-foreground"><ShieldCheck className="size-4"/> Partage public en lecture seule</div>
<h2 className="break-words text-2xl font-semibold tracking-tight">{data.root.name}</h2>
{data.root.is_folder ? <p className="mt-2 text-sm text-muted-foreground">{parent ? data.breadcrumb?.map(i=>i.name).join(' / ') : `${items.length} élément${items.length>1?'s':''}`}</p> : <p className="mt-2 text-sm text-muted-foreground">{fmtSize(data.root.size)}</p>}
</div>
{data.root.is_folder && <div className="px-3 py-3 sm:px-5 sm:py-5">
{parent && <Button variant="ghost" size="sm" className="mb-3" onClick={previous}><ArrowLeft className="size-4"/> Retour</Button>}
<div className="space-y-1.5">{items.length ? items.map(item=><div key={item.id} className="group flex items-center gap-3 rounded-xl border border-transparent p-3 transition-colors hover:border-border hover:bg-muted/45">
<div className="grid size-11 shrink-0 place-items-center rounded-xl bg-primary/10 text-primary">{item.is_folder?<Folder className="size-6"/>:typeOf(item)==='image'?<Image className="size-5"/>:typeOf(item)==='pdf'?<FileText className="size-5"/>:<File className="size-5"/>}</div>
<button type="button" onClick={()=>item.is_folder?setParent(item.id):setSelected(item)} className="min-w-0 flex-1 text-left"><span className="block truncate text-sm font-medium">{item.name}</span><span className="text-xs text-muted-foreground">{item.is_folder?'Dossier':fmtSize(item.size)}</span></button>
<Button variant="outline" size="sm" onClick={()=>item.is_folder?setParent(item.id):setSelected(item)}>{item.is_folder?<FolderOpen className="size-4"/>:<Eye className="size-4"/>} Ouvrir</Button>
{!item.is_folder && <a className="grid size-8 shrink-0 place-items-center rounded-lg hover:bg-muted" aria-label={`Télécharger ${item.name}`} href={publicFileUrl(token,item.id)}><Download className="size-4"/></a>}
</div>):<p className="py-12 text-center text-sm text-muted-foreground">Ce dossier est vide.</p>}</div>
</div>}
{!data.root.is_folder && <div className="flex flex-wrap items-center justify-end gap-3 px-5 py-5 sm:px-8">
<Button onClick={()=>setSelected(data.root)} className="gap-2"><Eye className="size-4"/> Ouvrir</Button>
<a className="inline-flex h-10 items-center gap-2 rounded-xl border bg-background px-4 text-sm font-medium hover:bg-muted" href={publicFileUrl(token,data.root.id)}><Download className="size-4"/> Télécharger</a>
</div>}
</>}
</main>
</div>
{selected && <div ref={viewerRef} className="fixed inset-0 z-50 flex flex-col bg-black/95 p-3 text-white sm:p-6" role="dialog" aria-modal="true" aria-label={`Visualiser ${selected.name}`}>
<div className="mb-4 flex items-center justify-between gap-2"><p className="min-w-0 truncate font-medium">{selected.name}</p><div className="flex items-center gap-2">{fullscreenSupported && !['video', 'audio'].includes(typeOf(selected)) && <Button variant="secondary" size="icon" aria-label={fullscreen ? "Quitter le plein écran" : "Afficher en plein écran"} title={fullscreen ? "Réduire" : "Plein écran"} onClick={toggleFullscreen}>{fullscreen ? <Minimize2 className="size-5"/> : <Maximize2 className="size-5"/>}</Button>}<a className="inline-flex items-center gap-2 rounded-xl bg-white/15 px-3 py-2 text-sm hover:bg-white/25" href={publicFileUrl(token,selected.id)}><Download className="size-4"/> Télécharger</a><Button variant="secondary" size="icon" aria-label="Fermer l’aperçu" onClick={closeViewer}><X className="size-5"/></Button></div></div>
<div className="flex min-h-0 flex-1 items-center justify-center overflow-auto"><Viewer item={selected} token={token} fullscreen={fullscreen}/></div>
</div>}
</div>
}