Files
initiative/apps/web/src/components/source-fetch-prompt.tsx
Lukas 86768842ff
All checks were successful
CI / check (push) Successful in 1m18s
CI / build-image (push) Has been skipped
Refactor App.tsx from god component to context-based architecture
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:33:33 +01:00

128 lines
3.3 KiB
TypeScript

import { Download, Loader2, Upload } from "lucide-react";
import { useId, useRef, useState } from "react";
import {
getDefaultFetchUrl,
getSourceDisplayName,
} from "../adapters/bestiary-index-adapter.js";
import { useBestiaryContext } from "../contexts/bestiary-context.js";
import { Button } from "./ui/button.js";
import { Input } from "./ui/input.js";
interface SourceFetchPromptProps {
sourceCode: string;
onSourceLoaded: () => void;
}
export function SourceFetchPrompt({
sourceCode,
onSourceLoaded,
}: Readonly<SourceFetchPromptProps>) {
const { fetchAndCacheSource, uploadAndCacheSource } = useBestiaryContext();
const sourceDisplayName = getSourceDisplayName(sourceCode);
const [url, setUrl] = useState(() => getDefaultFetchUrl(sourceCode));
const [status, setStatus] = useState<"idle" | "fetching" | "error">("idle");
const [error, setError] = useState<string>("");
const fileInputRef = useRef<HTMLInputElement>(null);
const sourceUrlId = useId();
const handleFetch = async () => {
setStatus("fetching");
setError("");
try {
await fetchAndCacheSource(sourceCode, url);
onSourceLoaded();
} catch (e) {
setStatus("error");
setError(e instanceof Error ? e.message : "Failed to fetch source data");
}
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setStatus("fetching");
setError("");
try {
const text = await file.text();
const json = JSON.parse(text);
await uploadAndCacheSource(sourceCode, json);
onSourceLoaded();
} catch (err) {
setStatus("error");
setError(
err instanceof Error ? err.message : "Failed to process uploaded file",
);
}
// Reset file input
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
return (
<div className="flex flex-col gap-4">
<div>
<h3 className="font-semibold text-foreground text-sm">
Load {sourceDisplayName}
</h3>
<p className="mt-1 text-muted-foreground text-xs">
Stat block data for this source needs to be loaded. Enter a URL or
upload a JSON file.
</p>
</div>
<div className="flex flex-col gap-2">
<label htmlFor={sourceUrlId} className="text-muted-foreground text-xs">
Source URL
</label>
<Input
id={sourceUrlId}
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
disabled={status === "fetching"}
className="text-xs"
/>
</div>
<div className="flex items-center gap-2">
<Button onClick={handleFetch} disabled={status === "fetching" || !url}>
{status === "fetching" ? (
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
) : (
<Download className="mr-1 h-3 w-3" />
)}
{status === "fetching" ? "Loading..." : "Load"}
</Button>
<span className="text-muted-foreground text-xs">or</span>
<Button
variant="outline"
onClick={() => fileInputRef.current?.click()}
disabled={status === "fetching"}
>
<Upload className="mr-1 h-3 w-3" />
Upload file
</Button>
<input
ref={fileInputRef}
type="file"
accept=".json"
className="hidden"
onChange={handleFileUpload}
/>
</div>
{status === "error" && (
<div className="rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-destructive text-xs">
{error}
</div>
)}
</div>
);
}