Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure.
649 lines
21 KiB
TypeScript
649 lines
21 KiB
TypeScript
"use client"
|
||
|
||
import dynamic from "next/dynamic"
|
||
import { useRouter } from "next/navigation"
|
||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"
|
||
import {
|
||
ChevronLeft,
|
||
ChevronRight,
|
||
Copy,
|
||
Download,
|
||
ExternalLink,
|
||
FolderInput,
|
||
HardDrive,
|
||
Loader2,
|
||
Star,
|
||
Trash2,
|
||
UserPlus,
|
||
X,
|
||
} from "lucide-react"
|
||
import Link from "next/link"
|
||
import { toast } from "sonner"
|
||
import {
|
||
Dialog,
|
||
DialogClose,
|
||
DialogContent,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog"
|
||
import { Button } from "@/components/ui/button"
|
||
import { DriveMoveDialog, type DriveFolderPickerMode } from "@/components/drive/drive-move-dialog"
|
||
import { downloadDriveFile, fetchDrivePreviewBlob } from "@/lib/api/drive-download"
|
||
import { fetchPublicShareBlob } from "@/lib/api/public-share"
|
||
import { resolveDemoDrivePreview } from "@/lib/demo/demo-drive-preview"
|
||
import { useIsDemoDrive } from "@/lib/demo/demo-drive-context"
|
||
import { downloadPublicShareFile } from "@/lib/drive/open-public-share-item"
|
||
import { useDriveMutations } from "@/lib/api/hooks/use-drive-queries"
|
||
import { ApiRequestError } from "@/lib/api/client"
|
||
import { displayFileName } from "@/lib/drive/display-file-name"
|
||
import {
|
||
drivePreviewKind,
|
||
isSvgFile,
|
||
previewTargetToFileInfo,
|
||
type DrivePreviewKind,
|
||
} from "@/lib/drive/drive-preview"
|
||
import { SvgPreviewViewer } from "@/components/drive/svg-preview-viewer"
|
||
import { useDriveUIStore } from "@/lib/stores/drive-ui-store"
|
||
import { apiClient } from "@/lib/api/client"
|
||
import { MailDriveFolderPicker } from "@/components/mail/mail-drive-folder-picker"
|
||
import { useSaveAttachmentToDrive } from "@/lib/api/hooks/use-mail-drive-save"
|
||
import {
|
||
mailDriveFileHref,
|
||
mailDriveFolderHref,
|
||
mailDriveFolderLabel,
|
||
mailDriveSaveErrorMessage,
|
||
mailDriveSaveSuccessMessage,
|
||
} from "@/lib/mail/mail-drive"
|
||
import { cn } from "@/lib/utils"
|
||
|
||
const TEXT_PREVIEW_MAX_BYTES = 2 * 1024 * 1024
|
||
|
||
const PdfPreviewViewer = dynamic(
|
||
() =>
|
||
import("@/components/drive/pdf-preview-viewer").then((m) => m.PdfPreviewViewer),
|
||
{
|
||
ssr: false,
|
||
loading: () => (
|
||
<div className="flex h-full flex-col items-center justify-center gap-2 text-zinc-400">
|
||
<Loader2 className="h-10 w-10 animate-spin" aria-hidden />
|
||
<span className="text-sm">Ouverture du PDF…</span>
|
||
</div>
|
||
),
|
||
},
|
||
)
|
||
|
||
const PREVIEW_ACTION_BTN =
|
||
"cursor-pointer text-zinc-300 hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-40"
|
||
|
||
function PreviewActionButton({
|
||
label,
|
||
onClick,
|
||
disabled,
|
||
className,
|
||
children,
|
||
}: {
|
||
label: string
|
||
onClick: () => void
|
||
disabled?: boolean
|
||
className?: string
|
||
children: ReactNode
|
||
}) {
|
||
return (
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
className={cn(PREVIEW_ACTION_BTN, className)}
|
||
onClick={onClick}
|
||
disabled={disabled}
|
||
aria-label={label}
|
||
>
|
||
{children}
|
||
</Button>
|
||
)
|
||
}
|
||
|
||
function PreviewBody({
|
||
kind,
|
||
blobUrl,
|
||
name,
|
||
textContent,
|
||
svgMarkup,
|
||
onImageError,
|
||
}: {
|
||
kind: DrivePreviewKind
|
||
blobUrl: string
|
||
name: string
|
||
textContent?: string | null
|
||
svgMarkup?: string | null
|
||
onImageError?: () => void
|
||
}) {
|
||
if (kind === "text") {
|
||
return (
|
||
<pre className="h-full w-full overflow-auto rounded-md bg-zinc-900 p-4 text-left font-mono text-sm leading-relaxed whitespace-pre-wrap break-words text-zinc-100">
|
||
{textContent ?? ""}
|
||
</pre>
|
||
)
|
||
}
|
||
if (svgMarkup) {
|
||
return <SvgPreviewViewer markup={svgMarkup} name={name} />
|
||
}
|
||
if (kind === "image") {
|
||
return (
|
||
<img
|
||
src={blobUrl}
|
||
alt={displayFileName(name)}
|
||
className="max-h-full max-w-full object-contain"
|
||
onError={onImageError}
|
||
/>
|
||
)
|
||
}
|
||
if (kind === "video") {
|
||
return (
|
||
<video
|
||
src={blobUrl}
|
||
controls
|
||
autoPlay
|
||
className="max-h-full max-w-full rounded-md bg-black"
|
||
playsInline
|
||
>
|
||
<track kind="captions" />
|
||
</video>
|
||
)
|
||
}
|
||
if (kind === "audio") {
|
||
return (
|
||
<audio src={blobUrl} controls autoPlay className="w-full max-w-lg">
|
||
<track kind="captions" />
|
||
</audio>
|
||
)
|
||
}
|
||
return <PdfPreviewViewer key={blobUrl} blobUrl={blobUrl} name={name} />
|
||
}
|
||
|
||
export function FilePreviewDialog() {
|
||
const previewFiles = useDriveUIStore((s) => s.previewFiles)
|
||
const previewIndex = useDriveUIStore((s) => s.previewIndex)
|
||
const previewContext = useDriveUIStore((s) => s.previewContext)
|
||
const closePreview = useDriveUIStore((s) => s.closePreview)
|
||
const stepPreview = useDriveUIStore((s) => s.stepPreview)
|
||
const setSharePath = useDriveUIStore((s) => s.setSharePath)
|
||
const updatePreviewFavorite = useDriveUIStore((s) => s.updatePreviewFavorite)
|
||
const removePreviewFile = useDriveUIStore((s) => s.removePreviewFile)
|
||
const mutations = useDriveMutations()
|
||
const [folderPickerMode, setFolderPickerMode] = useState<DriveFolderPickerMode | null>(null)
|
||
const [mailSavePickerOpen, setMailSavePickerOpen] = useState(false)
|
||
|
||
const file = previewIndex >= 0 ? (previewFiles[previewIndex] ?? null) : null
|
||
const fileInfo = useMemo(() => (file ? previewTargetToFileInfo(file) : null), [file])
|
||
const allowShare = previewContext?.allowShare ?? true
|
||
const isTrash = previewContext?.isTrash ?? false
|
||
const publicShare = previewContext?.publicShare
|
||
const mailSource = previewContext?.mailSource ?? false
|
||
const mailMessageId = previewContext?.mailMessageId ?? file?.mailMessageId ?? ""
|
||
const router = useRouter()
|
||
const isDemoDrive = useIsDemoDrive()
|
||
const saveToDrive = useSaveAttachmentToDrive(mailMessageId)
|
||
const isMailAttachment = mailSource && Boolean(file?.mailAttachmentId)
|
||
const showWriteActions = !isTrash && !publicShare && !isMailAttachment
|
||
const mailDrivePath = isMailAttachment && file?.path.startsWith("/") ? file.path : undefined
|
||
const [blobUrl, setBlobUrl] = useState<string | null>(null)
|
||
const [textContent, setTextContent] = useState<string | null>(null)
|
||
const [svgMarkup, setSvgMarkup] = useState<string | null>(null)
|
||
const [loading, setLoading] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [imgFailed, setImgFailed] = useState(false)
|
||
const [demoPreviewAsImage, setDemoPreviewAsImage] = useState(false)
|
||
const blobUrlRef = useRef<string | null>(null)
|
||
|
||
const kind = file ? drivePreviewKind(file) : null
|
||
const isSvg = file ? isSvgFile(file) : false
|
||
const hasPrev = previewIndex > 0
|
||
const hasNext = previewIndex >= 0 && previewIndex < previewFiles.length - 1
|
||
const positionLabel =
|
||
previewFiles.length > 1 ? `${previewIndex + 1} / ${previewFiles.length}` : null
|
||
|
||
const revokeBlobUrl = () => {
|
||
if (blobUrlRef.current) {
|
||
URL.revokeObjectURL(blobUrlRef.current)
|
||
blobUrlRef.current = null
|
||
}
|
||
setBlobUrl(null)
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (!file || !kind) {
|
||
revokeBlobUrl()
|
||
setLoading(false)
|
||
setError(null)
|
||
setImgFailed(false)
|
||
setTextContent(null)
|
||
setSvgMarkup(null)
|
||
return
|
||
}
|
||
|
||
let cancelled = false
|
||
|
||
setLoading(true)
|
||
setError(null)
|
||
setImgFailed(false)
|
||
setDemoPreviewAsImage(false)
|
||
setTextContent(null)
|
||
setSvgMarkup(null)
|
||
|
||
;(async () => {
|
||
try {
|
||
if (isDemoDrive && !publicShare && !file.mailAttachmentId) {
|
||
const resolved = resolveDemoDrivePreview(file, { width: 1600, height: 1200 })
|
||
if (!resolved) {
|
||
throw new Error("Aperçu indisponible en mode démo.")
|
||
}
|
||
if (cancelled) return
|
||
if (resolved.type === "text") {
|
||
if (blobUrlRef.current) {
|
||
URL.revokeObjectURL(blobUrlRef.current)
|
||
blobUrlRef.current = null
|
||
}
|
||
setBlobUrl(null)
|
||
setSvgMarkup(null)
|
||
setTextContent(resolved.content)
|
||
return
|
||
}
|
||
if (resolved.type === "svg") {
|
||
if (blobUrlRef.current) {
|
||
URL.revokeObjectURL(blobUrlRef.current)
|
||
blobUrlRef.current = null
|
||
}
|
||
setBlobUrl(null)
|
||
setSvgMarkup(resolved.markup)
|
||
return
|
||
}
|
||
setTextContent(null)
|
||
setSvgMarkup(null)
|
||
setBlobUrl(resolved.url)
|
||
setDemoPreviewAsImage(true)
|
||
return
|
||
}
|
||
|
||
const blob = file.mailAttachmentId
|
||
? await apiClient.getBlob(`/mail/attachments/${file.mailAttachmentId}`)
|
||
: publicShare
|
||
? await fetchPublicShareBlob(
|
||
publicShare.token,
|
||
{ path: file.path, name: file.name, mime_type: file.mime_type },
|
||
publicShare.password
|
||
)
|
||
: await fetchDrivePreviewBlob(file)
|
||
if (cancelled) return
|
||
if (kind === "text") {
|
||
if (blobUrlRef.current) {
|
||
URL.revokeObjectURL(blobUrlRef.current)
|
||
blobUrlRef.current = null
|
||
}
|
||
setBlobUrl(null)
|
||
setSvgMarkup(null)
|
||
if (blob.size > TEXT_PREVIEW_MAX_BYTES) {
|
||
setError("Fichier trop volumineux pour l’aperçu texte. Téléchargez-le.")
|
||
return
|
||
}
|
||
setTextContent(await blob.text())
|
||
return
|
||
}
|
||
if (isSvg) {
|
||
if (blobUrlRef.current) {
|
||
URL.revokeObjectURL(blobUrlRef.current)
|
||
blobUrlRef.current = null
|
||
}
|
||
setBlobUrl(null)
|
||
setSvgMarkup(await blob.text())
|
||
return
|
||
}
|
||
const url = URL.createObjectURL(blob)
|
||
const previous = blobUrlRef.current
|
||
blobUrlRef.current = url
|
||
setBlobUrl(url)
|
||
if (previous && previous !== url) {
|
||
URL.revokeObjectURL(previous)
|
||
}
|
||
} catch (err) {
|
||
if (!cancelled) {
|
||
const msg =
|
||
err instanceof ApiRequestError
|
||
? err.message
|
||
: "Impossible de charger l’aperçu."
|
||
setError(msg)
|
||
}
|
||
} finally {
|
||
if (!cancelled) setLoading(false)
|
||
}
|
||
})()
|
||
|
||
return () => {
|
||
cancelled = true
|
||
}
|
||
}, [
|
||
file?.path,
|
||
file?.mime_type,
|
||
file?.mailAttachmentId,
|
||
kind,
|
||
isSvg,
|
||
publicShare?.token,
|
||
publicShare?.password,
|
||
isDemoDrive,
|
||
])
|
||
|
||
const previewReady =
|
||
kind === "text"
|
||
? textContent !== null
|
||
: isSvg
|
||
? svgMarkup !== null
|
||
: Boolean(blobUrl)
|
||
|
||
useEffect(() => {
|
||
if (previewFiles.length === 0) return
|
||
|
||
const onKeyDown = (e: KeyboardEvent) => {
|
||
if (e.key === "ArrowLeft") {
|
||
e.preventDefault()
|
||
stepPreview(-1)
|
||
} else if (e.key === "ArrowRight") {
|
||
e.preventDefault()
|
||
stepPreview(1)
|
||
}
|
||
}
|
||
|
||
window.addEventListener("keydown", onKeyDown)
|
||
return () => window.removeEventListener("keydown", onKeyDown)
|
||
}, [previewFiles.length, stepPreview])
|
||
|
||
useEffect(() => {
|
||
return () => revokeBlobUrl()
|
||
}, [])
|
||
|
||
const title = file ? displayFileName(file.name) : ""
|
||
const open = Boolean(file && kind)
|
||
|
||
const onShare = () => {
|
||
if (!file) return
|
||
setSharePath(file.path, "file")
|
||
}
|
||
|
||
const onToggleFavorite = async () => {
|
||
if (!file) return
|
||
const next = !file.is_favorite
|
||
try {
|
||
await mutations.favorite.mutateAsync({ path: file.path, favorite: next })
|
||
updatePreviewFavorite(file.path, next)
|
||
toast.success(next ? "Ajouté aux favoris" : "Retiré des favoris")
|
||
} catch {
|
||
toast.error("Impossible de modifier les favoris")
|
||
}
|
||
}
|
||
|
||
const onDelete = async () => {
|
||
if (!file) return
|
||
try {
|
||
await mutations.deleteFile.mutateAsync(file.path)
|
||
removePreviewFile(file.path)
|
||
toast.success("Supprimé")
|
||
} catch {
|
||
toast.error("Impossible de supprimer")
|
||
}
|
||
}
|
||
|
||
const favoriteLabel = file?.is_favorite ? "Retirer des favoris" : "Ajouter aux favoris"
|
||
|
||
const onMailDownload = () => {
|
||
if (!file?.mailAttachmentId) return
|
||
void apiClient.getBlob(`/mail/attachments/${file.mailAttachmentId}`).then((blob) => {
|
||
const url = URL.createObjectURL(blob)
|
||
const a = document.createElement("a")
|
||
a.href = url
|
||
a.download = displayFileName(file.name)
|
||
a.click()
|
||
URL.revokeObjectURL(url)
|
||
})
|
||
}
|
||
|
||
const onMailSaveToDrive = async (folderPath: string) => {
|
||
if (!file?.mailAttachmentId || !mailMessageId) return
|
||
try {
|
||
const drivePath = await saveToDrive.mutateAsync({
|
||
attachmentId: file.mailAttachmentId,
|
||
folderPath,
|
||
})
|
||
useDriveUIStore.setState((state) => ({
|
||
previewFiles: state.previewFiles.map((f) =>
|
||
f.mailAttachmentId === file.mailAttachmentId
|
||
? { ...f, path: drivePath }
|
||
: f
|
||
),
|
||
}))
|
||
setMailSavePickerOpen(false)
|
||
toast.success(mailDriveSaveSuccessMessage(folderPath), {
|
||
action: {
|
||
label: "Ouvrir le dossier",
|
||
onClick: () => router.push(mailDriveFolderHref(folderPath)),
|
||
},
|
||
})
|
||
} catch (err) {
|
||
toast.error(mailDriveSaveErrorMessage(err))
|
||
}
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<MailDriveFolderPicker
|
||
open={mailSavePickerOpen}
|
||
onOpenChange={setMailSavePickerOpen}
|
||
title="Enregistrer dans UltiDrive"
|
||
description="Choisissez un dossier dans votre Drive."
|
||
confirmLabel="Enregistrer ici"
|
||
pending={saveToDrive.isPending}
|
||
onConfirm={onMailSaveToDrive}
|
||
/>
|
||
<DriveMoveDialog
|
||
open={folderPickerMode !== null}
|
||
onOpenChange={(nextOpen) => {
|
||
if (!nextOpen) setFolderPickerMode(null)
|
||
}}
|
||
mode={folderPickerMode ?? "move"}
|
||
sources={fileInfo ? [fileInfo] : []}
|
||
onMoved={() => {
|
||
if (folderPickerMode === "move" && file) removePreviewFile(file.path)
|
||
setFolderPickerMode(null)
|
||
}}
|
||
/>
|
||
<Dialog
|
||
open={open}
|
||
onOpenChange={(nextOpen) => {
|
||
if (!nextOpen) closePreview()
|
||
}}
|
||
>
|
||
<DialogContent
|
||
aria-describedby={undefined}
|
||
showCloseButton={false}
|
||
overlayClassName="bg-black/90"
|
||
className={cn(
|
||
"flex h-[min(92dvh,920px)] w-[min(96vw,1280px)] max-w-none flex-col gap-0 overflow-hidden",
|
||
"border-0 bg-zinc-950 p-0 text-zinc-100 shadow-2xl sm:max-w-none"
|
||
)}
|
||
>
|
||
<div className="flex h-14 shrink-0 items-center gap-2 border-b border-white/10 px-4">
|
||
<DialogTitle className="min-w-0 flex-1 truncate text-left text-base font-medium text-zinc-100">
|
||
{title}
|
||
</DialogTitle>
|
||
{positionLabel ? (
|
||
<span className="shrink-0 text-sm tabular-nums text-zinc-400">{positionLabel}</span>
|
||
) : null}
|
||
{file ? (
|
||
<div className="flex shrink-0 items-center gap-0.5">
|
||
{showWriteActions && allowShare ? (
|
||
<PreviewActionButton label="Partager" onClick={onShare}>
|
||
<UserPlus className="h-5 w-5" />
|
||
</PreviewActionButton>
|
||
) : null}
|
||
{showWriteActions ? (
|
||
<PreviewActionButton
|
||
label={favoriteLabel}
|
||
onClick={() => void onToggleFavorite()}
|
||
disabled={mutations.favorite.isPending}
|
||
className={cn(
|
||
file.is_favorite && "text-amber-400 hover:bg-amber-400/10 hover:text-amber-300"
|
||
)}
|
||
>
|
||
<Star
|
||
className="h-5 w-5"
|
||
fill={file.is_favorite ? "currentColor" : "none"}
|
||
/>
|
||
</PreviewActionButton>
|
||
) : null}
|
||
{showWriteActions ? (
|
||
<>
|
||
<PreviewActionButton
|
||
label="Déplacer vers"
|
||
onClick={() => setFolderPickerMode("move")}
|
||
>
|
||
<FolderInput className="h-5 w-5" />
|
||
</PreviewActionButton>
|
||
<PreviewActionButton
|
||
label="Copier vers"
|
||
onClick={() => setFolderPickerMode("copy")}
|
||
className="max-sm:hidden"
|
||
>
|
||
<Copy className="h-5 w-5" />
|
||
</PreviewActionButton>
|
||
</>
|
||
) : null}
|
||
{isMailAttachment && !mailDrivePath ? (
|
||
<PreviewActionButton
|
||
label="Enregistrer dans UltiDrive"
|
||
onClick={() => setMailSavePickerOpen(true)}
|
||
disabled={saveToDrive.isPending}
|
||
>
|
||
<HardDrive className="h-5 w-5" />
|
||
</PreviewActionButton>
|
||
) : null}
|
||
{isMailAttachment && mailDrivePath ? (
|
||
<PreviewActionButton
|
||
label={`Emplacement : ${mailDriveFolderLabel(mailDrivePath)}`}
|
||
onClick={() => {}}
|
||
disabled
|
||
className="!w-auto max-w-[min(40vw,16rem)] px-2 opacity-100"
|
||
>
|
||
<Link
|
||
href={mailDriveFileHref(mailDrivePath)}
|
||
className="inline-flex min-w-0 items-center gap-1.5 text-zinc-300 hover:text-white"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<ExternalLink className="h-4 w-4 shrink-0" />
|
||
<span className="truncate text-xs font-normal">
|
||
{mailDriveFolderLabel(mailDrivePath)}
|
||
</span>
|
||
</Link>
|
||
</PreviewActionButton>
|
||
) : null}
|
||
<PreviewActionButton
|
||
label="Télécharger"
|
||
onClick={() =>
|
||
void (isMailAttachment
|
||
? onMailDownload()
|
||
: publicShare
|
||
? downloadPublicShareFile(
|
||
publicShare.token,
|
||
{ path: file.path, name: file.name, mime_type: file.mime_type, type: "file", size: 0, last_modified: "", is_favorite: false, is_shared: false },
|
||
publicShare.password
|
||
)
|
||
: downloadDriveFile(file.path, file.name, file.name))
|
||
}
|
||
>
|
||
<Download className="h-5 w-5" />
|
||
</PreviewActionButton>
|
||
{showWriteActions ? (
|
||
<PreviewActionButton
|
||
label="Supprimer"
|
||
onClick={() => void onDelete()}
|
||
disabled={mutations.deleteFile.isPending}
|
||
className="hover:bg-red-500/10 hover:text-red-400"
|
||
>
|
||
<Trash2 className="h-5 w-5" />
|
||
</PreviewActionButton>
|
||
) : null}
|
||
<DialogClose asChild>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
className={PREVIEW_ACTION_BTN}
|
||
aria-label="Fermer"
|
||
>
|
||
<X className="h-5 w-5" />
|
||
</Button>
|
||
</DialogClose>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
<div className="relative flex min-h-0 flex-1 overflow-hidden">
|
||
{hasPrev ? (
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
className="absolute left-2 top-1/2 z-10 h-10 w-10 -translate-y-1/2 cursor-pointer rounded-full bg-black/40 text-white hover:bg-black/60"
|
||
onClick={() => stepPreview(-1)}
|
||
aria-label="Fichier précédent"
|
||
>
|
||
<ChevronLeft className="h-6 w-6" />
|
||
</Button>
|
||
) : null}
|
||
{hasNext ? (
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
className="absolute right-2 top-1/2 z-10 h-10 w-10 -translate-y-1/2 cursor-pointer rounded-full bg-black/40 text-white hover:bg-black/60"
|
||
onClick={() => stepPreview(1)}
|
||
aria-label="Fichier suivant"
|
||
>
|
||
<ChevronRight className="h-6 w-6" />
|
||
</Button>
|
||
) : null}
|
||
|
||
<div
|
||
className={cn(
|
||
"flex min-h-0 flex-1 overflow-hidden",
|
||
kind === "pdf" ? "flex-col p-0" : kind === "text" ? "flex-col p-4" : kind === "audio" ? "flex-col items-center justify-center p-8" : "items-center justify-center p-4"
|
||
)}
|
||
>
|
||
{loading ? (
|
||
<div className="flex flex-col items-center gap-2 text-zinc-400">
|
||
<Loader2 className="h-10 w-10 animate-spin" aria-hidden />
|
||
<span className="text-sm">Chargement…</span>
|
||
</div>
|
||
) : null}
|
||
{!loading && error ? (
|
||
<p className="text-center text-sm text-zinc-400">{error}</p>
|
||
) : null}
|
||
{!loading && !error && imgFailed ? (
|
||
<p className="text-center text-sm text-zinc-400">
|
||
Aperçu non pris en charge par le navigateur (ex. HEIC). Téléchargez le fichier.
|
||
</p>
|
||
) : null}
|
||
{!loading && !error && !imgFailed && kind && file && previewReady ? (
|
||
<PreviewBody
|
||
kind={demoPreviewAsImage ? "image" : kind}
|
||
blobUrl={blobUrl ?? ""}
|
||
name={file.name}
|
||
textContent={textContent}
|
||
svgMarkup={svgMarkup}
|
||
onImageError={() => setImgFailed(true)}
|
||
/>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</>
|
||
)
|
||
}
|