e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Loader2 } from "lucide-react";
|
|
|
|
/**
|
|
* Image preview. Uses a native <img> instead of next/image because:
|
|
* 1. The /api/attachments URL is dynamic per-session and not in the
|
|
* next.config remotePatterns whitelist.
|
|
* 2. We want object-contain centering against a checkered backdrop.
|
|
*
|
|
* Data URLs (the pending-attachment case) work the same way.
|
|
*/
|
|
export default function ImagePreview({
|
|
url,
|
|
filename,
|
|
}: {
|
|
url: string;
|
|
filename: string;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
|
|
|
|
return (
|
|
<div
|
|
className="relative flex h-full w-full items-center justify-center bg-[var(--muted)]/30"
|
|
style={{
|
|
backgroundImage:
|
|
"linear-gradient(45deg, rgba(0,0,0,0.04) 25%, transparent 25%), " +
|
|
"linear-gradient(-45deg, rgba(0,0,0,0.04) 25%, transparent 25%), " +
|
|
"linear-gradient(45deg, transparent 75%, rgba(0,0,0,0.04) 75%), " +
|
|
"linear-gradient(-45deg, transparent 75%, rgba(0,0,0,0.04) 75%)",
|
|
backgroundSize: "16px 16px",
|
|
backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0px",
|
|
}}
|
|
>
|
|
{state === "loading" && (
|
|
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center">
|
|
<Loader2
|
|
size={16}
|
|
className="animate-spin text-[var(--muted-foreground)]"
|
|
/>
|
|
</div>
|
|
)}
|
|
{state === "error" ? (
|
|
<div className="text-[12px] text-[var(--muted-foreground)]">
|
|
{t("Failed to load image.")}
|
|
</div>
|
|
) : (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={url}
|
|
alt={filename}
|
|
className="max-h-full max-w-full object-contain"
|
|
onLoad={() => setState("ready")}
|
|
onError={() => setState("error")}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|