41 lines
2.5 KiB
TypeScript
41 lines
2.5 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
|
||
type Attachment = { name?: string; filename?: string; url?: string; externalLink?: string; type?: string; size?: string | number };
|
||
|
||
function resourceUrl(attachment: Attachment, sourceBaseUrl?: string) {
|
||
if (attachment.url) return attachment.url;
|
||
if (attachment.externalLink) return attachment.externalLink;
|
||
if (!sourceBaseUrl || !attachment.name || !attachment.filename) return null;
|
||
const resourceName = attachment.name.split("/").map(encodeURIComponent).join("/");
|
||
return `${sourceBaseUrl.replace(/\/$/, "")}/file/${resourceName}/${encodeURIComponent(attachment.filename)}`;
|
||
}
|
||
|
||
export function Attachments({ json, sourceBaseUrl, compact = false }: { json: string; sourceBaseUrl?: string | null; compact?: boolean }) {
|
||
const [activeImage, setActiveImage] = useState<{ href: string; label: string } | null>(null);
|
||
useEffect(() => {
|
||
const closeOnEscape = (event: KeyboardEvent) => { if (event.key === "Escape") setActiveImage(null); };
|
||
window.addEventListener("keydown", closeOnEscape);
|
||
return () => window.removeEventListener("keydown", closeOnEscape);
|
||
}, []);
|
||
|
||
let attachments: Attachment[] = [];
|
||
try { attachments = JSON.parse(json); } catch { return null; }
|
||
const displayable = attachments.map((attachment) => ({ attachment, href: resourceUrl(attachment, sourceBaseUrl || undefined) })).filter((item): item is { attachment: Attachment; href: string } => Boolean(item.href));
|
||
if (!displayable.length) return null;
|
||
return <>
|
||
<section className={`attachments${compact ? " attachments-compact" : ""}`} aria-label="附件">
|
||
{displayable.map(({ attachment, href }) => {
|
||
const label = attachment.filename || attachment.name || "附件";
|
||
if (attachment.type?.startsWith("image/")) return <button type="button" className="attachment-image" onClick={() => setActiveImage({ href, label })} key={href} aria-label={`放大檢視:${label}`}><img src={href} alt={label} /></button>;
|
||
return <a className="attachment-file" href={href} target="_blank" rel="noreferrer" key={href}>📎 {label}</a>;
|
||
})}
|
||
</section>
|
||
{activeImage && <div className="image-lightbox" role="dialog" aria-modal="true" aria-label={activeImage.label} onClick={() => setActiveImage(null)}>
|
||
<button type="button" className="image-lightbox-close" onClick={() => setActiveImage(null)} aria-label="關閉圖片檢視">×</button>
|
||
<img src={activeImage.href} alt={activeImage.label} onClick={(event) => event.stopPropagation()} />
|
||
</div>}
|
||
</>;
|
||
}
|