test

import React, { useState, useEffect } from ‘react’; import { Users, Swords, Plus, Trash2, Shuffle, Download, ArrowLeft } from ‘lucide-react’; function shuffleArray(array) { for (let i = array.length – 1; i > 0; i–) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } export default function App() { const [events, setEvents] = useState([]); const [currentEvent, setCurrentEvent] = useState(null); useEffect(() => { loadEvents(); }, []); const loadEvents = async () => { try { const result = await window.storage.get(‘hh-events-v3’, true); if (result?.value) setEvents(JSON.parse(result.value)); } catch (e) {} }; const saveEvents = async (e) => { try { await window.storage.set(‘hh-events-v3’, JSON.stringify(e), true); setEvents(e); } catch (err) {} }; if (!currentEvent) { return ; } return ; } function EventList({ events, setCurrentEvent, saveEvents }) { const [name, setName] = useState(”); const [show, setShow] = useState(false); const [del, setDel] = useState(null); return (

Horus Heresy Tournament

{!show ? ( ) : (
setName(e.target.value)} className=”w-full px-4 py-3 mb-4 rounded bg-white/20 text-white placeholder-gray-300″ />
)}
{events.map(e => (

{e.name}

{e.players.length} spelare
{e.rounds.length} rundor
{del === e.id ? (
Är du säker?
) : (
)}
))}
); } function EventDetail({ event, setCurrentEvent, events, saveEvents }) { const [tab, setTab] = useState(‘players’); const [np, setNp] = useState({ name: ”, club: ”, legion: ”, faction: ‘Loyalist’ }); const update = (u) => { const updated = { …event, …u }; saveEvents(events.map(e => e.id === event.id ? updated : e)); setCurrentEvent(updated); }; const addPlayer = () => { if (!np.name.trim()) return; update({ players: […event.players, { id: Date.now(), …np, wins: 0, losses: 0, draws: 0, points: 0, totalScore: 0, tablesUsed: [] }] }); setNp({ name: ”, club: ”, legion: ”, faction: ‘Loyalist’ }); }; const genRound = () => { if (event.rounds.length >= 10) return; const loys = event.players.filter(p => p.faction === ‘Loyalist’); const tras = event.players.filter(p => p.faction === ‘Traitor’); if (!loys.length || !tras.length) return; const hist = new Set(); event.rounds.forEach(r => r.matches.forEach(m => { if (m.loyalist && m.traitor) { hist.add(`${m.loyalist.id}-${m.traitor.id}`); hist.add(`${m.traitor.id}-${m.loyalist.id}`); } })); const sL = […loys].sort((a, b) => b.wins – a.wins); const sT = […tras].sort((a, b) => b.wins – a.wins); if (event.rounds.length === 0) { shuffleArray(sL); shuffleArray(sT); } const ms = []; const uL = new Set(); const uT = new Set(); for (const l of sL) { if (uL.has(l.id)) continue; let best = null; let bestS = -999; for (const t of sT) { if (uT.has(t.id) || hist.has(`${l.id}-${t.id}`)) continue; let s = -Math.abs(l.wins – t.wins) * 10; if (l.club && t.club && l.club === t.club) s -= 5; if (s > bestS) { bestS = s; best = t; } } if (best) { let tbl = (ms.length % event.numTables) + 1; if (l.tablesUsed.length > 0 || best.tablesUsed.length > 0) { let bTbl = tbl; let bS = 999; for (let t = 1; t <= event.numTables; t++) { let s = 0; if (l.tablesUsed.includes(t)) s += 10; if (best.tablesUsed.includes(t)) s += 10; if (s < bS) { bS = s; bTbl = t; } } tbl = bTbl; } ms.push({ id: Date.now() + ms.length, loyalist: l, traitor: best, table: tbl, winner: null, result: null, loyalistScore: '', traitorScore: '' }); uL.add(l.id); uT.add(best.id); update({ players: event.players.map(p => p.id === l.id || p.id === best.id ? { …p, tablesUsed: […(p.tablesUsed || []), tbl] } : p) }); } } const nr = { id: Date.now(), roundNumber: event.rounds.length + 1, matches: ms, timestamp: new Date().toLocaleString() }; update({ rounds: […event.rounds, nr] }); setTab(`round-${nr.id}`); }; const resetLast = () => { if (event.rounds.length === 0) return; const lr = event.rounds[event.rounds.length – 1]; const up = event.players.map(p => { const m = lr.matches.find(ma => ma.loyalist?.id === p.id || ma.traitor?.id === p.id); if (m && m.result) { let pts = 0, scr = 0; if (m.result === ‘win’) { if (m.winner === p.id) { pts = 2; p.wins = Math.max(0, p.wins – 1); } else { p.losses = Math.max(0, p.losses – 1); } } else if (m.result === ‘draw’) { pts = 1; p.draws = Math.max(0, p.draws – 1); } if (p.id === m.loyalist?.id) scr = parseInt(m.loyalistScore) || 0; else if (p.id === m.traitor?.id) scr = parseInt(m.traitorScore) || 0; return { …p, points: Math.max(0, p.points – pts), totalScore: Math.max(0, p.totalScore – scr), tablesUsed: p.tablesUsed.filter(t => t !== m.table) }; } return p; }); update({ rounds: event.rounds.slice(0, -1), players: up }); setTab(‘players’); }; const recWin = (ri, mi, wi) => { const m = event.rounds[ri].matches.find(ma => ma.id === mi); if (!m || m.result) return; const ls = parseInt(m.loyalistScore) || 0; const ts = parseInt(m.traitorScore) || 0; update({ rounds: event.rounds.map((r, i) => i === ri ? { …r, matches: r.matches.map(ma => ma.id === mi ? { …ma, result: ‘win’, winner: wi } : ma) } : r), players: event.players.map(p => { if (p.id === wi) return { …p, wins: p.wins + 1, points: p.points + 2, totalScore: p.totalScore + (p.id === m.loyalist?.id ? ls : ts) }; else if (p.id === m.loyalist?.id || p.id === m.traitor?.id) return { …p, losses: p.losses + 1, totalScore: p.totalScore + (p.id === m.loyalist?.id ? ls : ts) }; return p; }) }); }; const recDraw = (ri, mi) => { const m = event.rounds[ri].matches.find(ma => ma.id === mi); if (!m || m.result) return; const ls = parseInt(m.loyalistScore) || 0; const ts = parseInt(m.traitorScore) || 0; update({ rounds: event.rounds.map((r, i) => i === ri ? { …r, matches: r.matches.map(ma => ma.id === mi ? { …ma, result: ‘draw’, winner: null } : ma) } : r), players: event.players.map(p => { if (p.id === m.loyalist?.id) return { …p, draws: p.draws + 1, points: p.points + 1, totalScore: p.totalScore + ls }; else if (p.id === m.traitor?.id) return { …p, draws: p.draws + 1, points: p.points + 1, totalScore: p.totalScore + ts }; return p; }) }); }; const undoResult = (ri, mi) => { const m = event.rounds[ri].matches.find(ma => ma.id === mi); if (!m || !m.result) return; const ls = parseInt(m.loyalistScore) || 0; const ts = parseInt(m.traitorScore) || 0; const revertedPlayers = event.players.map(p => { if (p.id !== m.loyalist?.id && p.id !== m.traitor?.id) return p; const score = p.id === m.loyalist?.id ? ls : ts; if (m.result === ‘win’) { if (p.id === m.winner) return { …p, wins: Math.max(0, p.wins – 1), points: Math.max(0, p.points – 2), totalScore: Math.max(0, p.totalScore – score) }; else return { …p, losses: Math.max(0, p.losses – 1), totalScore: Math.max(0, p.totalScore – score) }; } else if (m.result === ‘draw’) { return { …p, draws: Math.max(0, p.draws – 1), points: Math.max(0, p.points – 1), totalScore: Math.max(0, p.totalScore – score) }; } return p; }); update({ rounds: event.rounds.map((r, i) => i === ri ? { …r, matches: r.matches.map(ma => ma.id === mi ? { …ma, result: null, winner: null } : ma) } : r), players: revertedPlayers }); }; const updScore = (ri, mi, side, scr) => { update({ rounds: event.rounds.map((r, i) => i === ri ? { …r, matches: r.matches.map(m => m.id === mi ? { …m, [side === ‘loyalist’ ? ‘loyalistScore’ : ‘traitorScore’]: scr } : m) } : r) }); }; const expCSV = (ri) => { const r = event.rounds[ri]; let csv = ‘Match,Table,Loyalist,Legion,Loyalist Score,Traitor,Legion,Traitor Score,Result\n’; r.matches.forEach((m, i) => { const res = m.result === ‘draw’ ? ‘Draw’ : m.winner === m.loyalist?.id ? `${m.loyalist?.name} Win` : m.winner === m.traitor?.id ? `${m.traitor?.name} Win` : ‘TBD’; csv += `${i + 1},${m.table},”${m.loyalist?.name || ”}”,”${m.loyalist?.legion || ”}”,${m.loyalistScore || 0},”${m.traitor?.name || ”}”,”${m.traitor?.legion || ”}”,${m.traitorScore || 0},”${res}”\n`; }); try { const blob = new Blob([csv], { type: ‘text/csv’ }); const url = URL.createObjectURL(blob); const a = document.createElement(‘a’); a.href = url; a.download = `${event.name.replace(/[^a-z0-9]/gi, ‘_’)}-Round-${r.roundNumber}.csv`; a.click(); } catch (e) {} }; const expDiscord = (ri) => { const r = event.rounds[ri]; let txt = `# ⚔️ ${event.name} — Round ${r.roundNumber}\n\n`; r.matches.forEach((m) => { const lName = m.loyalist?.name || ‘?’; const tName = m.traitor?.name || ‘?’; const lLeg = m.loyalist?.legion ? ` (${m.loyalist.legion})` : ”; const tLeg = m.traitor?.legion ? ` (${m.traitor.legion})` : ”; txt += `**Bord ${m.table}:** 🔵 ${lName}${lLeg} vs 🔴 ${tName}${tLeg}\n`; }); return txt; }; const loys = event.players.filter(p => p.faction === ‘Loyalist’); const tras = event.players.filter(p => p.faction === ‘Traitor’); return (

{event.name}

{event.players.length} spelare • {event.rounds.length} rundor

{event.rounds.map(r => ( ))}
{tab === ‘players’ && (

Lägg till spelare

setNp({…np, name: e.target.value})} className=”px-4 py-2 rounded bg-white/20 text-white placeholder-gray-300″ /> setNp({…np, club: e.target.value})} className=”px-4 py-2 rounded bg-white/20 text-white placeholder-gray-300″ /> setNp({…np, legion: e.target.value})} className=”px-4 py-2 rounded bg-white/20 text-white placeholder-gray-300″ />
{event.rounds.length === 0 && ( )}

Loyalists ({loys.length})

{loys.map(p => (
{p.name}
{p.legion} • {p.club}
W:{p.wins} D:{p.draws} L:{p.losses} • {p.points}pts • Score:{p.totalScore}
))}

Traitors ({tras.length})

{tras.map(p => (
{p.name}
{p.legion} • {p.club}
W:{p.wins} D:{p.draws} L:{p.losses} • {p.points}pts • Score:{p.totalScore}
))}
)} {event.rounds.map((round, ri) => { if (tab !== `round-${round.id}`) return null; const isLastRound = ri === event.rounds.length – 1; const allResultsDone = round.matches.every(m => m.result); const canGenNext = isLastRound && allResultsDone && event.rounds.length < 10; const pendingCount = round.matches.filter(m => !m.result).length; return (

Round {round.roundNumber}

{isLastRound && (
{!allResultsDone && ( {pendingCount} bord saknar resultat )} {event.rounds.length >= 10 && ( Max 10 rundor uppnått )}
)}
{round.matches.map((m, i) => (
Match {i + 1} • Bord {m.table}
{m.result && {m.result === ‘draw’ ? ‘Draw’ : `Winner: ${event.players.find(p => p.id === m.winner)?.name}`}} {m.result && ( )}
LOYALIST
{m.loyalist?.name}
{m.loyalist?.legion}
updScore(ri, m.id, ‘loyalist’, e.target.value)} disabled={m.result} className=”w-full px-2 py-1 rounded bg-white/20 text-white disabled:opacity-50″ />
TRAITOR
{m.traitor?.name}
{m.traitor?.legion}
updScore(ri, m.id, ‘traitor’, e.target.value)} disabled={m.result} className=”w-full px-2 py-1 rounded bg-white/20 text-white disabled:opacity-50″ />
))}
); })} {tab === ‘standings’ && (

Standings

{[…event.players].sort((a, b) => b.points – a.points || b.totalScore – a.totalScore).map((p, i) => ( ))}
Rank Player Faction W D L Pts Score
{i + 1} {p.name} {p.faction} {p.wins} {p.draws} {p.losses} {p.points} {p.totalScore || 0}
)} {tab === ‘factions’ && (() => { let loyPts = 0, traPts = 0; const roundRows = event.rounds.map(r => { const loyWins = r.matches.filter(m => m.result === ‘win’ && m.winner === m.loyalist?.id).length; const traWins = r.matches.filter(m => m.result === ‘win’ && m.winner === m.traitor?.id).length; let roundWinner = null; if (loyWins > traWins) { roundWinner = ‘Loyalist’; loyPts += 1; } else if (traWins > loyWins) { roundWinner = ‘Traitor’; traPts += 1; } else if (loyWins > 0 || traWins > 0) { roundWinner = ‘Tie’; } return { r, loyWins, traWins, roundWinner }; }); return (

Faction Standings

LOYALISTS
{loyPts}
rundpoäng
TRAITORS
{traPts}
rundpoäng
{event.rounds.length === 0 ? (

Inga rundor ännu.

) : ( {roundRows.map(({ r, loyWins, traWins, roundWinner }) => ( ))}
Runda Loyalist Wins Traitor Wins Vinnare
Round {r.roundNumber} {loyWins} {traWins} {roundWinner === ‘Loyalist’ && ⚔ Loyalist +1} {roundWinner === ‘Traitor’ && ⚔ Traitor +1} {roundWinner === ‘Tie’ && Oavgjort} {!roundWinner && Pågår…}
)}
); })()} {tab === ‘export’ && (() => { const [discordRi, setDiscordRi] = React.useState(null); const [copied, setCopied] = React.useState(false); const discordText = discordRi !== null ? expDiscord(discordRi) : ”; const copyToClipboard = () => { navigator.clipboard.writeText(discordText).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); }; return (

Export Rounds

{event.rounds.length === 0 ? (

Inga rounds ännu.

) : (
{event.rounds.map((r, i) => (
Round {r.roundNumber}
{r.timestamp}
{discordRi === i && (
Klistra in detta på Discord: