203 lines
6.4 KiB
TypeScript
203 lines
6.4 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
|
import Link from "next/link"
|
|
import { Button } from "@/components/ui/button"
|
|
import { ArrowLeft, Loader2 } from "lucide-react"
|
|
import { OfficeEditorChrome } from "@/components/drive/office-editor-chrome"
|
|
import { RichTextDocumentEditor } from "@/components/drive/richtext-document"
|
|
import { displayFileBaseName } from "@/lib/drive/display-file-name"
|
|
import { resolvePublicShareEditReturnTo, shouldShowPublicShareEditorBack } from "@/lib/drive/public-share-url"
|
|
import type { PublicShareRootType } from "@/lib/drive/public-share-url"
|
|
import { useDriveDocumentTitle } from "@/lib/drive/use-drive-document-title"
|
|
import { getGuestEditorIdentity } from "@/lib/drive/guest-editor-identity"
|
|
import type { RichTextSaveStatus, RichTextSessionResponse } from "@/lib/drive/richtext-types"
|
|
import { fetchPublicShareBlob } from "@/lib/api/public-share"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
function fileNameFromPath(filePath: string, fallback?: string): string {
|
|
const base = filePath.split("/").filter(Boolean).pop()
|
|
return base || fallback || filePath
|
|
}
|
|
|
|
function saveStatusLabel(status: RichTextSaveStatus): string {
|
|
switch (status) {
|
|
case "saving":
|
|
return "Enregistrement…"
|
|
case "saved":
|
|
return "Enregistré"
|
|
case "error":
|
|
return "Erreur d'enregistrement"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
export function PublicRichTextEditor({
|
|
token,
|
|
filePath,
|
|
password,
|
|
returnTo,
|
|
mode = "edit",
|
|
fileDisplayName,
|
|
shareRoot,
|
|
}: {
|
|
token: string
|
|
filePath: string
|
|
password?: string
|
|
returnTo?: string | null
|
|
mode?: "edit" | "view"
|
|
fileDisplayName?: string
|
|
shareRoot?: PublicShareRootType | null
|
|
}) {
|
|
const guest = useMemo(() => getGuestEditorIdentity(token), [token])
|
|
const [session, setSession] = useState<RichTextSessionResponse | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [saveStatus, setSaveStatus] = useState<RichTextSaveStatus>("idle")
|
|
const [resolvedMode, setResolvedMode] = useState<"edit" | "view">(mode)
|
|
|
|
const fileName = fileDisplayName || fileNameFromPath(filePath)
|
|
const title = displayFileBaseName(fileName)
|
|
useDriveDocumentTitle(title)
|
|
|
|
const backHref = useMemo(
|
|
() => resolvePublicShareEditReturnTo(token, returnTo, filePath),
|
|
[token, returnTo, filePath]
|
|
)
|
|
const showBack = shouldShowPublicShareEditorBack(shareRoot, returnTo, filePath)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
setSession(null)
|
|
setError(null)
|
|
void (async () => {
|
|
try {
|
|
const res = await fetch(
|
|
`/api/v1/drive/public/shares/${encodeURIComponent(token)}/richtext/session`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
path: filePath,
|
|
mode,
|
|
password: password ?? "",
|
|
guest_id: guest.guestId,
|
|
guest_name: guest.guestName,
|
|
display_name: fileName,
|
|
}),
|
|
}
|
|
)
|
|
if (!res.ok) throw new Error("Session indisponible")
|
|
const data = (await res.json()) as RichTextSessionResponse
|
|
if (!cancelled) {
|
|
setSession(data)
|
|
setResolvedMode(data.mode === "view" ? "view" : mode)
|
|
}
|
|
} catch (e) {
|
|
if (!cancelled) setError(e instanceof Error ? e.message : "Impossible d'ouvrir le document")
|
|
}
|
|
})()
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [token, filePath, mode, password, guest.guestId, guest.guestName, fileName])
|
|
|
|
const fetchSourceBytes = useCallback(
|
|
async (path: string) => {
|
|
const blob = await fetchPublicShareBlob(
|
|
token,
|
|
{ path, name: path.split("/").pop() ?? path },
|
|
password
|
|
)
|
|
return blob.arrayBuffer()
|
|
},
|
|
[token, password]
|
|
)
|
|
|
|
const importApi = useCallback(
|
|
async (body: { source_path: string; content: Record<string, unknown> }) => {
|
|
const res = await fetch(
|
|
`/api/v1/drive/public/shares/${encodeURIComponent(token)}/richtext/import`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ ...body, password: password ?? "", display_name: fileName }),
|
|
}
|
|
)
|
|
if (!res.ok) throw new Error("Import impossible")
|
|
},
|
|
[token, password, fileName]
|
|
)
|
|
|
|
const statusText = saveStatusLabel(saveStatus)
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex h-full flex-col items-center justify-center gap-4 p-8">
|
|
<p className="text-sm text-muted-foreground">{error}</p>
|
|
{showBack ? (
|
|
<Button variant="outline" asChild>
|
|
<Link href={backHref}>
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Retour
|
|
</Link>
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full min-h-0 flex-col bg-background">
|
|
<OfficeEditorChrome
|
|
backHref={backHref}
|
|
backLabel="Partage"
|
|
showBack={showBack}
|
|
title={title}
|
|
showShare={false}
|
|
showAccount={false}
|
|
trailing={
|
|
<div className="flex items-center gap-3">
|
|
<span
|
|
className="rounded px-2 py-1 text-xs font-semibold text-white"
|
|
style={{ backgroundColor: guest.color }}
|
|
>
|
|
Vous êtes {guest.guestName}
|
|
</span>
|
|
{resolvedMode === "view" ? (
|
|
<span className="text-xs text-muted-foreground">Lecture seule</span>
|
|
) : null}
|
|
{statusText ? (
|
|
<span
|
|
className={cn(
|
|
"text-xs text-muted-foreground",
|
|
saveStatus === "error" && "text-destructive"
|
|
)}
|
|
>
|
|
{statusText}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
}
|
|
/>
|
|
<div className="min-h-0 flex-1">
|
|
{!session ? (
|
|
<div className="flex h-full items-center justify-center">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : (
|
|
<RichTextDocumentEditor
|
|
session={session}
|
|
mode={resolvedMode}
|
|
userName={guest.guestName}
|
|
userColor={guest.color}
|
|
onSaveStatus={setSaveStatus}
|
|
fetchSourceBytes={fetchSourceBytes}
|
|
importApi={importApi}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|