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 (