Something went wrong. Try again.
A Social Deduction / Deception Game
Something went wrong. Try again.
4.3 kB · 101 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102// The map chooser, rendered on the takeover's back page (light/paper theme — dark ink on the white// sheet). Lists the server's curated maps (town.deception.listMaps) and offers a borderless field to// paste a map AT-URI directly. Picking one continues the flow (investigation for deduction, the// create-game form for deception); the back button returns to the mode picker.import { useEffect, useState } from 'react'import { listMaps, type MapRef } from './maps-api'
const MAP_URI_RE = /^at:\/\/[^/]+\/world\.tilemap\.map\/[^/]+$/
export function MapSelector({ onBack, onPick }: { onBack: () => void; onPick: (map: MapRef) => void }) { const [maps, setMaps] = useState<MapRef[] | null>(null) const [error, setError] = useState<string | null>(null) const [custom, setCustom] = useState('') const customUri = custom.trim() const customOk = MAP_URI_RE.test(customUri)
useEffect(() => { // The fetch is typically already in flight (prefetched on folder hover/open), so this usually // resolves immediately from the shared cache. let cancelled = false listMaps() .then((m) => !cancelled && setMaps(m)) .catch((e) => !cancelled && setError(e instanceof Error ? e.message : String(e))) return () => { cancelled = true } }, [])
function submitCustom(e: React.FormEvent) { e.preventDefault() if (customOk) onPick({ uri: customUri, name: 'Custom map' }) }
return ( <div className="flex h-full flex-col"> <button onClick={onBack} className="self-start font-case text-xs text-stone-500 underline-offset-2 hover:text-stone-900 hover:underline" > ← back </button>
<header className="mt-3 shrink-0"> <h2 className="font-case text-2xl text-stone-900">Choose a Map</h2> <p className="mt-1 font-case text-[10px] uppercase tracking-widest text-stone-500">Case site</p> </header>
{error && <p className="mt-4 font-case text-sm text-red-700">{error}</p>} {!maps && !error && <p className="mt-4 font-case text-sm text-stone-500">loading maps…</p>} {maps && maps.length === 0 && <p className="mt-4 font-case text-sm text-stone-500">No maps available.</p>}
{maps && maps.length > 0 && ( <ul className="mt-5 min-h-0 flex-1 space-y-3 overflow-y-auto pr-1"> {maps.map((m) => ( <li key={m.uri}> <button onClick={() => onPick(m)} className="group w-full border-b border-dashed border-stone-300 pb-3 text-left hover:border-stone-800 focus:outline-none" > <div className="flex items-baseline justify-between gap-3"> <span className="font-case text-lg text-stone-900">{m.name}</span> <span aria-hidden className="font-case text-stone-400 transition-transform group-hover:translate-x-0.5 group-hover:text-stone-900" > → </span> </div> <div className="mt-0.5 truncate font-mono text-[10px] text-stone-400">{m.uri}</div> </button> </li> ))} </ul> )}
{/* Paste a map AT-URI directly — styled to read as a line written on the page, not a form field. */} <form onSubmit={submitCustom} className="mt-5 shrink-0"> <label className="font-case text-[10px] uppercase tracking-widest text-stone-500">Or name a site</label> <div className="mt-1 flex items-baseline gap-2"> <input value={custom} onChange={(e) => setCustom(e.target.value)} spellCheck={false} autoCapitalize="off" autoCorrect="off" placeholder="at://…/world.tilemap.map/…" className="min-w-0 flex-1 border-0 bg-transparent p-0 font-case text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-0" /> <button type="submit" disabled={!customOk} aria-label="use this map" className="font-case text-stone-400 transition-colors hover:text-stone-900 disabled:opacity-30 disabled:hover:text-stone-400" > → </button> </div> </form> </div> )}