// reported-mods-panel.jsx - ReportedModsPanel component + helpers // Loaded after sortof-shared.jsx; defines globals that sortof-app.jsx references. const { useState, useEffect, useMemo } = React; function fmtReportDate(iso) { if (!iso) return ''; const d = new Date(iso); if (isNaN(d.getTime())) return iso; const pad = (n) => String(n).padStart(2, '0'); const HH = pad(d.getUTCHours()); const MM = pad(d.getUTCMinutes()); const mo = pad(d.getUTCMonth() + 1); const da = pad(d.getUTCDate()); const yy = d.getUTCFullYear(); return `${HH}:${MM} ${mo}-${da}-${yy}`; } const _VOTED_KEY = 'sortof.brokenmod.voted'; function loadVoted() { try { return JSON.parse(localStorage.getItem(_VOTED_KEY) || '{}') || {}; } catch { return {}; } } function saveVoted(v) { try { localStorage.setItem(_VOTED_KEY, JSON.stringify(v)); } catch {} } function ReportedModsPanel({ onClose }) { const [versions, setVersions] = useState({}); const [reports, setReports] = useState([]); const [search, setSearch] = useState(''); const [wsidIn, setWsidIn] = useState(''); const [verIn, setVerIn] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); const [voted, setVoted] = useState(loadVoted()); useEffect(() => { let cancelled = false; (async () => { try { const [vRes, rRes] = await Promise.all([ fetch('/api/pz-versions').then(r => r.json()), fetch('/api/broken-mods').then(r => r.json()), ]); if (cancelled) return; setVersions(vRes || {}); setReports(Array.isArray(rRes) ? rRes : []); const firstKey = Object.keys(vRes || {})[0]; if (firstKey) setVerIn(firstKey); } catch (e) { if (!cancelled) setError(`load failed: ${e?.message || e}`); } })(); return () => { cancelled = true; }; }, []); async function onSubmit(e) { e.preventDefault(); setError(''); const wsid = (wsidIn || '').trim(); if (!/^\d+$/.test(wsid)) { setError('workshop id must be digits only'); return; } if (!verIn) { setError('pick a version'); return; } setSubmitting(true); try { const res = await fetch('/api/broken-mods', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ workshop_id: wsid, version: verIn }), }); if (!res.ok) { const txt = await res.text(); setError(`submit failed: ${res.status} ${txt}`); return; } setWsidIn(''); const list = await fetch('/api/broken-mods').then(r => r.json()); setReports(Array.isArray(list) ? list : []); } catch (e) { setError(`network error: ${e?.message || e}`); } finally { setSubmitting(false); } } async function onVote(id, direction) { if (voted[id]) return; try { const res = await fetch(`/api/broken-mods/${id}/vote`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ direction }), }); if (!res.ok) return; const counts = await res.json(); setReports(prev => prev.map(r => r.id === id ? { ...r, upvotes: counts.upvotes, downvotes: counts.downvotes } : r)); const next = { ...voted, [id]: direction }; setVoted(next); saveVoted(next); } catch {} } const filtered = useMemo(() => { const q = (search || '').trim().toLowerCase(); if (!q) return reports; return reports.filter(r => (r.workshop_id || '').toLowerCase().includes(q) || (r.mod_name || '').toLowerCase().includes(q), ); }, [reports, search]); return (
Reported broken mods
add a workshop id + which PZ build it broke on. duplicates of the same (id, version) bubble back to the top with their existing votes.
setWsidIn(e.target.value)} inputMode="numeric" spellCheck={false} />
{error &&
{error}
} setSearch(e.target.value)} />
{filtered.length} {filtered.length === 1 ? 'report' : 'reports'} {search && reports.length !== filtered.length ? ` (of ${reports.length})` : ''}
); } Object.assign(window, { ReportedModsPanel, fmtReportDate, loadVoted, saveVoted });