[READ-ONLY] Mirror of https://github.com/flo-bit/achtung-5-kegelzaehler.
0

Configure Feed

Select the types of activity you want to include in your feed.

Add fair 100-point win condition and team-colored history

+85 -16
+84 -15
index.html
··· 27 27 --active:#1f4a3c; 28 28 --active-ink:#fff; 29 29 --special:#1f4a3c; 30 + --team-a:#df4935; 31 + --team-a-bg:#f9e5df; 32 + --team-b:#2b765e; 33 + --team-b-bg:#e1eee8; 30 34 --shadow:0 20px 55px rgba(29,38,34,.1); 31 35 } 32 36 html[data-theme="dark"] { ··· 42 46 --active:#d7b950; 43 47 --active-ink:#151a17; 44 48 --special:#315e4f; 49 + --team-a:#f05a44; 50 + --team-a-bg:#38231f; 51 + --team-b:#57aa89; 52 + --team-b-bg:#1d332b; 45 53 --shadow:0 22px 65px rgba(0,0,0,.3); 46 54 } 47 55 * { box-sizing:border-box; } ··· 115 123 .throw:hover { transform:translateY(-2px); filter:brightness(.97); } 116 124 [data-theme="dark"] .throw:hover { filter:brightness(1.1); } 117 125 .throw:active { transform:scale(.97); } 126 + .throw:disabled { opacity:.35; cursor:not-allowed; transform:none; filter:none; } 118 127 .throw.two { color:var(--accent-ink); background:var(--accent); border-color:var(--accent); } 119 128 .throw.five { color:#fff; background:var(--special); border-color:var(--special); } 120 129 .throw small { position:absolute; left:3px; right:3px; bottom:7px; font-size:8px; line-height:1; font-weight:900; letter-spacing:.04em; text-transform:uppercase; opacity:.88; } ··· 137 146 .modal-body { padding:20px; overflow:auto; max-height:520px; } 138 147 .history-list { list-style:none; margin:0; padding:0; display:grid; gap:8px; } 139 148 .history-empty { padding:28px 0; color:var(--muted); text-align:center; font-size:12px; } 140 - .history-item { display:flex; align-items:center; gap:11px; padding:11px; border-radius:11px; background:var(--panel-2); } 141 - .history-pin { width:36px; height:36px; flex:0 0 auto; display:grid; place-items:center; border-radius:9px; color:var(--ink); background:var(--panel); font-weight:900; } 149 + .history-legend { display:flex; gap:15px; margin:0 0 13px; color:var(--muted); font-size:10px; font-weight:750; } 150 + .history-legend span { display:flex; align-items:center; gap:6px; min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; } 151 + .history-legend i { width:8px; height:8px; flex:0 0 auto; border-radius:50%; } 152 + .history-legend .a i { background:var(--team-a); } 153 + .history-legend .b i { background:var(--team-b); } 154 + .history-item { display:flex; align-items:center; gap:11px; padding:11px; border:1px solid transparent; border-radius:11px; } 155 + .history-item.team-0 { border-color:var(--team-a); background:var(--team-a-bg); } 156 + .history-item.team-1 { border-color:var(--team-b); background:var(--team-b-bg); } 157 + .history-pin { width:36px; height:36px; flex:0 0 auto; display:grid; place-items:center; border-radius:9px; color:#fff; font-weight:900; } 158 + .team-0 .history-pin { background:var(--team-a); } 159 + .team-1 .history-pin { background:var(--team-b); } 142 160 .history-copy { min-width:0; flex:1; } 143 161 .history-copy b { display:block; overflow:hidden; color:var(--ink); font-size:12px; white-space:nowrap; text-overflow:ellipsis; } 144 162 .history-copy small { color:var(--muted); font-size:10px; } ··· 148 166 .qr-wrap { width:220px; height:220px; margin:auto; padding:10px; border-radius:13px; background:#fff; } 149 167 .qr-wrap img { display:block; width:100%; height:100%; } 150 168 .copy-link { width:220px; margin-top:14px; padding:11px; border:1px solid var(--line); border-radius:10px; color:var(--ink); background:var(--panel-2); cursor:pointer; font-size:11px; font-weight:850; } 169 + .winner-body { padding:32px 24px 25px; text-align:center; } 170 + .trophy { width:66px; height:66px; display:grid; place-items:center; margin:0 auto 17px; border-radius:50%; color:var(--active-ink); background:var(--active); } 171 + .trophy svg { width:32px; } 172 + .winner-body h2 { font-size:27px; } 173 + .winner-body p { margin:8px 0 0; color:var(--muted); font-size:13px; } 174 + .winner-actions { display:grid; grid-template-columns:1fr 1fr; gap:9px; margin-top:25px; } 175 + .winner-actions button { padding:12px 10px; border-radius:10px; cursor:pointer; font-size:11px; font-weight:850; } 176 + .winner-undo { border:1px solid var(--line); color:var(--ink); background:var(--panel-2); } 177 + .new-game { border:1px solid var(--active); color:var(--active-ink); background:var(--active); } 151 178 .toast { 152 179 position:fixed; z-index:20; left:50%; bottom:max(20px,env(safe-area-inset-bottom)); transform:translate(-50%,15px); 153 180 padding:10px 15px; border-radius:999px; color:var(--panel); background:var(--ink); box-shadow:0 10px 30px #0004; ··· 217 244 </div> 218 245 219 246 <div class="throws"> 220 - <div class="throws-head"><h2>Gefallene Kegel</h2><span class="count">Würfe <b id="throwCount">0</b></span></div> 247 + <div class="throws-head"><h2>Gefallene Kegel</h2><span class="count">Ziel <b>100</b> · Würfe <b id="throwCounts">0:0</b></span></div> 221 248 <div class="grid"> 222 249 <button class="throw" data-pins="0" type="button">0</button> 223 250 <button class="throw" data-pins="1" type="button">1</button> ··· 243 270 244 271 <dialog id="historyDialog"> 245 272 <div class="modal-head"><h2>Wurfverlauf</h2><button class="close" data-close="historyDialog" type="button" aria-label="Schließen">×</button></div> 246 - <div class="modal-body"><ol class="history-list" id="historyList"></ol></div> 273 + <div class="modal-body"> 274 + <div class="history-legend"><span class="a"><i></i><b id="legend0">Team Rot</b></span><span class="b"><i></i><b id="legend1">Team Grün</b></span></div> 275 + <ol class="history-list" id="historyList"></ol> 276 + </div> 247 277 </dialog> 248 278 249 279 <dialog id="qrDialog"> ··· 255 285 </div> 256 286 </dialog> 257 287 288 + <dialog id="winnerDialog"> 289 + <div class="modal-body winner-body"> 290 + <span class="trophy" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M8 4h8v5a4 4 0 0 1-8 0V4Z"/><path d="M8 6H4v1a4 4 0 0 0 4 4M16 6h4v1a4 4 0 0 1-4 4M12 13v4M8 21h8M9 17h6"/></svg></span> 291 + <h2 id="winnerTitle">Gewonnen!</h2> 292 + <p id="winnerScore"></p> 293 + <div class="winner-actions"><button class="winner-undo" id="winnerUndo" type="button">Wurf korrigieren</button><button class="new-game" id="newGame" type="button">Neues Spiel</button></div> 294 + </div> 295 + </dialog> 296 + 258 297 <div class="toast" id="toast" role="status" aria-live="polite"></div> 259 298 260 299 <script> ··· 276 315 } 277 316 function save() { try { localStorage.setItem('achtung5-state',JSON.stringify(state)); } catch (_) {} } 278 317 function name(i) { return (state.names[i] || `Team ${i+1}`).trim() || `Team ${i+1}`; } 318 + function throwCounts() { 319 + return state.history.reduce((counts,entry)=>{ counts[entry.thrower]=(counts[entry.thrower]||0)+1; return counts; },[0,0]); 320 + } 321 + function getWinner() { 322 + const counts=throwCounts(); 323 + if (counts[0]!==counts[1] || (state.scores[0]<100 && state.scores[1]<100) || state.scores[0]===state.scores[1]) return null; 324 + return state.scores[0]>state.scores[1] ? 0 : 1; 325 + } 279 326 function render() { 327 + const winner=getWinner(); 328 + const counts=throwCounts(); 280 329 names.forEach((el,i) => { if (document.activeElement !== el) el.value=state.names[i]; }); 281 330 scores.forEach((el,i) => el.textContent=state.scores[i]); 282 - cards.forEach((el,i) => el.classList.toggle('active',state.active===i)); 283 - $('turnText').innerHTML=`<strong>${escapeHtml(name(state.active))}</strong> ist am Zug`; 284 - $('throwCount').textContent=state.history.length; 331 + cards.forEach((el,i) => el.classList.toggle('active',winner===null && state.active===i)); 332 + $('turnText').innerHTML=winner===null ? `<strong>${escapeHtml(name(state.active))}</strong> ist am Zug` : `<strong>${escapeHtml(name(winner))}</strong> gewinnt`; 333 + $('throwCounts').textContent=`${counts[0]}:${counts[1]}`; 285 334 $('undo').disabled=!state.history.length; 335 + document.querySelectorAll('[data-pins]').forEach(button=>button.disabled=winner!==null); 336 + $('switch').disabled=winner!==null; 286 337 if ($('historyDialog').open) renderHistory(); 287 338 } 288 339 function escapeHtml(text) { const el=document.createElement('span'); el.textContent=text; return el.innerHTML; } 289 340 function renderHistory() { 341 + $('legend0').textContent=name(0); $('legend1').textContent=name(1); 290 342 if (!state.history.length) { 291 343 $('historyList').innerHTML='<li class="history-empty">Noch keine Würfe.</li>'; 292 344 return; ··· 295 347 const thrower=escapeHtml(entry.throwerName || name(entry.thrower)); 296 348 const receiver=escapeHtml(entry.receiverName || name(entry.receiver)); 297 349 const detail=entry.pins===5 ? `${thrower} warf 5 · für ${receiver}` : `${thrower} warf ${entry.pins}`; 298 - return `<li class="history-item"><span class="history-pin">${entry.pins}</span><span class="history-copy"><b>${receiver}</b><small>${detail}</small></span><span class="history-points">+${entry.points}</span></li>`; 350 + return `<li class="history-item team-${entry.receiver}"><span class="history-pin">${entry.pins}</span><span class="history-copy"><b>${receiver}</b><small>${detail}</small></span><span class="history-points">+${entry.points}</span></li>`; 299 351 }).join(''); 300 352 } 301 353 function throwPins(pins) { 354 + if (getWinner()!==null) return; 302 355 const thrower=state.active; 303 356 const receiver=pins===5 ? 1-thrower : thrower; 304 357 const points=pins===2 ? 10 : pins; ··· 306 359 state.history.push({pins,points,thrower,receiver,throwerName:name(thrower),receiverName:name(receiver)}); 307 360 state.active=1-thrower; 308 361 save(); render(); 309 - if (pins===2) toast(`+10 für ${name(receiver)}`); 310 - if (pins===5) toast(`+5 für ${name(receiver)}`); 362 + const winner=getWinner(); 363 + if (winner!==null) showWinner(winner); 364 + else if (pins===2) toast(`+10 für ${name(receiver)}`); 365 + else if (pins===5) toast(`+5 für ${name(receiver)}`); 311 366 } 312 367 function undo() { 313 368 const last=state.history.pop(); 314 369 if (!last) return; 370 + if ($('winnerDialog').open) $('winnerDialog').close(); 315 371 state.scores[last.receiver]=Math.max(0,state.scores[last.receiver]-last.points); 316 372 state.active=last.thrower; 317 373 save(); render(); toast('Wurf zurückgenommen'); 318 374 } 319 - function setActive(i) { state.active=i; save(); render(); } 375 + function setActive(i) { if (getWinner()===null) { state.active=i; save(); render(); } } 376 + function showWinner(winner) { 377 + $('winnerTitle').textContent=`${name(winner)} gewinnt!`; 378 + $('winnerScore').textContent=`${state.scores[0]} : ${state.scores[1]} · beide Teams hatten gleich viele Würfe`; 379 + if (!$('winnerDialog').open) $('winnerDialog').showModal(); 380 + } 381 + function resetGame() { 382 + state.scores=[0,0]; state.history=[]; state.active=0; save(); render(); 383 + if ($('winnerDialog').open) $('winnerDialog').close(); 384 + toast('Neues Spiel gestartet'); 385 + } 320 386 function toast(text) { 321 387 $('toast').textContent=text; $('toast').classList.add('show'); 322 388 clearTimeout(toastTimer); toastTimer=setTimeout(()=>$('toast').classList.remove('show'),1800); ··· 340 406 $('switch').addEventListener('click',()=>setActive(1-state.active)); 341 407 $('undo').addEventListener('click',undo); 342 408 $('reset').addEventListener('click',()=>{ 343 - if (!state.history.length || confirm('Punkte und Würfe zurücksetzen?')) { 344 - state.scores=[0,0]; state.history=[]; state.active=0; save(); render(); toast('Zurückgesetzt'); 345 - } 409 + if (!state.history.length || confirm('Punkte und Würfe zurücksetzen?')) resetGame(); 346 410 }); 411 + $('winnerUndo').addEventListener('click',undo); 412 + $('newGame').addEventListener('click',resetGame); 413 + $('winnerDialog').addEventListener('cancel',event=>event.preventDefault()); 347 414 $('historyButton').addEventListener('click',()=>{ renderHistory(); $('historyDialog').showModal(); }); 348 415 $('qrButton').addEventListener('click',()=>$('qrDialog').showModal()); 349 416 $('themeButton').addEventListener('click',()=>setTheme(document.documentElement.dataset.theme==='dark'?'light':'dark')); 350 417 document.querySelectorAll('[data-close]').forEach(button=>button.addEventListener('click',()=>$(button.dataset.close).close())); 351 - document.querySelectorAll('dialog').forEach(modal=>modal.addEventListener('click',e=>{ if(e.target===modal) modal.close(); })); 418 + document.querySelectorAll('dialog').forEach(modal=>modal.addEventListener('click',e=>{ if(e.target===modal && modal.id!=='winnerDialog') modal.close(); })); 352 419 $('copyLink').addEventListener('click',async()=>{ 353 420 try { await navigator.clipboard.writeText(location.href); } 354 421 catch (_) { const input=document.createElement('input'); input.value=location.href; document.body.appendChild(input); input.select(); document.execCommand('copy'); input.remove(); } ··· 367 434 try { theme=localStorage.getItem('achtung5-theme'); } catch (_) {} 368 435 setTheme(theme || (matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light')); 369 436 render(); 437 + const savedWinner=getWinner(); 438 + if (savedWinner!==null) setTimeout(()=>showWinner(savedWinner)); 370 439 })(); 371 440 </script> 372 441 </body>
+1 -1
sw.js
··· 1 - const CACHE = 'achtung5-v1'; 1 + const CACHE = 'achtung5-v2'; 2 2 const APP_SHELL = [ 3 3 './', 4 4 './index.html',