const { useState: useStateA, useEffect: useEffectA, useRef: useRefA } = React; // ── PIN Security ────────────────────────────────────────────── const PIN_HASH = "e68259a977d847c9ed26fc47c6855021c9a51eac28a63f9a0c012f7bbac0b556"; const MAX_ATTEMPTS = 3; const LOCKOUT_MS = 5 * 60 * 1000; async function hashPin(pin) { const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(pin)); return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2,"0")).join(""); } function getLockout() { try { return JSON.parse(localStorage.getItem("trix.lockout") || "null") || { attempts: 0, until: 0 }; } catch { return { attempts: 0, until: 0 }; } } function setLockout(d) { try { localStorage.setItem("trix.lockout", JSON.stringify(d)); } catch {} } function clearLockout() { try { localStorage.removeItem("trix.lockout"); } catch {} } function PinStep({ onSuccess }) { const [pin, setPin] = useStateA(""); const [error, setError] = useStateA(false); const [errMsg, setErrMsg] = useStateA(""); const [locked, setLocked] = useStateA(false); const [remaining, setRemaining] = useStateA(0); const timerRef = useRefA(null); useEffectA(() => { function tick() { const { until, attempts } = getLockout(); const now = Date.now(); if (until > now) { setLocked(true); setRemaining(Math.ceil((until - now) / 1000)); } else if (locked) { setLocked(false); setRemaining(0); if (attempts >= MAX_ATTEMPTS) setLockout({ attempts: 0, until: 0 }); } } tick(); timerRef.current = setInterval(tick, 1000); return () => clearInterval(timerRef.current); }, [locked]); async function pressKey(k) { if (locked) return; if (k === "del") { setPin(p => p.slice(0,-1)); return; } if (pin.length >= 4) return; const next = pin + k; setPin(next); if (next.length === 4) { const h = await hashPin(next); if (h === PIN_HASH) { clearLockout(); onSuccess(); } else { setError(true); setTimeout(() => { setError(false); setPin(""); }, 500); const state = getLockout(); const attempts = state.attempts + 1; if (attempts >= MAX_ATTEMPTS) { const until = Date.now() + LOCKOUT_MS; setLockout({ attempts, until }); setLocked(true); setErrMsg("تم الإغلاق · حاول بعد ٥ دقائق"); } else { setLockout({ attempts, until: 0 }); setErrMsg(`محاولة خاطئة · تبقى ${MAX_ATTEMPTS - attempts} محاولة`); } } } } const mins = String(Math.floor(remaining / 60)).padStart(2,"0"); const secs = String(remaining % 60).padStart(2,"0"); return (
صالة الإدارة
أدخل الرمز السري · للحكم فقط
{locked ? (
تم الإغلاق مؤقتاً
{mins}:{secs}
محاولات خاطئة كثيرة · انتظر
) : ( <>
{[0,1,2,3].map(i => (
i ? "filled" : ""} ${error ? "wrong" : ""}`}>{pin[i] ? "●" : ""}
))}
{errMsg &&
{errMsg}
}
{[1,2,3,4,5,6,7,8,9].map(n => ( ))}
)}
); } const PRESET_ICONS = ["♛","♠","♥","♦","♣","☠","⚡","🔥","👑","💀","😈","🎯","🃏","🎴","⚔️","🏆"]; const PRESET_COLORS = [ { name: "ذهبي", value: "#f5c542" }, { name: "أحمر", value: "#e63946" }, { name: "سماوي", value: "#22d3ee" }, { name: "وردي", value: "#ff4fa3" }, { name: "ليموني", value: "#d9f635" }, { name: "بنفسجي", value: "#a78bfa" }, { name: "برتقالي", value: "#ff8c42" }, { name: "أخضر", value: "#4ade80" }, { name: "مرجاني", value: "#fb7185" }, { name: "أبيض", value: "#f6efd9" }, ]; function AdminPage({ onSaved }) { const [authed, setAuthed] = useStateA(false); const [tab, setTab] = useStateA("match"); // match state const [game, setGame] = useStateA(null); const [teamA, setTeamA] = useStateA([]); const [teamB, setTeamB] = useStateA([]); const [active, setActive] = useStateA("A"); const [winner, setWinner] = useStateA(null); const [matchStep, setMatchStep] = useStateA("game"); const [saveError, setSaveError] = useStateA(""); // badge state const [bIcon, setBIcon] = useStateA(PRESET_ICONS[0]); const [bColor, setBColor] = useStateA(PRESET_COLORS[0].value); const [bName, setBName] = useStateA(""); const [bFlavor, setBFlavor] = useStateA(""); const [bPlayer, setBPlayer] = useStateA(null); const [bError, setBError] = useStateA(""); // player edit state const [editPlayer, setEditPlayer] = useStateA(null); const [editName, setEditName] = useStateA(""); const [editNick, setEditNick] = useStateA(""); const [editPalette, setEditPalette] = useStateA(0); const [pError, setPError] = useStateA(""); // match delete state const [confirmDelete, setConfirmDelete] = useStateA(null); const [deleteError, setDeleteError] = useStateA(""); if (!authed) return setAuthed(true)} />; // ── Match ── function togglePlayer(p) { const inA = teamA.includes(p.id), inB = teamB.includes(p.id); if (inA) { setTeamA(teamA.filter(id => id !== p.id)); return; } if (inB) { setTeamB(teamB.filter(id => id !== p.id)); return; } if (active === "A" && teamA.length < 2) { const next = [...teamA, p.id]; setTeamA(next); if (next.length === 2) setActive("B"); } else if (active === "B" && teamB.length < 2) { setTeamB([...teamB, p.id]); } } function removeFrom(team, id) { if (team === "A") setTeamA(t => t.filter(x => x !== id)); else setTeamB(t => t.filter(x => x !== id)); setActive(team); } function resetMatch() { setGame(null); setTeamA([]); setTeamB([]); setWinner(null); setActive("A"); setMatchStep("game"); setSaveError(""); } function saveMatch(w) { setWinner(w); setMatchStep("saved"); DB.saveMatch(game, teamA, teamB, w).catch(() => setSaveError("فشل الحفظ في الخادم")); } // ── Badges ── function addBadge() { setBError(""); if (!bName.trim()) return setBError("اكتب اسم الشارة"); if (!bPlayer) return setBError("اختر لاعباً"); const playerBadges = DB.badges.filter(b => b.playerIds.includes(bPlayer)); if (playerBadges.length >= 2) return setBError("هذا اللاعب عنده شارتين بالفعل"); const newBadge = { id: "b" + Date.now(), icon: bIcon, label: bName.trim(), color: bColor, flavor: bFlavor.trim() || "🃏", playerIds: [bPlayer] }; setBName(""); setBFlavor(""); setBPlayer(null); DB.saveBadges([...DB.badges, newBadge]).catch(() => setBError("فشل الحفظ في الخادم")); } function removeBadge(badgeId, playerId) { const updated = DB.badges.map(b => { if (b.id !== badgeId) return b; const ids = b.playerIds.filter(id => id !== playerId); return ids.length ? { ...b, playerIds: ids } : null; }).filter(Boolean); DB.saveBadges(updated).catch(() => {}); } // ── Players ── function startEdit(p) { setEditPlayer(p.id); setEditName(p.name); setEditNick(p.nick); setEditPalette(p.paletteIndex ?? DB.players.indexOf(p)); setPError(""); } function savePlayerEdit() { if (!editName.trim()) return setPError("اكتب الاسم"); if (!editNick.trim()) return setPError("اكتب الحرف"); const updated = DB.raw.players.map(p => p.id === editPlayer ? { ...p, name: editName.trim(), nick: editNick.trim()[0], paletteIndex: editPalette } : p ); setEditPlayer(null); DB.savePlayers(updated).catch(() => setPError("فشل الحفظ في الخادم")); } const players = DB.players; const badges = DB.badges; return (
{/* Tab switcher */}
{[ { id: "match", label: "مباراة ♠" }, { id: "badges", label: "شارات ♛" }, { id: "players", label: "لاعبون ♥" }, ].map(t => ( ))}
{/* ── MATCH TAB ── */} {tab === "match" && <> {matchStep === "game" && <>
١ اختر اللعبة
{/* Match history / delete */}
سجل المباريات · حذف
{deleteError &&
{deleteError}
} {DB.matches.length === 0 && (
لا توجد مباريات
)} {DB.matches.map(m => { const nameA = m.teamA.map(id => byId(id)?.name ?? "؟").join(" · "); const nameB = m.teamB.map(id => byId(id)?.name ?? "؟").join(" · "); const winnerLabel = m.winner === "A" ? nameA : nameB; const isConfirming = confirmDelete === m.id; return (
{m.game} {arabicDateLabel(m.date)}
{nameA} ضد {nameB}
فاز: {winnerLabel}
{isConfirming ? (
) : ( )}
); })}
} {matchStep === "teams" && <>
٢ اختر الفريقين
{["A","B"].map(team => { const arr = team === "A" ? teamA : teamB; const label = team === "A" ? "الفريق الأزرق" : "الفريق الوردي"; const color = team === "A" ? "var(--neon-cyan)" : "var(--neon-mag)"; return (
{label}
{[0,1].map(i => { const id = arr[i], p = id ? byId(id) : null; return (
{p ? <>{p.name} : لاعب {i+1}… }
); })}
); })}
اللاعبون المتاحون
{players.map(p => { const inA = teamA.includes(p.id), inB = teamB.includes(p.id), picked = inA || inB; return (
togglePlayer(p)}> {p.name} {inA && الأزرق} {inB && الوردي}
); })}
} {matchStep === "winner" && <>
٣ من كسبها؟
{saveError &&
{saveError}
}
{["A","B"].map(w => { const arr = w === "A" ? teamA : teamB; const label = w === "A" ? "الأزرق" : "الوردي"; const bg = w === "A" ? "linear-gradient(135deg,#22d3ee,#0891b2)" : "linear-gradient(135deg,#ff4fa3,#c026d3)"; return (
saveMatch(w)}>
{label}
{arr.map(id => byId(id)?.name).join(" · ")}
); })}
لعبة {game} · ٢ ضد ٢ · اضغط الفريق الفائز
} {matchStep === "saved" && (() => { const winArr = winner === "A" ? teamA : teamB; const winName = winner === "A" ? "الفريق الأزرق" : "الفريق الوردي"; return ( <>
تم التسجيل!
مبروك الفوز يا أبطال
{winName}
{winArr.map(id => byId(id)?.name).join(" · ")}
); })()} } {/* ── BADGES TAB ── */} {tab === "badges" && <>
إدارة الشارات
شارة جديدة
الأيقونة
{PRESET_ICONS.map(ic => ( ))}
اللون
{PRESET_COLORS.map(c => (
setBName(e.target.value)} style={{ marginBottom: 10, display: "block" }} /> setBFlavor(e.target.value)} style={{ marginBottom: 10, display: "block" }} />
اختر اللاعب
{players.map(p => { const count = badges.filter(b => b.playerIds.includes(p.id)).length; const full = count >= 2; return (
!full && setBPlayer(p.id)}> {p.name} {full && ممتلئ}
); })}
{bName.trim() && (
{bIcon}
{bName}
{bFlavor || "..."}
)} {bError &&
{bError}
}
الشارات الحالية
{players.map(p => { const pb = badges.filter(b => b.playerIds.includes(p.id)); if (!pb.length) return null; return (
{p.name}
{pb.map(b => (
{b.icon} {b.label}
))}
); })} {badges.filter(b => b.playerIds.length).length === 0 && (
لا توجد شارات بعد
)} } {/* ── PLAYERS TAB ── */} {tab === "players" && <>
إدارة اللاعبين
{players.map(p => (
{editPlayer === p.id ? (
{editNick || "؟"}
setEditName(e.target.value)} style={{ marginBottom: 8, display: "block" }} /> setEditNick(e.target.value.slice(0,1))} style={{ display: "block" }} />
اللون
{PALETTES.map((pal, i) => (
{pError &&
{pError}
}
) : (
{p.name}
{p.wins} فوز · {p.losses} خسارة
)}
))} }
); } Object.assign(window, { AdminPage });