Tonnie's Huiskamer | ([webnode.com](https://www.webnode.com/support/add-html-code/?utm_source=chatgpt.com)) :root { --bg: #121212; --card: #1b1b1b; --line: #2d2d2d; --text: #f3eadc; --muted: #c8bda7; --gold: #d4a24a; --gold-soft: #e0b869; --ok: #8fd39d; --bad: #f2b1b1; } * { box-sizing: border-box; } body { margin: 0; font-family: Georgia, "Times New Roman", serif; background: var(--bg); color: var(--text); line-height: 1.6; } a { color: inherit; text-decoration: none; } .wrap { width: min(1040px, calc(100% - 32px)); margin: 0 auto; } .topline { background: #0a0a0a; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 14px; } .topline .wrap { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 10px 0; } .nav { background: #000; border-bottom: 1px solid var(--line); } .nav .wrap { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 18px 0; flex-wrap: wrap; } .brand { color: var(--gold); font-size: 28px; font-weight: 700; } .tagline { color: var(--muted); font-size: 14px; margin-top: 4px; } .menu { display: flex; gap: 16px; flex-wrap: wrap; color: var(--muted); font-size: 15px; } .hero, .section { padding: 28px 0; border-bottom: 1px solid var(--line); } .hero-grid, .grid-2, .grid-3, .stats-grid { display: grid; gap: 18px; } .hero-grid { grid-template-columns: 1.1fr 0.9fr; align-items: center; } .grid-2 { grid-template-columns: 1fr 1fr; } .grid-3 { grid-template-columns: repeat(3, 1fr); } .stats-grid { grid-template-columns: repeat(4, 1fr); } h1, h2, h3 { line-height: 1.15; margin: 0 0 12px; } h1 { font-size: clamp(32px, 5vw, 52px); } h2 { font-size: clamp(24px, 3vw, 34px); color: var(--gold); } h3 { font-size: 22px; color: var(--gold-soft); } p { margin: 0 0 12px; } .muted { color: var(--muted); } .small { font-size: 14px; } .card { background: var(--card); border: 1px solid var(--line); padding: 16px; margin-bottom: 18px; } .pool-visual { background: var(--card); border: 1px solid var(--line); padding: 12px; } .btn, button { background: var(--gold); color: #111; border: none; padding: 12px 16px; font: inherit; font-weight: 700; cursor: pointer; } .btn.secondary { background: transparent; color: var(--text); border: 1px solid var(--line); } .hero-actions, .contact-links { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; } table { width: 100%; border-collapse: collapse; font-size: 15px; } th, td { padding: 10px 8px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; } th { color: var(--gold-soft); } .team-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 8px; background: linear-gradient(135deg, #fff 0%, #f0c34d 100%); vertical-align: middle; } .stat { background: var(--card); border: 1px solid var(--line); padding: 16px; text-align: center; } .stat strong { display: block; font-size: 32px; color: var(--gold-soft); margin-bottom: 6px; } .notice { padding: 10px 12px; background: #101010; border: 1px solid var(--line); color: var(--muted); margin-top: 12px; } .ok { color: var(--ok); } .bad { color: var(--bad); } input, textarea, select { width: 100%; background: #111; color: var(--text); border: 1px solid var(--line); padding: 11px 12px; font: inherit; margin-bottom: 12px; } textarea { min-height: 110px; resize: vertical; } .captcha { display: flex; align-items: center; gap: 10px; border: 1px solid var(--line); background: #101010; padding: 12px; margin: 8px 0 14px; } .captcha input[type="checkbox"] { width: 20px; height: 20px; margin: 0; accent-color: var(--gold); } .admin-toggle { margin-bottom: 12px; } .admin-panel { display: none; } .admin-panel.visible { display: block; } .footer { background: #0b0b0b; color: var(--muted); padding: 30px 0 40px; } .gallery-item { background: var(--card); border: 1px solid var(--line); padding: 12px; } .gallery-item .art { margin-bottom: 12px; border: 1px solid var(--line); background: #151515; } @media (max-width: 860px) { .hero-grid, .grid-2, .grid-3, .stats-grid { grid-template-columns: 1fr; } } </style> </head> <body> <div class="topline"> <div class="wrap"> <div>Standaard open do, vr, za vanaf 19:30</div> <div>info@tonnieshuiskamer.nl • 06 38829023</div> </div> </div> <header class="nav"> <div class="wrap"> <div> <div class="brand">Tonnie's Huiskamer</div> <div class="tagline">Sport in Huiselijke Sfeer!</div> </div> <nav class="menu"> <a href="#uitslagen">Uitslagen</a> <a href="#standen">Stand</a> <a href="#regels">8-ball regels</a> <a href="#inschrijven">Inschrijven</a> <a href="#admin">Admin</a> </nav> </div> </header> <main class="wrap"> <section class="hero"> <div class="hero-grid"> <div> <h1>Poolbiljart Competitie</h1> <p class="muted"> Webnode-versie van de competitiepagina, met Supabase-koppeling voor uitslagen, standen en inschrijvingen. Deze versie werkt als één HTML-embed zonder Next.js. </p> <div class="hero-actions"> <a class="btn" href="#standen">Bekijk stand</a> <a class="btn secondary" href="#inschrijven">Schrijf je in</a> </div> </div> <div class="pool-visual"> <svg viewBox="0 0 760 500" aria-label="Pooltafel illustratie" role="img"> <defs> <linearGradient id="felt" x1="0" x2="1"><stop offset="0%" stop-color="#2d7047"/><stop offset="100%" stop-color="#23563a"/></linearGradient> <linearGradient id="wood" x1="0" x2="1"><stop offset="0%" stop-color="#7a4f25"/><stop offset="100%" stop-color="#5f3a18"/></linearGradient> </defs> <rect x="34" y="40" width="692" height="420" rx="30" fill="url(#wood)"/> <rect x="84" y="86" width="592" height="328" rx="18" fill="url(#felt)"/> <circle cx="84" cy="86" r="18" fill="#0b0b0b"/><circle cx="380" cy="86" r="14" fill="#0b0b0b"/><circle cx="676" cy="86" r="18" fill="#0b0b0b"/> <circle cx="84" cy="414" r="18" fill="#0b0b0b"/><circle cx="380" cy="414" r="14" fill="#0b0b0b"/><circle cx="676" cy="414" r="18" fill="#0b0b0b"/> <circle cx="196" cy="250" r="22" fill="#f2f2f2"/><circle cx="190" cy="243" r="5" fill="#fff" fill-opacity=".8"/> <g transform="translate(485 250)"><circle cx="0" cy="-50" r="21" fill="#f0c34d"/><circle cx="-22" cy="-24" r="21" fill="#c94232"/><circle cx="22" cy="-24" r="21" fill="#2a60a5"/><circle cx="-44" cy="2" r="21" fill="#ea8b32"/><circle cx="0" cy="2" r="21" fill="#141414"/><circle cx="44" cy="2" r="21" fill="#6f41a0"/><circle cx="-22" cy="28" r="21" fill="#2c7247"/><circle cx="22" cy="28" r="21" fill="#943233"/><circle cx="0" cy="54" r="21" fill="#8e6538"/></g> <line x1="220" y1="250" x2="440" y2="250" stroke="#ead8b5" stroke-width="3" stroke-dasharray="8 8"/> </svg> </div> </div> </section> <section class="section"> <div class="stats-grid"> <div class="stat"><strong id="statTeams">-</strong><span class="muted">Deelnemers</span></div> <div class="stat"><strong id="statMatches">-</strong><span class="muted">Wedstrijden</span></div> <div class="stat"><strong>3</strong><span class="muted">Punten voor winst</span></div> <div class="stat"><strong id="statLeader">-</strong><span class="muted">Koploper</span></div> </div> </section> <section class="section" id="uitslagen"> <h2>Uitslagen</h2> <div class="card"> <table> <thead> <tr><th>Datum</th><th>Wedstrijd</th><th>Score</th><th>Frames</th></tr> </thead> <tbody id="resultsTableBody"> <tr><td colspan="4">Laden…</td></tr> </tbody> </table> </div> </section> <section class="section" id="standen"> <h2>Stand</h2> <div class="card"> <table> <thead> <tr><th>#</th><th>Team / speler</th><th>G</th><th>W</th><th>Gelijk</th><th>V</th><th>Frames</th><th>Saldo</th><th>Pnt</th></tr> </thead> <tbody id="standingsTableBody"> <tr><td colspan="9">Laden…</td></tr> </tbody> </table> </div> </section> <section class="section" id="regels"> <h2>8-ball regels</h2> <div class="grid-3"> <div class="card"><h3>Break</h3><p>De break is geldig als minimaal vier objectballen een band raken of er direct een bal gepot wordt.</p></div> <div class="card"><h3>Heel of half</h3><p>Na open tafel wordt na de eerste correct gepotte bal bepaald wie heel en wie half speelt.</p></div> <div class="card"><h3>De zwarte 8</h3><p>De 8-ball mag pas gespeeld worden nadat alle eigen ballen weg zijn. Te vroeg potten is verlies van het frame.</p></div> </div> </section> <section class="section"> <h2>Fotogalerij</h2> <div class="grid-3"> <article class="gallery-item"><div class="art"><svg viewBox="0 0 500 300"><rect width="500" height="300" fill="#131313"/><rect x="30" y="40" width="440" height="220" rx="18" fill="#6d4722"/><rect x="60" y="66" width="380" height="168" rx="12" fill="#2b6a46"/><circle cx="60" cy="66" r="14" fill="#0a0a0a"/><circle cx="250" cy="66" r="11" fill="#0a0a0a"/><circle cx="440" cy="66" r="14" fill="#0a0a0a"/><circle cx="60" cy="234" r="14" fill="#0a0a0a"/><circle cx="250" cy="234" r="11" fill="#0a0a0a"/><circle cx="440" cy="234" r="14" fill="#0a0a0a"/></svg></div><strong>De tafel</strong><p class="muted small">Warm hout, groen laken en een eenvoudige opmaak zonder poespas.</p></article> <article class="gallery-item"><div class="art"><svg viewBox="0 0 500 300"><rect width="500" height="300" fill="#141414"/><polygon points="250,45 350,225 150,225" fill="none" stroke="#d4a24a" stroke-width="8"/><circle cx="250" cy="85" r="18" fill="#f0c34d"/><circle cx="231" cy="120" r="18" fill="#c94232"/><circle cx="269" cy="120" r="18" fill="#2a60a5"/><circle cx="212" cy="155" r="18" fill="#ea8b32"/><circle cx="250" cy="155" r="18" fill="#151515"/><circle cx="288" cy="155" r="18" fill="#6f41a0"/></svg></div><strong>Het rack</strong><p class="muted small">Een compact sfeerbeeld voor speelavonden en competitie-updates.</p></article> <article class="gallery-item"><div class="art"><svg viewBox="0 0 500 300"><rect width="500" height="300" fill="#161616"/><circle cx="140" cy="160" r="55" fill="#f2f2f2"/><circle cx="250" cy="130" r="55" fill="#cb4333"/><circle cx="360" cy="170" r="55" fill="#171717"/></svg></div><strong>De ballen</strong><p class="muted small">Ingebouwde illustraties, zodat de pagina zonder losse bestanden werkt.</p></article> </div> </section> <section class="section" id="inschrijven"> <h2>Inschrijven</h2> <div class="grid-2"> <div class="card"> <p class="muted">Dit formulier slaat inschrijvingen op in Supabase en opent daarna optioneel e-mail en WhatsApp met dezelfde gegevens.</p> <form id="signupForm"> <label for="name">Naam</label> <input id="name" name="name" type="text" required /> <label for="team">Teamnaam</label> <input id="team" name="team" type="text" required /> <label for="email">E-mailadres</label> <input id="email" name="email" type="email" required /> <label for="phone">Telefoonnummer</label> <input id="phone" name="phone" type="tel" required /> <label for="notes">Opmerking</label> <textarea id="notes" name="notes"></textarea> <div class="captcha"> <input id="notRobot" name="notRobot" type="checkbox" required /> <label for="notRobot" style="margin:0">Ik ben geen robot</label> </div> <button type="submit">Verstuur inschrijving</button> <div id="signupMessage" class="notice" style="display:none"></div> </form> </div> <div class="card"> <h3>Contact</h3> <p class="muted">Je kunt ook rechtstreeks contact opnemen met Tonnie's Huiskamer.</p> <div class="contact-links"> <a class="btn secondary" href="mailto:info@tonnieshuiskamer.nl?subject=Inschrijving%20Poolbiljart%20Competitie">E-mail sturen</a> <a class="btn secondary" href="https://wa.me/31638829023?text=Hallo%20Tonnie%2C%20ik%20wil%20me%20inschrijven%20voor%20de%20poolbiljartcompetitie." target="_blank" rel="noopener">WhatsApp openen</a> </div> <div class="notice small">Voor productie is een echte captcha zoals reCAPTCHA of Turnstile veiliger dan alleen een checkbox.</div> </div> </div> </section> <section class="section" id="admin"> <h2>Admin</h2> <div class="card"> <div class="admin-toggle"> <button id="toggleAdminBtn" class="btn secondary" type="button">Toon / verberg admin</button> </div> <div class="notice small">Deze admin werkt alleen als jouw Supabase policies inserts toestaan op <code>players</code> en <code>matches</code>. Voor productie liever via login of een serverfunctie.</div> <div id="adminPanel" class="admin-panel"> <div class="grid-2"> <div class="card"> <h3>Deelnemer toevoegen</h3> <form id="playerForm"> <label for="playerName">Team / speler</label> <input id="playerName" type="text" required /> <button type="submit">Toevoegen</button> </form> <div id="playerMessage" class="notice" style="display:none"></div> </div> <div class="card"> <h3>Uitslag invoeren</h3> <form id="matchForm"> <label for="matchDate">Datum</label> <input id="matchDate" type="date" required /> <label for="homePlayer">Thuis</label> <select id="homePlayer" required></select> <label for="awayPlayer">Uit</label> <select id="awayPlayer" required></select> <label for="homeFrames">Frames thuis</label> <input id="homeFrames" type="number" min="0" required /> <label for="awayFrames">Frames uit</label> <input id="awayFrames" type="number" min="0" required /> <button type="submit">Uitslag opslaan</button> </form> <div id="matchMessage" class="notice" style="display:none"></div> </div> </div> </div> </div> </section> </main> <footer class="footer"> <div class="wrap"> <div class="card"> <h3>Webnode versie</h3> <p>Deze pagina is bedoeld voor een HTML-element in Webnode. Webnode ondersteunt HTML-inhoud op pagina’s en aparte headercode, maar dat is voor veel projecten een premiumfunctie. ([webnode.com](https://www.webnode.com/support/add-html-code/?utm_source=chatgpt.com))</p> </div> </div> </footer> <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script> <script> const SUPABASE_URL = 'https://amxajnacwngngchyzgws.supabase.co'; const SUPABASE_KEY = 'sb_publishable__hNij5uQ5ZMkCmim9rxQFg_chHfER9e'; const supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY); function formatDate(dateStr) { return new Intl.DateTimeFormat('nl-NL', { day: '2-digit', month: 'short', year: 'numeric' }).format(new Date(dateStr + 'T12:00:00')); } function calculateStandings(players, matches) { const map = {}; players.forEach((player) => { map[player.name] = { name: player.name, played: 0, won: 0, drawn: 0, lost: 0, framesFor: 0, framesAgainst: 0, points: 0 }; }); matches.forEach((m) => { if (!map[m.home_player]) map[m.home_player] = { name: m.home_player, played: 0, won: 0, drawn: 0, lost: 0, framesFor: 0, framesAgainst: 0, points: 0 }; if (!map[m.away_player]) map[m.away_player] = { name: m.away_player, played: 0, won: 0, drawn: 0, lost: 0, framesFor: 0, framesAgainst: 0, points: 0 }; const home = map[m.home_player]; const away = map[m.away_player]; home.played += 1; away.played += 1; home.framesFor += Number(m.home_frames); home.framesAgainst += Number(m.away_frames); away.framesFor += Number(m.away_frames); away.framesAgainst += Number(m.home_frames); if (Number(m.home_frames) > Number(m.away_frames)) { home.won += 1; away.lost += 1; home.points += 3; } else if (Number(m.home_frames) < Number(m.away_frames)) { away.won += 1; home.lost += 1; away.points += 3; } else { home.drawn += 1; away.drawn += 1; home.points += 1; away.points += 1; } }); return Object.values(map).sort((a, b) => { const pointDiff = b.points - a.points; if (pointDiff !== 0) return pointDiff; const saldoA = a.framesFor - a.framesAgainst; const saldoB = b.framesFor - b.framesAgainst; if (saldoB !== saldoA) return saldoB - saldoA; return b.framesFor - a.framesFor; }); } async function loadData() { const resultsBody = document.getElementById('resultsTableBody'); const standingsBody = document.getElementById('standingsTableBody'); const [{ data: players, error: playersError }, { data: matches, error: matchesError }] = await Promise.all([ supabaseClient.from('players').select('name').order('name'), supabaseClient.from('matches').select('*').order('played_on', { ascending: false }) ]); if (playersError || matchesError) { resultsBody.innerHTML = `<tr><td colspan="4">Fout bij laden van gegevens.</td></tr>`; standingsBody.innerHTML = `<tr><td colspan="9">Controleer je Supabase-tabellen en RLS policies.</td></tr>`; return; } renderPlayerOptions(players || []); renderResults(matches || []); renderStandings(players || [], matches || []); } function renderPlayerOptions(players) { const homeSel = document.getElementById('homePlayer'); const awaySel = document.getElementById('awayPlayer'); const options = ['<option value="">Kies deelnemer</option>'] .concat(players.map((p) => `<option value="${String(p.name).replace(/"/g, '"')}">${p.name}</option>`)) .join(''); homeSel.innerHTML = options; awaySel.innerHTML = options; document.getElementById('statTeams').textContent = String(players.length); } function renderResults(matches) { const body = document.getElementById('resultsTableBody'); if (!matches.length) { body.innerHTML = '<tr><td colspan="4">Nog geen uitslagen.</td></tr>'; document.getElementById('statMatches').textContent = '0'; return; } body.innerHTML = matches.map((m) => { const homePoints = Number(m.home_frames) > Number(m.away_frames) ? 3 : Number(m.home_frames) < Number(m.away_frames) ? 0 : 1; const awayPoints = Number(m.home_frames) < Number(m.away_frames) ? 3 : Number(m.home_frames) > Number(m.away_frames) ? 0 : 1; return `<tr><td>${formatDate(m.played_on)}</td><td>${m.home_player} – ${m.away_player}</td><td>${homePoints} - ${awayPoints}</td><td>${m.home_frames} - ${m.away_frames}</td></tr>`; }).join(''); document.getElementById('statMatches').textContent = String(matches.length); } function renderStandings(players, matches) { const standings = calculateStandings(players, matches); const body = document.getElementById('standingsTableBody'); if (!standings.length) { body.innerHTML = '<tr><td colspan="9">Nog geen stand beschikbaar.</td></tr>'; document.getElementById('statLeader').textContent = '-'; return; } body.innerHTML = standings.map((s, idx) => `<tr><td>${idx + 1}</td><td><span class="team-dot"></span>${s.name}</td><td>${s.played}</td><td>${s.won}</td><td>${s.drawn}</td><td>${s.lost}</td><td>${s.framesFor} - ${s.framesAgainst}</td><td>${s.framesFor - s.framesAgainst}</td><td>${s.points}</td></tr>`).join(''); document.getElementById('statLeader').textContent = standings[0].name; } function showMessage(id, text, ok) { const el = document.getElementById(id); el.style.display = 'block'; el.className = 'notice ' + (ok ? 'ok' : 'bad'); el.textContent = text; } document.getElementById('signupForm').addEventListener('submit', async (e) => { e.preventDefault(); const payload = { name: document.getElementById('name').value.trim(), team: document.getElementById('team').value.trim(), email: document.getElementById('email').value.trim(), phone: document.getElementById('phone').value.trim(), notes: document.getElementById('notes').value.trim() }; const notRobot = document.getElementById('notRobot').checked; if (!notRobot) { showMessage('signupMessage', 'Bevestig eerst dat je geen robot bent.', false); return; } const { error } = await supabaseClient.from('signups').insert([payload]); if (error) { showMessage('signupMessage', 'Opslaan mislukt: ' + error.message, false); return; } showMessage('signupMessage', 'Inschrijving opgeslagen.', true); const emailBody = `Nieuwe inschrijving poolbiljart competitie\n\nNaam: ${payload.name}\nTeam: ${payload.team}\nEmail: ${payload.email}\nTelefoon: ${payload.phone}\nOpmerking: ${payload.notes}`; const waText = `Hallo Tonnie, ik wil me inschrijven voor de poolbiljartcompetitie.\n\nNaam: ${payload.name}\nTeam: ${payload.team}\nEmail: ${payload.email}\nTelefoon: ${payload.phone}\nOpmerking: ${payload.notes}`; window.open(`mailto:info@tonnieshuiskamer.nl?subject=Inschrijving%20Poolbiljart%20Competitie&body=${encodeURIComponent(emailBody)}`, '_blank'); window.open(`https://wa.me/31638829023?text=${encodeURIComponent(waText)}`, '_blank'); e.target.reset(); }); document.getElementById('playerForm').addEventListener('submit', async (e) => { e.preventDefault(); const name = document.getElementById('playerName').value.trim(); const { error } = await supabaseClient.from('players').insert([{ name }]); if (error) { showMessage('playerMessage', 'Toevoegen mislukt: ' + error.message, false); return; } showMessage('playerMessage', 'Deelnemer toegevoegd.', true); e.target.reset(); loadData(); }); document.getElementById('matchForm').addEventListener('submit', async (e) => { e.preventDefault(); const payload = { played_on: document.getElementById('matchDate').value, home_player: document.getElementById('homePlayer').value, away_player: document.getElementById('awayPlayer').value, home_frames: Number(document.getElementById('homeFrames').value), away_frames: Number(document.getElementById('awayFrames').value) }; if (payload.home_player === payload.away_player) { showMessage('matchMessage', 'Thuis en uit mogen niet hetzelfde zijn.', false); return; } const { error } = await supabaseClient.from('matches').insert([payload]); if (error) { showMessage('matchMessage', 'Opslaan mislukt: ' + error.message, false); return; } showMessage('matchMessage', 'Wedstrijd opgeslagen.', true); e.target.reset(); loadData(); }); document.getElementById('toggleAdminBtn').addEventListener('click', () => { document.getElementById('adminPanel').classList.toggle('visible'); }); loadData(); </script> </body> </html>