import { useState, useEffect, useCallback } from "react"; const PLAYERS = [ "Joueur 1", "Joueur 2", "Joueur 3", "Joueur 4", "Joueur 5", "Joueur 6", "Joueur 7", "Joueur 8", "Joueur 9", "Joueur 10", "Joueur 11" ]; // All 104 matches - group stage from Al Jazeera (June 11, 2026), knockout TBD const ALL_MATCHES = [ // --- PHASE DE GROUPES --- (heures françaises CEST = GMT+2) { id: 101, phase: "Groupes", match: "Mexique vs Afrique du Sud (Groupe A)", date: "11 juin", heure: "21h00" }, { id: 102, phase: "Groupes", match: "Corée du Sud vs Tchéquie (Groupe A)", date: "12 juin", heure: "04h00" }, { id: 103, phase: "Groupes", match: "Canada vs Bosnie-Herzégovine (Groupe B)", date: "12 juin", heure: "21h00" }, { id: 104, phase: "Groupes", match: "États-Unis vs Paraguay (Groupe D)", date: "13 juin", heure: "03h00" }, { id: 105, phase: "Groupes", match: "Qatar vs Suisse (Groupe B)", date: "13 juin", heure: "21h00" }, { id: 106, phase: "Groupes", match: "Brésil vs Maroc (Groupe C)", date: "14 juin", heure: "00h00" }, { id: 107, phase: "Groupes", match: "Haïti vs Écosse (Groupe C)", date: "14 juin", heure: "03h00" }, { id: 108, phase: "Groupes", match: "Australie vs Turquie (Groupe D)", date: "14 juin", heure: "06h00" }, { id: 109, phase: "Groupes", match: "Allemagne vs Curaçao (Groupe E)", date: "14 juin", heure: "19h00" }, { id: 110, phase: "Groupes", match: "Pays-Bas vs Japon (Groupe F)", date: "14 juin", heure: "22h00" }, { id: 111, phase: "Groupes", match: "Côte d'Ivoire vs Équateur (Groupe E)", date: "15 juin", heure: "01h00" }, { id: 112, phase: "Groupes", match: "Suède vs Tunisie (Groupe F)", date: "15 juin", heure: "04h00" }, { id: 113, phase: "Groupes", match: "Espagne vs Cap-Vert (Groupe H)", date: "15 juin", heure: "18h00" }, { id: 114, phase: "Groupes", match: "Belgique vs Égypte (Groupe G)", date: "15 juin", heure: "21h00" }, { id: 115, phase: "Groupes", match: "Arabie Saoudite vs Uruguay (Groupe H)", date: "16 juin", heure: "00h00" }, { id: 116, phase: "Groupes", match: "Iran vs Nouvelle-Zélande (Groupe G)", date: "16 juin", heure: "03h00" }, { id: 117, phase: "Groupes", match: "France vs Sénégal (Groupe I)", date: "16 juin", heure: "21h00" }, { id: 118, phase: "Groupes", match: "Irak vs Norvège (Groupe I)", date: "17 juin", heure: "00h00" }, { id: 119, phase: "Groupes", match: "Argentine vs Algérie (Groupe J)", date: "17 juin", heure: "03h00" }, { id: 120, phase: "Groupes", match: "Autriche vs Jordanie (Groupe J)", date: "17 juin", heure: "06h00" }, { id: 121, phase: "Groupes", match: "Portugal vs RD Congo (Groupe K)", date: "17 juin", heure: "19h00" }, { id: 122, phase: "Groupes", match: "Angleterre vs Croatie (Groupe L)", date: "17 juin", heure: "22h00" }, { id: 123, phase: "Groupes", match: "Ghana vs Panama (Groupe L)", date: "18 juin", heure: "01h00" }, { id: 124, phase: "Groupes", match: "Ouzbékistan vs Colombie (Groupe K)", date: "18 juin", heure: "04h00" }, { id: 125, phase: "Groupes", match: "Tchéquie vs Afrique du Sud (Groupe A)", date: "18 juin", heure: "18h00" }, { id: 126, phase: "Groupes", match: "Suisse vs Bosnie-Herzégovine (Groupe B)", date: "18 juin", heure: "21h00" }, { id: 127, phase: "Groupes", match: "Canada vs Qatar (Groupe B)", date: "19 juin", heure: "00h00" }, { id: 128, phase: "Groupes", match: "Mexique vs Corée du Sud (Groupe A)", date: "19 juin", heure: "03h00" }, { id: 129, phase: "Groupes", match: "Écosse vs Maroc (Groupe C)", date: "20 juin", heure: "00h00" }, { id: 130, phase: "Groupes", match: "États-Unis vs Australie (Groupe D)", date: "19 juin", heure: "21h00" }, { id: 131, phase: "Groupes", match: "Brésil vs Haïti (Groupe C)", date: "20 juin", heure: "02h30" }, { id: 132, phase: "Groupes", match: "Turquie vs Paraguay (Groupe D)", date: "20 juin", heure: "05h00" }, { id: 133, phase: "Groupes", match: "Pays-Bas vs Suède (Groupe F)", date: "20 juin", heure: "19h00" }, { id: 134, phase: "Groupes", match: "Allemagne vs Côte d'Ivoire (Groupe E)", date: "20 juin", heure: "22h00" }, { id: 135, phase: "Groupes", match: "Équateur vs Curaçao (Groupe E)", date: "21 juin", heure: "05h00" }, { id: 136, phase: "Groupes", match: "Tunisie vs Japon (Groupe F)", date: "21 juin", heure: "06h00" }, { id: 137, phase: "Groupes", match: "Espagne vs Arabie Saoudite (Groupe H)", date: "21 juin", heure: "18h00" }, { id: 138, phase: "Groupes", match: "Belgique vs Iran (Groupe G)", date: "21 juin", heure: "21h00" }, { id: 139, phase: "Groupes", match: "Uruguay vs Cap-Vert (Groupe H)", date: "22 juin", heure: "00h00" }, { id: 140, phase: "Groupes", match: "Nouvelle-Zélande vs Égypte (Groupe G)", date: "22 juin", heure: "03h00" }, { id: 141, phase: "Groupes", match: "Argentine vs Autriche (Groupe J)", date: "22 juin", heure: "19h00" }, { id: 142, phase: "Groupes", match: "France vs Irak (Groupe I)", date: "22 juin", heure: "23h00" }, { id: 143, phase: "Groupes", match: "Norvège vs Sénégal (Groupe I)", date: "23 juin", heure: "02h00" }, { id: 144, phase: "Groupes", match: "Jordanie vs Algérie (Groupe J)", date: "23 juin", heure: "05h00" }, { id: 145, phase: "Groupes", match: "Portugal vs Ouzbékistan (Groupe K)", date: "23 juin", heure: "19h00" }, { id: 146, phase: "Groupes", match: "Angleterre vs Ghana (Groupe L)", date: "23 juin", heure: "22h00" }, { id: 147, phase: "Groupes", match: "Panama vs Croatie (Groupe L)", date: "24 juin", heure: "01h00" }, { id: 148, phase: "Groupes", match: "Colombie vs RD Congo (Groupe K)", date: "24 juin", heure: "04h00" }, { id: 149, phase: "Groupes", match: "Suisse vs Canada (Groupe B)", date: "24 juin", heure: "21h00" }, { id: 150, phase: "Groupes", match: "Bosnie-Herzégovine vs Qatar (Groupe B)", date: "24 juin", heure: "21h00" }, { id: 151, phase: "Groupes", match: "Écosse vs Brésil (Groupe C)", date: "25 juin", heure: "00h00" }, { id: 152, phase: "Groupes", match: "Maroc vs Haïti (Groupe C)", date: "25 juin", heure: "00h00" }, { id: 153, phase: "Groupes", match: "Tchéquie vs Mexique (Groupe A)", date: "25 juin", heure: "03h00" }, { id: 154, phase: "Groupes", match: "Afrique du Sud vs Corée du Sud (Groupe A)", date: "25 juin", heure: "03h00" }, { id: 155, phase: "Groupes", match: "Équateur vs Allemagne (Groupe E)", date: "25 juin", heure: "22h00" }, { id: 156, phase: "Groupes", match: "Curaçao vs Côte d'Ivoire (Groupe E)", date: "25 juin", heure: "22h00" }, { id: 157, phase: "Groupes", match: "Japon vs Suède (Groupe F)", date: "26 juin", heure: "01h00" }, { id: 158, phase: "Groupes", match: "Tunisie vs Pays-Bas (Groupe F)", date: "26 juin", heure: "01h00" }, { id: 159, phase: "Groupes", match: "Turquie vs États-Unis (Groupe D)", date: "26 juin", heure: "04h00" }, { id: 160, phase: "Groupes", match: "Paraguay vs Australie (Groupe D)", date: "26 juin", heure: "04h00" }, { id: 161, phase: "Groupes", match: "Norvège vs France (Groupe I)", date: "26 juin", heure: "21h00" }, { id: 162, phase: "Groupes", match: "Sénégal vs Irak (Groupe I)", date: "26 juin", heure: "21h00" }, { id: 163, phase: "Groupes", match: "Cap-Vert vs Arabie Saoudite (Groupe H)", date: "27 juin", heure: "02h00" }, { id: 164, phase: "Groupes", match: "Uruguay vs Espagne (Groupe H)", date: "27 juin", heure: "02h00" }, { id: 165, phase: "Groupes", match: "Égypte vs Iran (Groupe G)", date: "27 juin", heure: "05h00" }, { id: 166, phase: "Groupes", match: "Nouvelle-Zélande vs Belgique (Groupe G)", date: "27 juin", heure: "05h00" }, { id: 167, phase: "Groupes", match: "Panama vs Angleterre (Groupe L)", date: "27 juin", heure: "23h00" }, { id: 168, phase: "Groupes", match: "Croatie vs Ghana (Groupe L)", date: "27 juin", heure: "23h00" }, { id: 169, phase: "Groupes", match: "Colombie vs Portugal (Groupe K)", date: "28 juin", heure: "01h30" }, { id: 170, phase: "Groupes", match: "RD Congo vs Ouzbékistan (Groupe K)", date: "28 juin", heure: "01h30" }, { id: 171, phase: "Groupes", match: "Algérie vs Autriche (Groupe J)", date: "28 juin", heure: "04h00" }, { id: 172, phase: "Groupes", match: "Jordanie vs Argentine (Groupe J)", date: "28 juin", heure: "04h00" }, // --- 1/8 DE FINALE --- { id: 201, phase: "1/16 de finale", match: "1er Gr. A vs 2e Gr. B", date: "28 juin", heure: "21h00" }, { id: 202, phase: "1/16 de finale", match: "1er Gr. C vs 2e Gr. F", date: "29 juin", heure: "21h00" }, { id: 203, phase: "1/16 de finale", match: "1er Gr. E vs 3e (A/B/C/D/F)", date: "29 juin", heure: "22h30" }, { id: 204, phase: "1/16 de finale", match: "1er Gr. F vs 2e Gr. C", date: "30 juin", heure: "03h00" }, { id: 205, phase: "1/16 de finale", match: "2e Gr. E vs 2e Gr. I", date: "30 juin", heure: "19h00" }, { id: 206, phase: "1/16 de finale", match: "1er Gr. I vs 3e (C/D/F/G/H)", date: "30 juin", heure: "23h00" }, { id: 207, phase: "1/16 de finale", match: "1er Gr. A vs 3e (C/E/F/H/I)", date: "1 juil", heure: "03h00" }, { id: 208, phase: "1/16 de finale", match: "1er Gr. L vs 3e (E/H/I/J/K)", date: "1 juil", heure: "18h00" }, { id: 209, phase: "1/16 de finale", match: "1er Gr. D vs 2e Gr. K", date: "1 juil", heure: "22h00" }, { id: 210, phase: "1/16 de finale", match: "2e Gr. D vs 2e Gr. G", date: "2 juil", heure: "00h00" }, { id: 211, phase: "1/16 de finale", match: "1er Gr. G vs 2e Gr. L", date: "2 juil", heure: "21h00" }, { id: 212, phase: "1/16 de finale", match: "1er Gr. K vs 3e (A/B/D/G/L)", date: "3 juil", heure: "00h00" }, { id: 213, phase: "1/16 de finale", match: "1er Gr. B vs 2e Gr. A", date: "3 juil", heure: "05h00" }, { id: 214, phase: "1/16 de finale", match: "1er Gr. J vs 2e Gr. H", date: "3 juil", heure: "20h00" }, { id: 215, phase: "1/16 de finale", match: "2e Gr. J vs 2e Gr. K", date: "4 juil", heure: "00h00" }, { id: 216, phase: "1/16 de finale", match: "1er Gr. H vs 3e (B/C/D/J/L)", date: "4 juil", heure: "03h30" }, // --- 1/4 DE FINALE --- { id: 301, phase: "1/8 de finale", match: "W201 vs W208", date: "4 juil", heure: "19h00" }, { id: 302, phase: "1/8 de finale", match: "W202 vs W210", date: "4 juil", heure: "23h00" }, { id: 303, phase: "1/8 de finale", match: "W204 vs W203", date: "5 juil", heure: "22h00" }, { id: 304, phase: "1/8 de finale", match: "W207 vs W212", date: "6 juil", heure: "02h00" }, { id: 305, phase: "1/8 de finale", match: "W209 vs W206", date: "6 juil", heure: "21h00" }, { id: 306, phase: "1/8 de finale", match: "W211 vs W205", date: "7 juil", heure: "02h00" }, { id: 307, phase: "1/8 de finale", match: "W213 vs W216", date: "7 juil", heure: "18h00" }, { id: 308, phase: "1/8 de finale", match: "W214 vs W215", date: "7 juil", heure: "22h00" }, // --- 1/2 FINALE --- { id: 401, phase: "1/4 de finale", match: "W301 vs W302", date: "9 juil", heure: "21h00" }, { id: 402, phase: "1/4 de finale", match: "W303 vs W304", date: "10 juil", heure: "21h00" }, { id: 403, phase: "1/4 de finale", match: "W305 vs W306", date: "11 juil", heure: "22h00" }, { id: 404, phase: "1/4 de finale", match: "W307 vs W308", date: "14 juil", heure: "03h00" }, // --- DEMI-FINALES --- { id: 501, phase: "Demi-finale", match: "W401 vs W402", date: "14 juil", heure: "21h00" }, { id: 502, phase: "Demi-finale", match: "W403 vs W404", date: "15 juil", heure: "21h00" }, // --- 3e PLACE --- { id: 601, phase: "3e place", match: "Perdant DF1 vs Perdant DF2", date: "18 juil", heure: "23h00" }, // --- FINALE --- { id: 701, phase: "Finale", match: "Finale — Vainqueur DF1 vs Vainqueur DF2", date: "19 juil", heure: "21h00" }, ]; const REGLEMENT = [ { titre: "Objet", contenu: "Ce concours de pronostics est organisé entre les 11 marins du CENTEX HLO, sans enjeu financier, pour le plaisir et la convivialité autour de la Coupe du monde FIFA 2026." }, { titre: "Matchs concernés", contenu: "Tous les matchs de la compétition sont inclus : phase de groupes (72 matchs), 1/16 de finale (16 matchs), 1/8 de finale (8 matchs), demi-finales (4 matchs), match pour la 3e place et finale — soit 104 matchs au total." }, { titre: "Dépôt des pronostics", contenu: "Chaque participant doit déposer son pronostic (score exact) au plus tard 1 heure avant le coup d'envoi du match. Passé ce délai, la saisie est automatiquement verrouillée et le pronostic est considéré comme non soumis pour ce match." }, { titre: "Système de points", contenu: "• Score exact : +3 points\n• Bonne issue (victoire / nul / défaite correcte, score faux) : +1 point\n• Mauvaise issue : 0 point\n\nEn phase à élimination directe, les prolongations et tirs au but comptent dans le résultat final (ex. si l'équipe A gagne aux t.a.b., le résultat est une victoire de A)." }, { titre: "Matchs nuls (phase de groupes)", contenu: "Le nul est possible en phase de groupes. Pronostiquer 1-1 alors que le match finit 2-2 rapporte +1 point (bonne issue). Pronostiquer 1-1 et que le score est exactement 1-1 rapporte +3 points." }, { titre: "Départage en cas d'égalité", contenu: "En cas d'égalité sur le total de points (y compris pour les places 1, 2 et 3) :\n1. Pronostic vainqueur correct (+10 pts) — avantage au participant ayant trouvé le bon champion\n2. Nombre de scores exacts (+3 pts)\n3. Nombre de bonnes issues (+1 pt)\n\nEn cas d'égalité parfaite sur tous ces critères : partage de la place concernée." }, { titre: "Pronostic vainqueur", contenu: "Chaque participant soumet un pronostic sur le vainqueur final de la compétition avant le début des 1/16 de finale.\n\n• Pronostic correct (équipe championne) : +10 points\n• Pronostic incorrect : 0 point\n\nCe bonus est ajouté au total de points en fin de compétition, après la finale.\n\nLa saisie est verrouillée automatiquement 1 heure avant le coup d'envoi du premier match des 1/16 de finale (le 28 juin 2026 à 20h00, heure française)." }, { titre: "Résultats & classement", contenu: "Le classement est mis à jour après chaque journée de matchs. Le/la vainqueur(e) est annoncé(e) après la finale." }, { titre: "Saisie des pronostics & confiance", contenu: "L'application est accessible via un lien partagé, sans authentification individuelle. Chaque participant est responsable de la saisie de ses propres pronostics.\n\nDeux modes de fonctionnement sont possibles :\n• Mode autonome : chaque participant saisit lui-même ses pronostics depuis son appareil, dans les délais impartis.\n• Mode centralisé : un référent de l'équipe saisit les pronostics communiqués par chaque participant (par messagerie, oral, etc.) avant la clôture.\n\nDans tous les cas, la saisie repose sur la confiance mutuelle entre les participants. Toute modification du pronostic d'un autre participant est contraire à l'esprit du concours." } ]; const phaseColors = { "Groupes": "#1a6b3a", "1/16 de finale": "#1a4a8a", "1/8 de finale": "#6b1a6b", "1/4 de finale": "#8a4a00", "Demi-finale": "#6b3a00", "3e place": "#5a5a00", "Finale": "#8a1a1a", }; const phaseOrder = ["Groupes", "1/16 de finale", "1/8 de finale", "1/4 de finale", "Demi-finale", "3e place", "Finale"]; async function storageGet(key) { try { const result = await window.storage.get(key, true); return result ? JSON.parse(result.value) : null; } catch { return null; } } async function storageSet(key, value) { try { await window.storage.set(key, JSON.stringify(value), true); } catch (e) { console.error("Storage error:", e); } } function ScoreInput({ value, onChange }) { return ( onChange(e.target.value)} style={{ width: 40, textAlign: "center", border: "none", borderBottom: "2px solid #c8a94a", background: "transparent", fontSize: 17, fontWeight: 700, color: "#e8c96a", outline: "none", padding: "2px 2px" }} /> ); } function calcPoints(prono, result) { if (!prono || prono.h === "" || prono.a === "" || !result || result.h === "" || result.a === "") return null; const ph = parseInt(prono.h), pa = parseInt(prono.a); const rh = parseInt(result.h), ra = parseInt(result.a); if (ph === rh && pa === ra) return 3; const pi = ph > pa ? "H" : ph < pa ? "A" : "N"; const ri = rh > ra ? "H" : rh < ra ? "A" : "N"; return pi === ri ? 1 : 0; } // Convert a match date+heure (French CEST) to a JS Date object // date e.g. "16 juin", heure e.g. "21h00" const MONTH_MAP = { "juin": 5, "juil": 6 // JS months 0-indexed }; function matchToDate(dateStr, heureStr) { if (!dateStr || !heureStr) return null; const [day, monthStr] = dateStr.trim().split(" "); const month = MONTH_MAP[monthStr]; if (month === undefined) return null; const [h, m] = heureStr.replace("h", ":").split(":").map(Number); // All matches are in 2026, France is CEST (UTC+2) during the tournament // We create the date in UTC then subtract 2h to get UTC equivalent return new Date(Date.UTC(2026, month, parseInt(day), h - 2, m || 0)); } // Returns true if the match is locked (within 1h of kickoff or past it) function isMatchLocked(dateStr, heureStr) { const kickoff = matchToDate(dateStr, heureStr); if (!kickoff) return false; const now = new Date(); return now >= new Date(kickoff.getTime() - 60 * 60 * 1000); // H-1 } // First 1/16 match kickoff — used to lock winner prono const FIRST_KNOCKOUT_MATCH = ALL_MATCHES.find(m => m.phase === "1/16 de finale"); function isWinnerPronoLocked() { if (!FIRST_KNOCKOUT_MATCH) return false; return isMatchLocked(FIRST_KNOCKOUT_MATCH.date, FIRST_KNOCKOUT_MATCH.heure); } export default function App() { const [tab, setTab] = useState("classement"); const [loaded, setLoaded] = useState(false); const [saveStatus, setSaveStatus] = useState("idle"); const [playerNames, setPlayerNames] = useState(PLAYERS); const [editingNames, setEditingNames] = useState(false); const [results, setResults] = useState({}); const [pronos, setPronos] = useState({}); const [winnerPronos, setWinnerPronos] = useState({}); const [tournamentWinner, setTournamentWinner] = useState(""); const [filterPhase, setFilterPhase] = useState("Groupes"); const [searchTerm, setSearchTerm] = useState(""); const [matchNames, setMatchNames] = useState({}); const [editingMatchId, setEditingMatchId] = useState(null); const [editingMatchVal, setEditingMatchVal] = useState(""); const [isAdmin, setIsAdmin] = useState(false); const [showPinModal, setShowPinModal] = useState(false); const [pinInput, setPinInput] = useState(""); const [pinError, setPinError] = useState(false); const [pendingTab, setPendingTab] = useState(null); const CORRECT_PIN = "83390"; useEffect(() => { async function load() { const [p, r, pr, wp, mn] = await Promise.all([ storageGet("cdm2026:players"), storageGet("cdm2026:results"), storageGet("cdm2026:pronos"), storageGet("cdm2026:winners"), storageGet("cdm2026:matchnames"), ]); if (p) setPlayerNames(p); if (r) setResults(r); if (pr) setPronos(pr); if (wp) setWinnerPronos(wp); if (mn) setMatchNames(mn); const tw = await storageGet("cdm2026:winner"); if (tw) setTournamentWinner(tw); setLoaded(true); } load(); }, []); const save = useCallback(async (players, res, pro, winners, mn, tw) => { setSaveStatus("saving"); try { await Promise.all([ storageSet("cdm2026:players", players), storageSet("cdm2026:results", res), storageSet("cdm2026:pronos", pro), storageSet("cdm2026:winners", winners), storageSet("cdm2026:matchnames", mn), storageSet("cdm2026:winner", tw), ]); setSaveStatus("saved"); setTimeout(() => setSaveStatus("idle"), 2000); } catch { setSaveStatus("error"); } }, []); useEffect(() => { if (!loaded) return; const t = setTimeout(() => save(playerNames, results, pronos, winnerPronos, matchNames, tournamentWinner), 600); return () => clearTimeout(t); }, [playerNames, results, pronos, winnerPronos, matchNames, tournamentWinner, loaded, save]); function setResult(matchId, side, val) { setResults(r => ({ ...r, [matchId]: { ...(r[matchId] || {}), [side]: val } })); } function setProno(matchId, player, side, val) { setPronos(p => ({ ...p, [matchId]: { ...(p[matchId] || {}), [player]: { ...(p[matchId]?.[player] || {}), [side]: val } } })); } function getScore(player) { let total = 0, exact = 0, bonneIssue = 0; ALL_MATCHES.forEach(m => { const r = results[m.id], p = pronos[m.id]?.[player]; if (!r || !p) return; const pts = calcPoints(p, r); if (pts === null) return; total += pts; if (pts === 3) exact++; if (pts === 1) bonneIssue++; }); // +10 bonus if player correctly predicted the tournament winner const winnerBonus = (tournamentWinner && winnerPronos[player] && winnerPronos[player].trim().toLowerCase() === tournamentWinner.trim().toLowerCase()) ? 10 : 0; return { total: total + winnerBonus, exact, bonneIssue, winnerBonus }; } const ranking = playerNames.map((name, i) => ({ name, idx: i, ...getScore(name) })) .sort((a, b) => b.total - a.total || (b.winnerBonus > 0 ? 1 : 0) - (a.winnerBonus > 0 ? 1 : 0) || b.exact - a.exact || b.bonneIssue - a.bonneIssue ); const saveIndicator = { saving: { color: "#c8a94a", text: "💾 Sauvegarde…" }, saved: { color: "#4ade80", text: "✓ Sauvegardé" }, error: { color: "#f87171", text: "⚠ Erreur de sauvegarde" }, idle: { color: "#4a6a8a", text: "Données persistantes · 104 matchs" }, }[saveStatus]; const sortedPhases = phaseOrder.filter(p => ALL_MATCHES.some(m => m.phase === p)); const filteredMatches = ALL_MATCHES.filter(m => { if (m.phase !== filterPhase) return false; if (searchTerm && !m.match.toLowerCase().includes(searchTerm.toLowerCase()) && !m.date.includes(searchTerm)) return false; return true; }); const matchesByDate = filteredMatches.reduce((acc, m) => { acc[m.date] = acc[m.date] || []; acc[m.date].push(m); return acc; }, {}); // Sort matches within each day by heure Object.values(matchesByDate).forEach(arr => arr.sort((a, b) => (a.heure || "").localeCompare(b.heure || ""))); if (!loaded) return (
⚽ Chargement des données…
); const inputStyle = { background: "#0d1b2a", border: "1px solid #1e3a5a", borderRadius: 6, color: "#e8e0d0", padding: "8px 10px", fontSize: 13, outline: "none" }; return (
{/* Header */}
{/* Ligne 1 : Logo Aéronautique Navale */}
Insigne Aéronautique Navale
{/* Ligne 2 : PRONOS DU CENTEX HÉLICO */}
Pronos du Centex Hélico
{/* Ligne 3 : Trophée + Ballon */}
Trophée FIFA 2026 Ballon de foot
{/* Ligne 4 : Titre */}

Coupe du Monde FIFA 2026

{/* Ligne 5 : Drapeaux */}
Canada États-Unis Mexique
11 participants · 104 matchs · Score exact +3 · Bonne issue +1 · Pronostic vainqueur +10
Horaires en heure française (CEST — GMT+2)
{saveIndicator.text}
{/* Nav */}
{[ { key: "classement", label: "🏆 Classement" }, { key: "pronos", label: "📝 Pronostics" }, { key: "resultats", label: "✅ Résultats" }, { key: "reglement", label: "📋 Règlement" }, { key: "config", label: "⚙️ Config" }, ].map(t => ( ))}
{isAdmin ? ( ) : ( )}
{/* CLASSEMENT */} {tab === "classement" && (

Classement général

{ranking.map((p, i) => ( ))}
Pos. Participant Pts 🎯 Exact ✓ Issue 🏆 Bonus
{i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : `${i + 1}`} {p.name} {winnerPronos[p.name] && ( ({winnerPronos[p.name]}{p.winnerBonus ? " ✓" : ""}) )} {p.total} {p.exact} {p.bonneIssue} {p.winnerBonus ? "+10 ✓" : "–"}
Départage : 1) Pronostic vainqueur correct · 2) Scores exacts · 3) Bonnes issues
)} {/* PRONOSTICS */} {tab === "pronos" && (

Saisie des pronostics

{/* Pronostic vainqueur */} {(() => { const winnerLocked = isWinnerPronoLocked(); return (
🏆 Pronostic vainqueur final {winnerLocked ? FERMÉ : — ferme 1h avant le 1er match des 1/16 ({FIRST_KNOCKOUT_MATCH?.date}, {FIRST_KNOCKOUT_MATCH?.heure}) }
{playerNames.map((name, i) => (
{winnerLocked ?
{winnerPronos[name] || non saisi}
: setWinnerPronos(w => ({ ...w, [name]: e.target.value }))} placeholder="ex: France" style={{ ...inputStyle, padding: "5px 8px" }} /> }
))}
); })()} {/* Phase filter */}
{sortedPhases.map(p => ( ))}
{/* Search */} setSearchTerm(e.target.value)} placeholder="Rechercher un match ou une date…" style={{ ...inputStyle, width: "100%", marginBottom: 14, boxSizing: "border-box" }} /> {Object.entries(matchesByDate).map(([date, dayMatches]) => (
📅 {date}
{dayMatches.map(m => (
{(() => { const locked = isMatchLocked(m.date, m.heure); return (
{matchNames[m.id] || m.match}
{m.heure && 🕐 {m.heure}} {locked && FERMÉ}
); })()}
{playerNames.map((name, i) => { const p = pronos[m.id]?.[name] || {}; const r = results[m.id]; const pts = r ? calcPoints(p, r) : null; return (
{name}
{(() => { const locked = isMatchLocked(m.date, m.heure); return (
{locked ? ( <> {p.h ?? "–"} {p.a ?? "–"} 🔒 ) : ( <> setProno(m.id, name, "h", v)} /> setProno(m.id, name, "a", v)} /> )} {pts !== null && ( {pts > 0 ? `+${pts}` : "✗"} )}
); })()}
); })}
))}
))}
)} {/* RÉSULTATS */} {tab === "resultats" && (

Résultats officiels

{!isAdmin && (
🔒 Lecture seule — pour modifier.
)}

Saisissez les scores réels pour mettre à jour le classement automatiquement.

{sortedPhases.map(p => ( ))}
setSearchTerm(e.target.value)} placeholder="Rechercher un match…" style={{ ...inputStyle, width: "100%", marginBottom: 14, boxSizing: "border-box" }} /> {Object.entries(matchesByDate).map(([date, dayMatches]) => (
📅 {date}
{dayMatches.map(m => { const r = results[m.id] || {}; const done = r.h !== undefined && r.h !== "" && r.a !== undefined && r.a !== ""; const isKnockout = m.phase !== "Groupes"; const displayName = matchNames[m.id] || m.match; const isEditing = editingMatchId === m.id; return (
{isKnockout && isEditing ? (
setEditingMatchVal(e.target.value)} onKeyDown={e => { if (e.key === "Enter") { setMatchNames(mn => ({ ...mn, [m.id]: editingMatchVal })); setEditingMatchId(null); } if (e.key === "Escape") setEditingMatchId(null); }} style={{ flex: 1, background: "#0d1b2a", border: "1px solid #c8a94a", borderRadius: 6, color: "#e8e0d0", padding: "5px 8px", fontSize: 13, outline: "none" }} />
) : (
{displayName} {m.heure && 🕐 {m.heure}} {isKnockout && isAdmin && ( )}
)}
setResult(m.id, "h", v) : () => {}} /> setResult(m.id, "a", v) : () => {}} /> {done && }
); })}
))}
)} {/* RÈGLEMENT */} {tab === "reglement" && (

📋 Règlement du concours

Concours de Pronostics
Coupe du Monde FIFA 2026
11 participants · Sans enjeu financier
{REGLEMENT.map((art, i) => (
Article {i + 1} — {art.titre}
{art.contenu}
))}
Règlement adopté par les 11 participants avant le début de la compétition.
)} {/* CONFIG */} {tab === "config" && (

⚙️ Configuration

{!isAdmin && (
🔒 Lecture seule — pour modifier.
)}
Noms des participants
{playerNames.map((name, i) => ( { const c = [...playerNames]; c[i] = e.target.value; setPlayerNames(c); }} style={{ ...inputStyle, background: editingNames ? "#0d1b2a" : "rgba(255,255,255,0.03)", border: `1px solid ${editingNames ? "#c8a94a" : "#1a2e45"}` }} /> ))}
Programme des matchs

104 matchs sont intégrés (données officielles FIFA 2026) et ne peuvent pas être modifiés dans cette version.

{sortedPhases.map(p => { const count = ALL_MATCHES.filter(m => m.phase === p).length; return (
{p} ({count})
); })}
🏆 Champion du monde (pour activer le bonus +10)

Saisissez le nom de l'équipe championne après la finale pour créditer automatiquement +10 pts aux participants qui l'avaient pronostiquée.

isAdmin && setTournamentWinner(e.target.value)} disabled={!isAdmin} placeholder="ex: France" style={{ ...inputStyle, flex: 1, opacity: isAdmin ? 1 : 0.5 }} /> {tournamentWinner && ( ✓ Bonus actif )}
Zone de danger

Remet à zéro toutes les données (noms, pronostics, résultats). Les matchs restent.

)}
{/* PIN MODAL */} {showPinModal && (
setShowPinModal(false)}>
e.stopPropagation()} style={{ background: "#111e2d", border: "2px solid #c8a94a", borderRadius: 14, padding: "32px 28px", width: 300, textAlign: "center", }}>
🔒
Accès administrateur
Saisissez le code PIN à 5 chiffres
{[0,1,2,3,4].map(i => (
i ? "#c8a94a" : "#1e3a5a"}`, borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 22, fontWeight: 800, color: "#c8a94a", }}> {pinInput.length > i ? "●" : ""}
))}
{pinError &&
Code incorrect, réessayez.
}
{[1,2,3,4,5,6,7,8,9].map(n => ( ))}
)}
); }