132 lines
3.3 KiB
TypeScript
132 lines
3.3 KiB
TypeScript
import { Download, Loader2, Upload } from "lucide-react";
|
|
import { useRef, useState } from "react";
|
|
import { getDefaultFetchUrl } from "../adapters/bestiary-index-adapter.js";
|
|
import { Button } from "./ui/button.js";
|
|
import { Input } from "./ui/input.js";
|
|
|
|
interface SourceFetchPromptProps {
|
|
sourceCode: string;
|
|
sourceDisplayName: string;
|
|
fetchAndCacheSource: (sourceCode: string, url: string) => Promise<void>;
|
|
onSourceLoaded: () => void;
|
|
onUploadSource: (sourceCode: string, jsonData: unknown) => Promise<void>;
|
|
}
|
|
|
|
export function SourceFetchPrompt({
|
|
sourceCode,
|
|
sourceDisplayName,
|
|
fetchAndCacheSource,
|
|
onSourceLoaded,
|
|
onUploadSource,
|
|
}: SourceFetchPromptProps) {
|
|
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 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 onUploadSource(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="text-sm font-semibold text-foreground">
|
|
Load {sourceDisplayName}
|
|
</h3>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
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="source-url" className="text-xs text-muted-foreground">
|
|
Source URL
|
|
</label>
|
|
<Input
|
|
id="source-url"
|
|
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
|
|
size="sm"
|
|
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-xs text-muted-foreground">or</span>
|
|
|
|
<Button
|
|
size="sm"
|
|
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-xs text-destructive">
|
|
{error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|