import { useRef, useState } from 'react'; import { Link } from '@tanstack/react-router'; import { Map as MapIcon, Cast, Download, ExternalLink, X } from 'lucide-react'; import type { BattleMap, Campaign } from '@/lib/schemas'; import { mapsRepo } from '@/lib/db/repositories'; import { parseUvtt, toUvtt } from '@/lib/vtt/uvtt'; import { downloadText, safeFilename } from '@/lib/io/file'; import { imageSize } from '@/lib/img/resize'; import { useUiStore } from '@/stores/uiStore'; import { useMaps } from './hooks'; import { Page, PageHeader, EmptyState, RequireCampaign } from '@/components/ui/Page'; import { Button } from '@/components/ui/Button'; import { cn } from '@/lib/cn'; import { MapEditor } from './map/MapEditor'; const MAX_FILE_BYTES = 24 * 1024 * 1024; const VTT_RE = /\.(dd2vtt|uvtt|df2vtt)$/i; export function MapsPage() { return {(c) => }; } function Maps({ campaign }: { campaign: Campaign }) { const maps = useMaps(campaign.id); const [selectedId, setSelectedId] = useState(null); const [error, setError] = useState(null); const fileRef = useRef(null); const activeMapId = useUiStore((s) => s.activeMapId); const setActiveMap = useUiStore((s) => s.setActiveMap); const selected = maps.find((m) => m.id === selectedId) ?? null; const importFile = (file: File) => { setError(null); if (file.size > MAX_FILE_BYTES) { setError('File is larger than 24 MB.'); return; } const isVtt = VTT_RE.test(file.name); const name = file.name.replace(/\.[^.]+$/, ''); const reader = new FileReader(); reader.onload = async () => { const result = reader.result; if (typeof result !== 'string') return; try { if (isVtt) { const vtt = parseUvtt(result); if (!vtt) { setError('Could not read that .dd2vtt / .uvtt file.'); return; } const map = await mapsRepo.createFromVtt(campaign.id, name, vtt); setSelectedId(map.id); } else { const map = await mapsRepo.create(campaign.id, name, result); setSelectedId(map.id); } } catch { setError('Import failed.'); } }; if (isVtt) reader.readAsText(file); else reader.readAsDataURL(file); }; const exportUvtt = async (map: BattleMap) => { const { w, h } = await imageSize(map.image); const cols = Math.max(0, Math.round(w / map.gridSize)); const rows = Math.max(0, Math.round(h / map.gridSize)); const obj = toUvtt({ image: map.image, gridSize: map.gridSize, cols, rows, walls: map.walls ?? [], doors: map.doors ?? [], lights: map.lights ?? [], }); downloadText(`${safeFilename(map.name)}.uvtt`, JSON.stringify(obj)); }; return ( fileRef.current?.click()}>+ Add map} /> { const f = e.target.files?.[0]; if (f) importFile(f); e.target.value = ''; }} /> {error &&

{error}

} {maps.length === 0 ? ( fileRef.current?.click()}>+ Add map} /> ) : (
    {maps.map((mp) => { const active = selected?.id === mp.id; return (
  • ); })}
{selected ? (
Open player view
) : (
Select a map.
)}
)}
); }