"use client"; import { useEffect, useRef, useState } from "react"; type Phase = | { kind: "idle" } | { kind: "loading" } | { kind: "ready"; text: string } | { kind: "error"; message: string }; type Props = { src: string; filename: string; }; export function TextFilePreview({ src, filename }: Props) { const [phase, setPhase] = useState({ kind: "idle" }); const abortRef = useRef(null); useEffect( () => () => { abortRef.current?.abort(); }, [], ); function handleToggle(e: React.SyntheticEvent) { if (!e.currentTarget.open || phase.kind !== "idle") return; const controller = new AbortController(); abortRef.current = controller; setPhase({ kind: "loading" }); fetch(src, { signal: controller.signal }) .then(async (res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.text(); }) .then((text) => { if (controller.signal.aborted) return; let display = text; if (filename.toLowerCase().endsWith(".json")) { try { display = JSON.stringify(JSON.parse(text), null, 2); } catch { // Keep raw text on parse failure (empty file, truncated, etc). } } setPhase({ kind: "ready", text: display }); }) .catch((err: unknown) => { if (controller.signal.aborted) return; const message = err instanceof Error ? err.message : String(err); setPhase({ kind: "error", message }); }); } return (
Preview contents
{phase.kind === "loading" && (

Loading…

)} {phase.kind === "error" && (

Failed to load: {phase.message}

)} {phase.kind === "ready" && (
            {phase.text}
          
)}
); }