import { useEffect, useMemo, useState } from 'react' import axios from 'axios'; function safeGet(key) { try { if (typeof window === 'undefined' || !('localStorage' in window)) return null const raw = window.localStorage.getItem(key) return raw ? JSON.parse(raw) : null } catch { return null } } function safeSet(key, val) { try { if (typeof window !== 'undefined' && 'localStorage' in window) window.localStorage.setItem(key, JSON.stringify(val)) } catch {} } function safeRemove(key) { try { if (typeof window !== 'undefined' && 'localStorage' in window) window.localStorage.removeItem(key) } catch {} } async function sha256Hex(str) { try { const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)) const arr = Array.from(new Uint8Array(buf)) return arr.map(b => b.toString(16).padStart(2, '0')).join('') } catch { return str } } const STORAGE_SHOP_PLAYERS = 'dw:shop:players:v1' const STORAGE_SHOP_AUTHED = 'dw:shop:authedPlayer' const GM_PASSWORD = 'bongo' const RANK_ORDER = ['None','Respected','Distinguished','Famed','Hero'] function normalizeRank(r) { const s = String(r||'').trim() const found = RANK_ORDER.find(x=>x.toLowerCase()===s.toLowerCase()) return found || (s? s : 'None') } function rankIndex(r) { const idx = RANK_ORDER.indexOf(normalizeRank(r)); return idx<0?0:idx } function renownClass(r) { const rr = normalizeRank(r) if (rr==='Respected') return 'bg-emerald-700' if (rr==='Distinguished') return 'bg-indigo-700' if (rr==='Famed') return 'bg-purple-700' if (rr==='Hero') return 'bg-rose-700' return 'bg-slate-700' } function slugify(s){ return String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'') } function normalizeItem(it) { const name = String(it.name||'').trim() if (!name) return null const id = String(it.id||slugify(name)) const category = String(it.category||'Gear').trim() const req = Math.max(0, Number.isFinite(+it.req) ? +it.req : (Number.isFinite(+it.cost)? +it.cost : 0)) const cost = req const desc = String(it.desc||'').trim() const renown = normalizeRank(it.renown||it.Renown) return { id, name, category, cost, req, renown, desc } } export default function RequisitionShop() { const [items, setItems] = useState([]) const [players, setPlayers] = useState(() => safeGet(STORAGE_SHOP_PLAYERS) || []); const [authedPlayer, setAuthedPlayer] = useState(() => safeGet(STORAGE_SHOP_AUTHED) || ''); const [gmOpen, setGmOpen] = useState(false) const [gmPassInput, setGmPassInput] = useState('') const [gmUnlocked, setGmUnlocked] = useState(false) const [playerName, setPlayerName] = useState('') const [playerPw, setPlayerPw] = useState('') const [search, setSearch] = useState('') const categories = useMemo(()=>{ const set = new Set() for (const it of items) { if (it && it.category) set.add(String(it.category)) } return ['All', ...Array.from(set).sort((a,b)=>String(a).localeCompare(String(b)))] },[items]) const [categoryFilter, setCategoryFilter] = useState('All') // Items are sourced from a single JSON file; disable local persistence // useEffect(()=>{ safeSet(STORAGE_SHOP_ITEMS, items) },[items]) useEffect(()=>{ safeSet(STORAGE_SHOP_PLAYERS, players) },[players]) useEffect(()=>{ // Always load from the static JSON on startup ;(async()=>{ try { const res = await fetch('/deathwatch-armoury.json', { cache: 'no-store' }) if (!res.ok) return const arr = await res.json() const next = Array.isArray(arr) ? arr.map(normalizeItem).filter(Boolean) : [] if (next.length>0) { setItems(next) } } catch {} })() // eslint-disable-next-line react-hooks/exhaustive-deps },[]) useEffect(()=>{ // Restore authed player on mount const ap = safeGet(STORAGE_SHOP_AUTHED) if (ap && typeof ap === 'string') setAuthedPlayer(ap) },[]) const currentPlayer = useMemo(()=> players.find(p=>p.name===authedPlayer) || null,[players, authedPlayer]) const filteredItems = useMemo(()=>{ const q = search.trim().toLowerCase() return items.filter(i => { const inQuery = !q || i.name.toLowerCase().includes(q) || (i.category||'').toLowerCase().includes(q) const inCat = categoryFilter==='All' || String(i.category||'').toLowerCase()===String(categoryFilter).toLowerCase() return inQuery && inCat }) },[items, search, categoryFilter]) // Define flash function const [saveMsg, setSaveMsg] = useState(''); function flash(msg) { setSaveMsg(msg); setTimeout(() => setSaveMsg(''), 2000); } // Ensure GM Panel login functionality works correctly async function handleGmUnlock() { if (gmPassInput === GM_PASSWORD) { setGmUnlocked(true); flash('GM Panel unlocked'); } else { flash('Incorrect GM password'); } } // Ensure player login functionality is consistent async function handlePlayerLogin() { const player = players.find(p => p.name === playerName); if (player && player.pw === playerPw) { setAuthedPlayer(playerName); flash('Player logged in'); } else { flash('Invalid player credentials'); } } function handlePlayerLogout() { safeRemove(STORAGE_SHOP_AUTHED); setAuthedPlayer(''); flash('Player logged out'); } // Updated purchaseItem to subtract requisition points from the player async function purchaseItem(item) { if (!currentPlayer) return; const updatedPlayer = { ...currentPlayer, rp: (currentPlayer.rp || 0) - (item.cost || 0), inventory: [...(currentPlayer.inventory || []), item], transactions: [...(currentPlayer.transactions || []), { item, date: new Date() }], }; try { await axios.put(`/api/players/${updatedPlayer.name}`, updatedPlayer); setPlayers(prevPlayers => { const index = prevPlayers.findIndex(p => p.name === updatedPlayer.name); if (index !== -1) { const newPlayers = [...prevPlayers]; newPlayers[index] = updatedPlayer; return newPlayers; } return [...prevPlayers, updatedPlayer]; }); } catch (error) { console.error('Failed to update player:', error); } } async function addPlayer(name, rp, pw) { const nm = String(name||'').trim() if (!nm) return const h = await sha256Hex(String(pw||'').trim()) setPlayers(prev => { const others = prev.filter(x=>x.name!==nm) return [...others, { name:nm, rp: Math.max(0, Number.isFinite(+rp)? +rp : 0), pwHash: h, inventory: [], renown: 'None' }].sort((a,b)=>a.name.localeCompare(b.name)) }) } function setPlayerRP(name, rp) { const nm = String(name||'').trim() setPlayers(prev => prev.map(p => p.name===nm ? { ...p, rp: Math.max(0, Number.isFinite(+rp)? +rp : 0) } : p)) } function setPlayerRenown(name, renown) { const nm = String(name||'').trim() setPlayers(prev => prev.map(p => p.name===nm ? { ...p, renown: normalizeRank(renown) } : p)) } async function resetPlayerPW(name, pw) { const nm = String(name || '').trim(); if (!nm) { flash('Player name is required'); return; } const h = await sha256Hex(String(pw || '').trim()); setPlayers(prev => prev.map(p => p.name === nm ? { ...p, pw: pw, pwHash: h } : p)); flash(`Password reset for ${nm}`); } function deletePlayer(name) { setPlayers(prev => prev.filter(p => p.name!==name)) } function setItemMeta(id, req, renown) { setItems(prev => prev.map(it => it.id===id ? { ...it, req: Math.max(0, Number.isFinite(+req)? +req : (it.req||0)), cost: Math.max(0, Number.isFinite(+req)? +req : (it.cost||0)), renown: normalizeRank(renown||it.renown) } : it)) } // Display saveMsg in the UI return (
Requisition Shop
{saveMsg && (
{saveMsg}
)}
Player Login
setPlayerPw(e.target.value)} /> {!authedPlayer ? ( ) : ( )}
RP: {currentPlayer ? (currentPlayer.rp||0) : '-'}{' '} • Renown: {currentPlayer ? (currentPlayer.renown||'None') : '-'}
Items
setSearch(e.target.value)} />
{filteredItems.map(it => (
{it.name}
{it.category}
Req {it.req ?? it.cost ?? 0}
{it.renown || 'None'} {gmUnlocked && ( )}
))} {filteredItems.length===0 && (
No items
)}
Inventory {currentPlayer ? `• ${currentPlayer.name}` : ''}
{currentPlayer && currentPlayer.inventory && currentPlayer.inventory.length > 0 ? ( currentPlayer.inventory.map((inv, i) => (
{inv.name} ({inv.cost})
)) ) : (
No purchases
)} {currentPlayer && currentPlayer.transactions && currentPlayer.transactions.length > 0 && (
Transaction History:
{currentPlayer.transactions.map((txn, i) => (
{txn.item.name} - {txn.item.cost} Req - {new Date(txn.date).toLocaleString()}
))}
)}
{gmOpen && (
GM Controls
{!gmUnlocked ? (
setGmPassInput(e.target.value)} />
) : (
Players
{players.map(p=> (
{p.name}
RP: {p.rp||0}
))} {players.length===0 && (
No players
)}
GM Settings
{ setGmPassInput(''); /* placeholder: fixed bongo; not persisted */ }} />
)}
)}
) } function GmAddPlayer({ onAdd }) { const [name, setName] = useState('') const [rp, setRp] = useState(10) const [pw, setPw] = useState('') return (
setName(e.target.value)} /> setRp(parseInt(e.target.value||'0'))} /> setPw(e.target.value)} />
Add or overwrite by name
) } function GmSetRP({ name, onSet }) { const [rp, setRp] = useState('') return (
setRp(e.target.value)} />
) } function GmSetRenown({ name, value, onSet }) { const [renown, setRenown] = useState(value) return (
) } function GmResetPW({ name, onReset }) { const [pw, setPw] = useState('') return (
setPw(e.target.value)} />
) } function GmEditItemMeta({ item, onSet }) { const [req, setReq] = useState(item.req ?? item.cost ?? 0) const [renown, setRenown] = useState(item.renown || 'None') return (
setReq(e.target.value)} />
) } function GmChangePassword({ onSet }){ const [pw, setPw] = useState('') return (
setPw(e.target.value)} />
) }