Files
initiative/apps/web/src/components/bulk-import-prompt.tsx
Lukas 1c107a500b
All checks were successful
CI / check (push) Successful in 2m25s
CI / build-image (push) Successful in 23s
Switch PF2e data source from Pf2eTools to Foundry VTT PF2e
Replace the stagnant Pf2eTools bestiary with Foundry VTT PF2e system
data (github.com/foundryvtt/pf2e, v13-dev branch). This gives us 4,355
remaster-era creatures across 49 sources including Monster Core 1+2 and
all adventure paths.

Changes:
- Rewrite index generation script to walk Foundry pack directories
- Rewrite PF2e normalization adapter for Foundry JSON shape (system.*
  fields, items[] for attacks/abilities/spells)
- Add stripFoundryTags utility for Foundry HTML + enrichment syntax
- Implement multi-file source fetching (one request per creature file)
- Add spellcasting section to PF2e stat block (ranked spells + cantrips)
- Add saveConditional and hpDetails to PF2e domain type and stat block
- Add size and rarity to PF2e trait tags
- Filter redundant glossary abilities (healing when in hp.details,
  spell mechanic reminders, allSaves duplicates)
- Add PF2e stat block component tests (22 tests)
- Bump IndexedDB cache version to 5 for clean migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:05:00 +02:00

119 lines
3.6 KiB
TypeScript

import { Loader2 } from "lucide-react";
import { useId, useState } from "react";
import { useAdapters } from "../contexts/adapter-context.js";
import { useBestiaryContext } from "../contexts/bestiary-context.js";
import { useBulkImportContext } from "../contexts/bulk-import-context.js";
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
import { useSidePanelContext } from "../contexts/side-panel-context.js";
import { Button } from "./ui/button.js";
import { Input } from "./ui/input.js";
const DND_BASE_URL =
"https://raw.githubusercontent.com/5etools-mirror-3/5etools-src/main/data/bestiary/";
const PF2E_BASE_URL =
"https://raw.githubusercontent.com/foundryvtt/pf2e/v13-dev/packs/pf2e/";
export function BulkImportPrompt() {
const { bestiaryIndex, pf2eBestiaryIndex } = useAdapters();
const { fetchAndCacheSource, isSourceCached, refreshCache } =
useBestiaryContext();
const { state: importState, startImport, reset } = useBulkImportContext();
const { dismissPanel } = useSidePanelContext();
const { edition } = useRulesEditionContext();
const indexPort = edition === "pf2e" ? pf2eBestiaryIndex : bestiaryIndex;
const defaultUrl = edition === "pf2e" ? PF2E_BASE_URL : DND_BASE_URL;
const [baseUrl, setBaseUrl] = useState(defaultUrl);
const baseUrlId = useId();
const totalSources = indexPort.getAllSourceCodes().length;
const handleStart = (url: string) => {
startImport(url, fetchAndCacheSource, isSourceCached, refreshCache);
};
const handleDone = () => {
dismissPanel();
reset();
};
if (importState.status === "complete") {
return (
<div className="flex flex-col gap-4">
<div className="rounded-md border border-green-500/50 bg-green-500/10 px-3 py-2 text-green-400 text-sm">
All sources loaded
</div>
<Button onClick={handleDone}>Done</Button>
</div>
);
}
if (importState.status === "partial-failure") {
return (
<div className="flex flex-col gap-4">
<div className="rounded-md border border-yellow-500/50 bg-yellow-500/10 px-3 py-2 text-sm text-yellow-400">
Loaded {importState.completed}/{importState.total} sources (
{importState.failed} failed)
</div>
<Button onClick={handleDone}>Done</Button>
</div>
);
}
if (importState.status === "loading") {
const processed = importState.completed + importState.failed;
const pct =
importState.total > 0
? Math.round((processed / importState.total) * 100)
: 0;
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<Loader2 className="h-4 w-4 animate-spin" />
Loading sources... {processed}/{importState.total}
</div>
<div className="h-2 overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary transition-all"
style={{ width: `${pct}%` }}
/>
</div>
</div>
);
}
// idle state
const isDisabled = !baseUrl.trim() || importState.status !== "idle";
return (
<div className="flex flex-col gap-4">
<div>
<h3 className="font-semibold text-foreground text-sm">
Import All Sources
</h3>
<p className="mt-1 text-muted-foreground text-xs">
Load stat block data for all {totalSources} sources at once.
</p>
</div>
<div className="flex flex-col gap-2">
<label htmlFor={baseUrlId} className="text-muted-foreground text-xs">
Base URL
</label>
<Input
id={baseUrlId}
type="url"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
className="text-xs"
/>
</div>
<Button onClick={() => handleStart(baseUrl)} disabled={isDisabled}>
Load All
</Button>
</div>
);
}