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>