// Full data layer — JSONBin backend const JSONBIN_KEY = "$2a$10$3yt0Qag7/dBSMpw.JxKlyuAUEJ5C9VoimIRourSn.qXYYaOcehi42"; const JSONBIN_HEADERS = { "Content-Type": "application/json", "X-Access-Key": JSONBIN_KEY, }; // ── Known groups (hardcoded; new groups created via UI use localStorage) ───── const GROUP_CONFIGS = { samawar: { slug: "samawar", binId: "69e795b6856a682189599665", name: "السماور", icon: "♠", color: "#f5c542", }, }; // Resolve binId for a slug: hardcoded → ?bid= URL param → localStorage function getGroupBinId(slug) { if (GROUP_CONFIGS[slug]) return GROUP_CONFIGS[slug].binId; const bid = new URLSearchParams(window.location.search).get("bid"); if (bid) { try { localStorage.setItem(`shilletna.${slug}`, bid); } catch {} return bid; } try { return localStorage.getItem(`shilletna.${slug}`); } catch { return null; } } // Create a new group bin on JSONBin and cache binId in localStorage async function createGroup(slug, name, icon) { const res = await fetch("https://api.jsonbin.io/v3/b", { method: "POST", headers: { ...JSONBIN_HEADERS, "X-Bin-Name": `shilletna-${slug}`, "X-Bin-Private": "true" }, body: JSON.stringify({ ...DEFAULT_DB }), }); if (!res.ok) throw new Error("فشل إنشاء المجموعة"); const json = await res.json(); const binId = json.metadata.id; try { localStorage.setItem(`shilletna.${slug}`, binId); } catch {} return binId; } // ── Palettes ───────────────────────────────────────────────── const PALETTES = [ { bg: "#22d3ee", glow: "#22d3ee" }, { bg: "#ff4fa3", glow: "#ff4fa3" }, { bg: "#d9f635", glow: "#d9f635" }, { bg: "#ff8c42", glow: "#ff8c42" }, { bg: "#a78bfa", glow: "#a78bfa" }, { bg: "#f5c542", glow: "#f5c542" }, { bg: "#fb7185", glow: "#fb7185" }, { bg: "#4ade80", glow: "#4ade80" }, ]; // ── Default DB structure (first run) ───────────────────────── const DEFAULT_DB = { players: [ { id: 1, name: "لاعب ١", nick: "١", paletteIndex: 0 }, { id: 2, name: "لاعب ٢", nick: "٢", paletteIndex: 1 }, { id: 3, name: "لاعب ٣", nick: "٣", paletteIndex: 2 }, { id: 4, name: "لاعب ٤", nick: "٤", paletteIndex: 3 }, { id: 5, name: "لاعب ٥", nick: "٥", paletteIndex: 4 }, { id: 6, name: "لاعب ٦", nick: "٦", paletteIndex: 5 }, { id: 7, name: "لاعب ٧", nick: "٧", paletteIndex: 6 }, { id: 8, name: "لاعب ٨", nick: "٨", paletteIndex: 7 }, ], matches: [], badges: [], }; function migrateDB(record) { if (!record.players) return record; const fixed = record.players.map((p, i) => { const arabicNums = ["١","٢","٣","٤","٥","٦","٧","٨"]; const needsFix = /لاعب \d/.test(p.name) || !p.nick || p.nick.trim() === ""; return needsFix ? { ...p, name: `لاعب ${arabicNums[i] || (i+1)}`, nick: arabicNums[i] || String(i+1), } : p; }); return { ...record, players: fixed }; } // ── API calls (use DB.binId set by load) ────────────────────── async function dbRead() { const url = `https://api.jsonbin.io/v3/b/${DB.binId}/latest`; const res = await fetch(url, { headers: JSONBIN_HEADERS }); if (!res.ok) throw new Error("فشل تحميل البيانات"); const json = await res.json(); return json.record; } async function dbWrite(data) { const url = `https://api.jsonbin.io/v3/b/${DB.binId}`; const res = await fetch(url, { method: "PUT", headers: JSONBIN_HEADERS, body: JSON.stringify(data), }); if (!res.ok) throw new Error("فشل حفظ البيانات"); return res.json(); } // ── Computed stats from match history ──────────────────────── function computeStats(players, matches) { const stats = {}; players.forEach(p => { stats[p.id] = { overall: { wins: 0, losses: 0 }, tarneeb: { wins: 0, losses: 0 }, trix: { wins: 0, losses: 0 }, partnerships: {}, }; players.forEach(other => { if (other.id !== p.id) stats[p.id].partnerships[other.id] = { wins: 0, losses: 0 }; }); }); matches.forEach(m => { const winners = m.winner === "A" ? m.teamA : m.teamB; const losers = m.winner === "A" ? m.teamB : m.teamA; const gameKey = m.game === "طرنيب" ? "tarneeb" : "trix"; winners.forEach(pid => { stats[pid].overall.wins++; stats[pid][gameKey].wins++; const partner = winners.find(x => x !== pid); if (partner) stats[pid].partnerships[partner].wins++; }); losers.forEach(pid => { stats[pid].overall.losses++; stats[pid][gameKey].losses++; const partner = losers.find(x => x !== pid); if (partner) stats[pid].partnerships[partner].losses++; }); }); return stats; } function enrichPlayers(players, stats) { return players.map((p, i) => { const s = stats[p.id]; const ps = s.partnerships; const palette = PALETTES[p.paletteIndex ?? i] || PALETTES[i % PALETTES.length]; const bestId = Object.keys(ps).sort((a, b) => ps[b].wins - ps[a].wins)[0]; const worstId = Object.keys(ps).sort((a, b) => ps[b].losses - ps[a].losses)[0]; return { ...p, palette, wins: s.overall.wins, losses: s.overall.losses, tarneeb: s.tarneeb.wins, trix: s.trix.wins, tarneebLosses: s.tarneeb.losses, trixLosses: s.trix.losses, bestPartner: Number(bestId), worstPartner: Number(worstId), partnerStats: ps, }; }); } function winRate(p) { const total = p.wins + p.losses; return total ? Math.round((p.wins / total) * 100) : 0; } function rankLabel(i) { if (i === 0) return "الأول"; if (i === 1) return "الثاني"; if (i === 2) return "الثالث"; return `#${i + 1}`; } function arabicDateLabel(dateInput) { const date = new Date(dateInput); const now = new Date(); const diffMs = now - date; const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays === 0) return "اليوم"; if (diffDays === 1) return "أمس"; if (diffDays <= 6) { const days = ["الأحد","الإثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"]; return days[date.getDay()]; } if (diffDays <= 13) return "منذ أسبوع"; if (diffDays <= 20) return "منذ أسبوعين"; return "منذ شهر"; } // ── Global reactive store ───────────────────────────────────── const DB = { binId: null, raw: null, players: [], matches: [], badges: [], loading: true, // start true to prevent empty-data flash error: null, listeners: [], notify() { this.listeners.forEach(fn => fn()); }, subscribe(fn) { this.listeners.push(fn); return () => { this.listeners = this.listeners.filter(l => l !== fn); }; }, async load(slug) { this.loading = true; this.error = null; this.notify(); const binId = getGroupBinId(slug); if (!binId) { this.error = "not-found"; this.loading = false; this.notify(); return; } this.binId = binId; try { let record = await dbRead(); if (!record.players || record.players.length === 0) { record = DEFAULT_DB; await dbWrite(record); } record = migrateDB(record); this._apply(record); } catch (e) { this.error = e.message; } this.loading = false; this.notify(); }, _apply(record) { this.raw = record; const stats = computeStats(record.players, record.matches || []); this.players = enrichPlayers(record.players, stats); this.matches = [...(record.matches || [])].reverse(); this.badges = record.badges || []; window.PLAYERS = this.players; window.MATCHES = this.matches; window.BADGES = this.badges; this.notify(); }, async saveMatch(game, teamA, teamB, winner) { const match = { id: Date.now(), game, teamA, teamB, winner, date: new Date().toISOString() }; const record = { ...this.raw, matches: [...(this.raw.matches || []), match] }; this._apply(record); await dbWrite(record); }, async deleteMatch(matchId) { const record = { ...this.raw, matches: (this.raw.matches || []).filter(m => m.id !== matchId) }; this._apply(record); await dbWrite(record); }, async saveBadges(badges) { const record = { ...this.raw, badges }; this._apply(record); await dbWrite(record); }, async savePlayers(players) { const record = { ...this.raw, players }; this._apply(record); await dbWrite(record); }, }; function byId(id) { return DB.players.find(p => p.id === id); } function badgesFor(pid) { return DB.badges.filter(b => b.playerIds.includes(pid)); } Object.assign(window, { PALETTES, DB, byId, badgesFor, winRate, rankLabel, arabicDateLabel, GROUP_CONFIGS, getGroupBinId, createGroup, PLAYERS: DB.players, MATCHES: DB.matches, BADGES: DB.badges, });