1068 lines
51 KiB
HTML
Executable File
1068 lines
51 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="sv">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Yatzy Poängkort</title>
|
|
|
|
<!-- PWA Meta Tags -->
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<meta name="theme-color" content="#1E1E2E">
|
|
<link rel="apple-touch-icon" href="https://placehold.co/180x180/8839EF/FFFFFF?text=Yatzy">
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
}
|
|
</script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
/* Catppuccin Latte (Light Theme) */
|
|
--bg-base: #EFF1F5;
|
|
--bg-mantle: #E6E9EF;
|
|
--bg-surface: #FFFFFF;
|
|
--text-primary: #4C4F69;
|
|
--text-secondary: #6C6F84;
|
|
--border-color: #CCD0DA;
|
|
--accent-color: #8839EF; /* Mauve */
|
|
--accent-text: #FFFFFF;
|
|
--success-color: #40A02B;
|
|
--error-color: #D20F39;
|
|
--subtle-hover: #DCE0E8;
|
|
}
|
|
|
|
html.dark {
|
|
/* Catppuccin Mocha (Dark Theme) */
|
|
--bg-base: #1E1E2E;
|
|
--bg-mantle: #181825;
|
|
--bg-surface: #11111B;
|
|
--text-primary: #CDD6F4;
|
|
--text-secondary: #A6ADC8;
|
|
--border-color: #45475A;
|
|
--accent-color: #CBA6F7; /* Mauve */
|
|
--accent-text: #1E1E2E;
|
|
--success-color: #A6E3A1;
|
|
--error-color: #F38BA8;
|
|
--subtle-hover: #313244;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
background-color: var(--bg-base);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* Component Styles */
|
|
.card {
|
|
background-color: var(--bg-surface);
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
.header-title { color: var(--accent-color); }
|
|
.header-subtitle { color: var(--text-secondary); }
|
|
.btn-primary { background-color: var(--success-color); color: var(--bg-base); }
|
|
.btn-secondary { background-color: var(--accent-color); color: var(--accent-text); }
|
|
.btn-neutral { background-color: var(--bg-mantle); color: var(--text-primary); border: 1px solid var(--border-color); }
|
|
.btn-neutral:hover { background-color: var(--subtle-hover); }
|
|
.btn-danger { background-color: var(--error-color); color: var(--bg-base); }
|
|
|
|
.score-cell {
|
|
width: 100%; height: 100%; min-height: 44px; display: flex;
|
|
align-items: center; justify-content: center; cursor: pointer;
|
|
border-radius: 8px; transition: background-color 0.2s ease-in-out;
|
|
}
|
|
.score-cell:hover { background-color: var(--subtle-hover); }
|
|
.score-cell.filled {
|
|
font-weight: 600;
|
|
color: var(--accent-color);
|
|
}
|
|
.score-cell-disabled { cursor: not-allowed; }
|
|
.score-cell-disabled:hover { background-color: transparent; }
|
|
|
|
.calculated-field { font-weight: 600; background-color: var(--bg-mantle); }
|
|
.grand-total { font-weight: 700; font-size: 1.125rem; background-color: var(--bg-mantle); color: var(--accent-color); }
|
|
.table-header th { background-color: var(--accent-color); color: var(--accent-text); font-weight: 600; position: sticky; top: 0; z-index: 10; }
|
|
.category-col { position: sticky; left: 0; z-index: 5; background-color: var(--bg-surface); width: 100px; }
|
|
@media (min-width: 640px) {
|
|
.category-col { width: 150px; }
|
|
}
|
|
.current-player-col { background-color: var(--subtle-hover) !important; }
|
|
|
|
/* Modal styles */
|
|
.modal-backdrop {
|
|
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.5); display: flex;
|
|
justify-content: center; align-items: center; z-index: 50;
|
|
}
|
|
.modal-content { width: 90%; max-width: 500px; background-color: var(--bg-surface); }
|
|
|
|
.score-input {
|
|
background-color: var(--bg-mantle); border: 1px solid var(--border-color);
|
|
}
|
|
.player-list-item {
|
|
background-color: var(--bg-surface); border: 1px solid var(--border-color);
|
|
}
|
|
.player-list-item:hover {
|
|
background-color: var(--subtle-hover);
|
|
}
|
|
.error-text {
|
|
color: var(--error-color);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="transition-colors duration-300">
|
|
|
|
<div id="password-modal" class="modal-backdrop">
|
|
<div class="modal-content p-6 sm:p-8 rounded-xl shadow-lg">
|
|
<h2 class="text-xl sm:text-2xl font-bold mb-4 text-center">Lösenordsskyddat</h2>
|
|
<p class="mb-6 text-center">Ange lösenordet för att fortsätta.</p>
|
|
<div class="flex flex-col gap-4">
|
|
<input type="password" id="password-input" class="score-input p-2 text-center text-lg">
|
|
<button id="password-submit-btn" class="btn-primary font-bold py-2 px-4 rounded-lg">Lås upp</button>
|
|
<p id="password-error" class="error-text text-center h-4"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="app" class="container mx-auto p-2 sm:p-4 max-w-7xl hidden">
|
|
<header class="mb-6 text-center relative">
|
|
<h1 class="text-3xl sm:text-4xl font-bold header-title">Yatzy Poängkort</h1>
|
|
<p id="header-subtitle" class="header-subtitle mt-1">Europeiska Regler | Realtidssynk</p>
|
|
<button id="theme-toggle" class="absolute top-0 right-0 p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
|
|
<svg id="theme-icon-light" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
</svg>
|
|
<svg id="theme-icon-dark" class="h-6 w-6 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
</svg>
|
|
</button>
|
|
</header>
|
|
|
|
<!-- Game Active State -->
|
|
<div id="game-container" class="hidden">
|
|
<div class="flex justify-between items-center mb-4 flex-wrap gap-4">
|
|
<div class="flex gap-2">
|
|
<button id="new-game-btn" class="btn-secondary font-bold py-2 px-4 rounded-lg text-sm sm:text-base">
|
|
Nytt Spel
|
|
</button>
|
|
<button id="back-to-start-btn" class="btn-neutral font-bold py-2 px-4 rounded-lg text-sm sm:text-base">
|
|
Till Startsidan
|
|
</button>
|
|
</div>
|
|
<button id="end-game-btn" class="hidden btn-primary font-bold py-2 px-4 rounded-lg text-sm sm:text-base">
|
|
Avsluta Spel & Utse Vinnare
|
|
</button>
|
|
<div id="game-id-container" class="bg-mantle text-sm font-mono p-2 rounded-md">
|
|
Spel-ID: <span id="game-id-display" class="font-semibold"></span>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-auto shadow-lg rounded-lg max-h-[70vh]">
|
|
<table class="w-full border-collapse">
|
|
<thead id="scorecard-head"></thead>
|
|
<tbody id="scorecard-body"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Start Screen -->
|
|
<div id="start-screen">
|
|
<div class="text-center p-4 sm:p-8 card rounded-xl shadow-md">
|
|
<h2 class="text-2xl sm:text-3xl font-semibold mb-2">Välkommen till Yatzy!</h2>
|
|
<p class="mb-6">Starta ett nytt spel eller se topplistorna nedan.</p>
|
|
<button id="show-start-modal-btn" class="btn-primary font-bold py-3 px-6 rounded-lg text-base sm:text-lg mb-8">
|
|
Starta Nytt Spel
|
|
</button>
|
|
<div class="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto mt-12">
|
|
<div class="mt-8">
|
|
<h3 class="text-xl font-semibold mb-4">Topplista Vinster</h3>
|
|
<div id="player-leaderboard"></div>
|
|
</div>
|
|
<div class="mt-8">
|
|
<h3 class="text-xl font-semibold mb-4">Topp 3 Högsta Poäng</h3>
|
|
<div id="high-score-list"></div>
|
|
</div>
|
|
<div class="mt-8">
|
|
<h3 class="text-xl font-semibold mb-4">Senaste Spel</h3>
|
|
<div id="recent-games-list"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Start Game Modal -->
|
|
<div id="start-game-modal" class="modal-backdrop hidden">
|
|
<div class="modal-content p-6 sm:p-8 rounded-xl shadow-lg">
|
|
<h2 class="text-2xl font-bold mb-6 text-center">Skapa Nytt Spel</h2>
|
|
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold mb-2">Lägg till ny spelare</h3>
|
|
<div class="flex flex-col sm:flex-row gap-2 items-center">
|
|
<input type="text" id="new-player-name" placeholder="Ange spelarens namn" class="score-input w-full sm:flex-grow p-2 border rounded-md focus:ring-2 focus:ring-indigo-500">
|
|
<input type="number" id="new-player-wins" placeholder="Vinster" class="score-input w-full sm:w-24 p-2 border rounded-md focus:ring-2 focus:ring-indigo-500">
|
|
<button id="add-player-btn" class="btn-secondary font-bold py-2 px-4 rounded-md w-full sm:w-auto">Lägg till</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-8">
|
|
<h3 class="text-lg font-semibold mb-2">Välj spelare för detta spel</h3>
|
|
<div id="player-list" class="space-y-2 max-h-48 overflow-y-auto p-2 rounded-md border border-color"></div>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-center">
|
|
<button id="cancel-start-game-btn" class="btn-neutral font-bold py-2 px-4 rounded-md">Avbryt</button>
|
|
<button id="start-game-btn" class="btn-primary font-bold py-2 px-4 rounded-md disabled:opacity-50 disabled:cursor-not-allowed" disabled>Starta Spel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Confirmation Modal -->
|
|
<div id="confirm-modal" class="modal-backdrop hidden">
|
|
<div class="modal-content p-8 rounded-xl shadow-lg">
|
|
<h2 id="confirm-modal-title" class="text-xl font-bold mb-4 text-center">Avsluta pågående spel?</h2>
|
|
<p id="confirm-modal-message" class="mb-6 text-center">Vill du spara spelet och fortsätta senare, eller avsluta och ta bort omgången helt?</p>
|
|
<div class="flex flex-col sm:flex-row justify-center gap-4">
|
|
<button id="confirm-save-btn" class="btn-secondary font-bold py-2 px-6 rounded-md">Spara & Avsluta</button>
|
|
<button id="confirm-delete-btn" class="btn-danger font-bold py-2 px-6 rounded-md">Avsluta & Ta Bort</button>
|
|
</div>
|
|
<div class="flex justify-center mt-6">
|
|
<button id="confirm-cancel-btn" class="btn-neutral font-bold py-2 px-4 rounded-md">Gå tillbaka till spelet</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Score Select Modal -->
|
|
<div id="score-select-modal" class="modal-backdrop hidden">
|
|
<div class="modal-content p-8 rounded-xl shadow-lg">
|
|
<h2 id="score-modal-title" class="text-2xl font-bold mb-6 text-center">Välj poäng</h2>
|
|
<div id="score-modal-options" class="flex flex-wrap justify-center gap-3"></div>
|
|
<div class="flex justify-center mt-6">
|
|
<button id="score-modal-cancel-btn" class="btn-neutral font-bold py-2 px-4 rounded-md">Avbryt</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Generic Alert Modal -->
|
|
<div id="alert-modal" class="modal-backdrop hidden">
|
|
<div class="modal-content p-8 rounded-xl shadow-lg">
|
|
<h2 id="alert-modal-title" class="text-xl font-bold mb-4 text-center">Varning</h2>
|
|
<div id="alert-modal-message" class="mb-6 text-center"></div>
|
|
<div class="flex justify-center">
|
|
<button id="alert-modal-close-btn" class="btn-secondary font-bold py-2 px-6 rounded-md">OK</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.0'
|
|
|
|
// --- VIKTIGT: SUPABASE-KONFIGURATION ---
|
|
// För att köra detta på din egen server måste du klistra in din egen Supabase URL och anon key här.
|
|
// Gå till ditt Supabase-projekt -> Project Settings -> API.
|
|
const SUPABASE_URL = 'https://db.tvalen.lol'; // KLISTRA IN DIN SUPABASE URL HÄR
|
|
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzUzOTk5MjAwLCJleHAiOjE5MTE3NjU2MDB9.rqUu8VzDYqPs28uxZUCqmK55abGvB_zMdS-LCb8KA8c'; // KLISTRA IN DIN SUPABASE ANON KEY HÄR
|
|
|
|
let supabase;
|
|
|
|
let currentGameId = null, currentGameData = null, gameUnsubscribe = null;
|
|
let selectedPlayersInOrder = [];
|
|
const appId = 'yatzy-app';
|
|
|
|
const SCORE_CATEGORIES = {
|
|
upper: [
|
|
{ id: 'ones', name: 'Ettor', maxCount: 5, multiple: 1, inputType: 'count' },
|
|
{ id: 'twos', name: 'Tvåor', maxCount: 5, multiple: 2, inputType: 'count' },
|
|
{ id: 'threes', name: 'Treor', maxCount: 5, multiple: 3, inputType: 'count' },
|
|
{ id: 'fours', name: 'Fyror', maxCount: 5, multiple: 4, inputType: 'count' },
|
|
{ id: 'fives', name: 'Femmor', maxCount: 5, multiple: 5, inputType: 'count' },
|
|
{ id: 'sixes', name: 'Sexor', maxCount: 5, multiple: 6, inputType: 'count' }
|
|
],
|
|
lower: [
|
|
{ id: 'pair', name: 'Ett par', inputType: 'options', options: [2, 4, 6, 8, 10, 12] },
|
|
{ id: 'twoPair', name: 'Två par', min: 6, max: 22, inputType: 'score' },
|
|
{ id: 'threeOfAKind', name: 'Tretal', inputType: 'options', options: [3, 6, 9, 12, 15, 18] },
|
|
{ id: 'fourOfAKind', name: 'Fyrtal', inputType: 'options', options: [4, 8, 12, 16, 20, 24] },
|
|
{ id: 'smallStraight', name: 'Liten stege', fixed: 15, inputType: 'score' },
|
|
{ id: 'largeStraight', name: 'Stor stege', fixed: 20, inputType: 'score' },
|
|
{ id: 'fullHouse', name: 'Kåk', min: 7, max: 28, inputType: 'score' },
|
|
{ id: 'chance', name: 'Chans', min: 5, max: 30, inputType: 'score' },
|
|
{ id: 'yatzee', name: 'Yatzy', fixed: 50, inputType: 'score' }
|
|
]
|
|
};
|
|
const ALL_CATEGORIES = [...SCORE_CATEGORIES.upper, ...SCORE_CATEGORIES.lower];
|
|
|
|
// --- DOM ELEMENTS ---
|
|
const appContainer = document.getElementById('app');
|
|
const gameContainer = document.getElementById('game-container');
|
|
const startScreen = document.getElementById('start-screen');
|
|
const scorecardHead = document.getElementById('scorecard-head');
|
|
const scorecardBody = document.getElementById('scorecard-body');
|
|
const gameIdDisplay = document.getElementById('game-id-display');
|
|
const startGameModal = document.getElementById('start-game-modal');
|
|
const showStartModalBtn = document.getElementById('show-start-modal-btn');
|
|
const newGameBtn = document.getElementById('new-game-btn');
|
|
const backToStartBtn = document.getElementById('back-to-start-btn');
|
|
const endGameBtn = document.getElementById('end-game-btn');
|
|
const cancelStartGameBtn = document.getElementById('cancel-start-game-btn');
|
|
const addPlayerBtn = document.getElementById('add-player-btn');
|
|
const newPlayerNameInput = document.getElementById('new-player-name');
|
|
const newPlayerWinsInput = document.getElementById('new-player-wins');
|
|
const playerListContainer = document.getElementById('player-list');
|
|
const startGameBtn = document.getElementById('start-game-btn');
|
|
const alertModal = document.getElementById('alert-modal');
|
|
const alertModalTitle = document.getElementById('alert-modal-title');
|
|
const alertModalMessage = document.getElementById('alert-modal-message');
|
|
const scoreSelectModal = document.getElementById('score-select-modal');
|
|
const scoreModalTitle = document.getElementById('score-modal-title');
|
|
const scoreModalOptions = document.getElementById('score-modal-options');
|
|
const scoreModalCancelBtn = document.getElementById('score-modal-cancel-btn');
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
const themeIconLight = document.getElementById('theme-icon-light');
|
|
const themeIconDark = document.getElementById('theme-icon-dark');
|
|
const passwordModal = document.getElementById('password-modal');
|
|
const passwordInput = document.getElementById('password-input');
|
|
const passwordSubmitBtn = document.getElementById('password-submit-btn');
|
|
const passwordError = document.getElementById('password-error');
|
|
const confirmModal = document.getElementById('confirm-modal');
|
|
|
|
// --- CUSTOM ALERT & CONFIRM ---
|
|
function showAlert(message, title = 'Varning', onOkCallback = null) {
|
|
alertModalTitle.textContent = title;
|
|
alertModalMessage.innerHTML = message;
|
|
alertModal.classList.remove('hidden');
|
|
|
|
const oldBtn = document.getElementById('alert-modal-close-btn');
|
|
const newBtn = oldBtn.cloneNode(true);
|
|
oldBtn.parentNode.replaceChild(newBtn, oldBtn);
|
|
|
|
newBtn.addEventListener('click', () => {
|
|
alertModal.classList.add('hidden');
|
|
if (onOkCallback) {
|
|
onOkCallback();
|
|
}
|
|
}, { once: true });
|
|
}
|
|
|
|
function showConfirm(onSave, onDelete) {
|
|
confirmModal.classList.remove('hidden');
|
|
|
|
const saveBtn = document.getElementById('confirm-save-btn');
|
|
const deleteBtn = document.getElementById('confirm-delete-btn');
|
|
const cancelBtn = document.getElementById('confirm-cancel-btn');
|
|
|
|
const close = () => confirmModal.classList.add('hidden');
|
|
|
|
saveBtn.onclick = () => { close(); onSave(); };
|
|
deleteBtn.onclick = () => { close(); onDelete(); };
|
|
cancelBtn.onclick = () => close();
|
|
}
|
|
|
|
// --- PLAYER MANAGEMENT ---
|
|
async function addDefaultPlayers() {
|
|
const defaultPlayers = [
|
|
{ name: "Emanuel", wins: 580 },
|
|
{ name: "Mamma", wins: 555 },
|
|
{ name: "Pappa", wins: 556 }
|
|
];
|
|
|
|
for (const player of defaultPlayers) {
|
|
const { data, error } = await supabase
|
|
.from('players')
|
|
.select('name')
|
|
.eq('name', player.name)
|
|
.single();
|
|
|
|
if (!data) { // Player does not exist
|
|
const { error: insertError } = await supabase
|
|
.from('players')
|
|
.insert([player]);
|
|
if (insertError) console.error(`Failed to add default player ${player.name}:`, insertError);
|
|
else console.log(`Default player "${player.name}" added.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function addPlayer() {
|
|
const name = newPlayerNameInput.value.trim();
|
|
const wins = newPlayerWinsInput.value;
|
|
const initialWins = parseInt(wins, 10) || 0;
|
|
|
|
if (!name) {
|
|
showAlert("Spelarens namn får inte vara tomt.", "Inmatningsfel");
|
|
return;
|
|
}
|
|
|
|
const { data } = await supabase.from('players').select('name').eq('name', name).single();
|
|
|
|
if (data) {
|
|
showAlert(`En spelare med namnet "${name}" finns redan.`, "Dubblett");
|
|
return;
|
|
}
|
|
|
|
const { error } = await supabase.from('players').insert([{ name: name, wins: initialWins }]);
|
|
if (error) {
|
|
console.error("Error adding player:", error);
|
|
showAlert(`Kunde inte lägga till spelare: ${error.message}`, "Databasfel");
|
|
} else {
|
|
newPlayerNameInput.value = '';
|
|
newPlayerWinsInput.value = '';
|
|
loadPlayersForSelection();
|
|
}
|
|
}
|
|
|
|
async function getPlayers() {
|
|
const { data, error } = await supabase.from('players').select('*');
|
|
if (error) {
|
|
console.error("Error fetching players:", error);
|
|
return [];
|
|
}
|
|
return data;
|
|
}
|
|
|
|
// --- GAME MANAGEMENT ---
|
|
async function getRecentGames() {
|
|
const { data, error } = await supabase
|
|
.from('games')
|
|
.select('*')
|
|
.eq('isFinished', true)
|
|
.order('finishedAt', { ascending: false })
|
|
.limit(10);
|
|
if (error) {
|
|
console.error("Error fetching recent games:", error);
|
|
return [];
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async function getHighScores() {
|
|
const { data, error } = await supabase
|
|
.from('games')
|
|
.select('*')
|
|
.eq('isFinished', true)
|
|
.order('winnerScore', { ascending: false })
|
|
.limit(3);
|
|
if (error) {
|
|
console.error("Error fetching high scores:", error);
|
|
return [];
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async function createNewGame(selectedPlayers) {
|
|
if (selectedPlayers.length === 0) {
|
|
showAlert("Välj minst en spelare.", "Inmatningsfel");
|
|
return;
|
|
}
|
|
try {
|
|
const initialScores = {};
|
|
const playersForGame = [];
|
|
|
|
selectedPlayers.forEach(p => {
|
|
playersForGame.push({ id: p.id, name: p.name });
|
|
initialScores[p.id] = {
|
|
...Object.fromEntries(ALL_CATEGORIES.map(c => [c.id, null])),
|
|
};
|
|
});
|
|
|
|
const { data, error } = await supabase
|
|
.from('games')
|
|
.insert([{
|
|
players: playersForGame,
|
|
scores: initialScores,
|
|
isFinished: false,
|
|
currentPlayerIndex: 0
|
|
}])
|
|
.select()
|
|
.single();
|
|
|
|
if (error) throw error;
|
|
|
|
setCurrentGame(data.id);
|
|
closeStartGameModal();
|
|
} catch (error) {
|
|
console.error("Error creating new game:", error);
|
|
showAlert(`Kunde inte skapa spel: ${error.message}`, "Databasfel");
|
|
}
|
|
}
|
|
|
|
function setCurrentGame(gameId) {
|
|
if (gameUnsubscribe) {
|
|
gameUnsubscribe.unsubscribe();
|
|
gameUnsubscribe = null;
|
|
}
|
|
currentGameId = gameId;
|
|
localStorage.setItem(`yatzee-gameId-${appId}`, gameId);
|
|
|
|
if(gameId){
|
|
gameUnsubscribe = supabase
|
|
.channel(`game:${gameId}`)
|
|
.on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'games', filter: `id=eq.${gameId}` }, payload => {
|
|
currentGameData = payload.new;
|
|
renderGame(currentGameData);
|
|
})
|
|
.subscribe();
|
|
|
|
// Initial fetch
|
|
supabase.from('games').select('*').eq('id', gameId).single().then(({data, error}) => {
|
|
if (data) {
|
|
currentGameData = data;
|
|
renderGame(currentGameData);
|
|
} else {
|
|
resetToStartScreen();
|
|
}
|
|
});
|
|
} else {
|
|
resetToStartScreen();
|
|
}
|
|
}
|
|
|
|
async function updateScore(playerId, categoryId, value) {
|
|
if (!currentGameId || currentGameData.isFinished) return;
|
|
|
|
let inputValue = value === '' ? null : parseInt(value, 10);
|
|
let finalScore = inputValue;
|
|
const category = ALL_CATEGORIES.find(c => c.id === categoryId);
|
|
|
|
if (category && inputValue !== null) {
|
|
if (finalScore < 0) finalScore = 0;
|
|
if (category.inputType === 'count') {
|
|
if (finalScore > category.maxCount) finalScore = category.maxCount;
|
|
finalScore = finalScore * category.multiple;
|
|
} else if (category.inputType === 'options') {
|
|
// value is already the score
|
|
} else { // 'score' type
|
|
if (category.fixed) {
|
|
finalScore = (inputValue === category.fixed) ? category.fixed : 0;
|
|
} else {
|
|
if (category.max && finalScore > category.max) finalScore = category.max;
|
|
if (category.min && finalScore > 0 && finalScore < category.min) finalScore = category.min;
|
|
}
|
|
}
|
|
}
|
|
|
|
const newScores = JSON.parse(JSON.stringify(currentGameData.scores));
|
|
newScores[playerId][categoryId] = finalScore;
|
|
|
|
const currentPlayerId = currentGameData.players[currentGameData.currentPlayerIndex].id;
|
|
let nextPlayerIndex = currentGameData.currentPlayerIndex;
|
|
if(playerId === currentPlayerId) {
|
|
nextPlayerIndex = (currentGameData.currentPlayerIndex + 1) % currentGameData.players.length;
|
|
}
|
|
|
|
// Optimistic UI Update
|
|
currentGameData.scores = newScores;
|
|
currentGameData.currentPlayerIndex = nextPlayerIndex;
|
|
renderScorecard(currentGameData);
|
|
if (isGameComplete(currentGameData)) {
|
|
endGameBtn.classList.remove('hidden');
|
|
}
|
|
|
|
// Update Database in the background
|
|
const { error } = await supabase
|
|
.from('games')
|
|
.update({ scores: newScores, currentPlayerIndex: nextPlayerIndex })
|
|
.eq('id', currentGameId);
|
|
|
|
if (error) {
|
|
console.error("Error updating score in Supabase:", error);
|
|
showAlert(`Poängen kunde inte sparas till databasen, men är sparad lokalt.`, "Synkroniseringsfel");
|
|
}
|
|
}
|
|
|
|
async function finishGame() {
|
|
if (!currentGameData || currentGameData.isFinished) return;
|
|
|
|
const totals = calculateTotals(currentGameData);
|
|
|
|
const results = currentGameData.players.map(p => ({
|
|
name: p.name,
|
|
id: p.id,
|
|
score: totals[p.id].grandTotal
|
|
})).sort((a, b) => b.score - a.score);
|
|
|
|
let resultHtml = '<ul class="text-left space-y-2">';
|
|
results.forEach((r, index) => {
|
|
const medal = index === 0 ? '🥇' : (index === 1 ? '🥈' : (index === 2 ? '🥉' : ''));
|
|
resultHtml += `<li class="flex justify-between items-center text-lg"><span class="font-semibold">${index + 1}. ${r.name} ${medal}</span> <span class="font-bold">${r.score} poäng</span></li>`;
|
|
});
|
|
resultHtml += '</ul>';
|
|
|
|
const onOk = async () => {
|
|
if (currentGameData.players.length < 3) {
|
|
showAlert("Resultat sparas inte för spel med färre än 3 spelare.", "Info", resetToStartScreen);
|
|
return;
|
|
}
|
|
|
|
const highScore = results[0].score;
|
|
const winners = results.filter(r => r.score === highScore);
|
|
let winnerName = "Oavgjort";
|
|
if (winners.length === 1) {
|
|
const winner = winners[0];
|
|
winnerName = winner.name;
|
|
|
|
const { data: playerData } = await supabase.from('players').select('wins').eq('id', winner.id).single();
|
|
const newWins = (playerData.wins || 0) + 1;
|
|
await supabase.from('players').update({ wins: newWins }).eq('id', winner.id);
|
|
}
|
|
|
|
await supabase.from('games').update({
|
|
isFinished: true,
|
|
finishedAt: new Date().toISOString(),
|
|
winnerName: winnerName,
|
|
winnerScore: highScore
|
|
}).eq('id', currentGameId);
|
|
};
|
|
|
|
showAlert(resultHtml, "Spelet är slut!", onOk);
|
|
}
|
|
|
|
// --- UI RENDERING & LOGIC ---
|
|
function calculateTotals(gameData) {
|
|
const playerTotals = {};
|
|
gameData.players.forEach(p => {
|
|
const pScores = gameData.scores[p.id] || {};
|
|
const upperSum = SCORE_CATEGORIES.upper.reduce((sum, cat) => sum + (pScores[cat.id] || 0), 0);
|
|
const upperBonus = upperSum >= 63 ? 50 : 0;
|
|
const upperTotal = upperSum + upperBonus;
|
|
const lowerTotal = SCORE_CATEGORIES.lower.reduce((sum, cat) => sum + (pScores[cat.id] || 0), 0);
|
|
const grandTotal = upperTotal + lowerTotal;
|
|
playerTotals[p.id] = { upperSum, upperBonus, upperTotal, lowerTotal, grandTotal };
|
|
});
|
|
return playerTotals;
|
|
}
|
|
|
|
function isGameComplete(gameData) {
|
|
if (!gameData || !gameData.players || !gameData.scores) return false;
|
|
return gameData.players.every(p => {
|
|
return ALL_CATEGORIES.every(cat => {
|
|
return gameData.scores[p.id]?.[cat.id] !== null;
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderGame(gameData) {
|
|
gameContainer.classList.remove('hidden');
|
|
startScreen.classList.add('hidden');
|
|
gameIdDisplay.textContent = currentGameId;
|
|
|
|
if (isGameComplete(gameData) && !gameData.isFinished) {
|
|
endGameBtn.classList.remove('hidden');
|
|
} else {
|
|
endGameBtn.classList.add('hidden');
|
|
}
|
|
|
|
renderScorecard(gameData);
|
|
}
|
|
|
|
async function resetToStartScreen() {
|
|
if (gameUnsubscribe) {
|
|
gameUnsubscribe.unsubscribe();
|
|
gameUnsubscribe = null;
|
|
}
|
|
currentGameId = null;
|
|
currentGameData = null;
|
|
localStorage.removeItem(`yatzee-gameId-${appId}`);
|
|
gameContainer.classList.add('hidden');
|
|
startScreen.classList.remove('hidden');
|
|
|
|
if (supabase) {
|
|
await renderAllLeaderboards();
|
|
} else {
|
|
const leaderboards = ['player-leaderboard', 'high-score-list', 'recent-games-list'];
|
|
leaderboards.forEach(id => {
|
|
const container = document.getElementById(id);
|
|
if (container) container.innerHTML = '<p class="text-gray-500">Loggar in...</p>';
|
|
});
|
|
}
|
|
}
|
|
|
|
async function renderAllLeaderboards() {
|
|
if (!supabase) return;
|
|
await Promise.all([
|
|
renderPlayerLeaderboard(),
|
|
renderRecentGames(),
|
|
renderHighScores()
|
|
]);
|
|
}
|
|
|
|
async function renderPlayerLeaderboard() {
|
|
const container = document.getElementById('player-leaderboard');
|
|
if (!container) return;
|
|
|
|
try {
|
|
const players = await getPlayers();
|
|
players.sort((a, b) => (b.wins || 0) - (a.wins || 0));
|
|
|
|
if (players.length === 0) {
|
|
container.innerHTML = '<p class="text-gray-500">Inga spelare har lagts till än.</p>';
|
|
} else {
|
|
let html = '<ul class="space-y-3">';
|
|
players.forEach((p, index) => {
|
|
const medal = index === 0 ? '👑' : '';
|
|
html += `
|
|
<li class="flex items-center justify-between p-3 bg-white dark:bg-gray-700 rounded-lg shadow-sm">
|
|
<span class="font-semibold text-lg text-gray-700 dark:text-gray-200">${index + 1}. ${p.name} ${medal}</span>
|
|
<span class="font-bold text-indigo-600 dark:text-indigo-400">${p.wins || 0} vinster</span>
|
|
</li>
|
|
`;
|
|
});
|
|
html += '</ul>';
|
|
container.innerHTML = html;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error rendering leaderboard:", error);
|
|
container.innerHTML = `<p class="text-red-500">Kunde inte ladda topplistan.</p>`;
|
|
}
|
|
}
|
|
|
|
async function renderHighScores() {
|
|
const container = document.getElementById('high-score-list');
|
|
if (!container) return;
|
|
|
|
try {
|
|
const games = await getHighScores();
|
|
if (games.length === 0) {
|
|
container.innerHTML = '<p class="text-gray-500">Inga avslutade spel än.</p>';
|
|
} else {
|
|
let html = '<ul class="space-y-3 text-left">';
|
|
games.forEach((game, index) => {
|
|
const medal = index === 0 ? '🥇' : (index === 1 ? '🥈' : (index === 2 ? '🥉' : ''));
|
|
html += `
|
|
<li class="p-3 bg-white dark:bg-gray-700 rounded-lg shadow-sm">
|
|
<div class="flex items-center justify-between">
|
|
<span class="font-semibold text-gray-700 dark:text-gray-200">${index + 1}. ${game.winnerName} ${medal}</span>
|
|
<span class="font-bold text-indigo-600 dark:text-indigo-400">${game.winnerScore} p</span>
|
|
</div>
|
|
</li>
|
|
`;
|
|
});
|
|
html += '</ul>';
|
|
container.innerHTML = html;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error rendering high scores:", error);
|
|
container.innerHTML = `<p class="text-red-500">Kunde inte ladda topplistan.</p>`;
|
|
}
|
|
}
|
|
|
|
async function renderRecentGames() {
|
|
const container = document.getElementById('recent-games-list');
|
|
if (!container) return;
|
|
|
|
try {
|
|
const games = await getRecentGames();
|
|
if (games.length === 0) {
|
|
container.innerHTML = '<p class="text-gray-500">Inga avslutade spel än.</p>';
|
|
} else {
|
|
let html = '<ul class="space-y-3 text-left">';
|
|
games.forEach(game => {
|
|
const date = new Date(game.finishedAt).toLocaleDateString('sv-SE');
|
|
html += `
|
|
<li class="p-3 bg-white dark:bg-gray-700 rounded-lg shadow-sm">
|
|
<div class="flex items-center justify-between">
|
|
<span class="font-semibold text-gray-700 dark:text-gray-200">${game.winnerName}</span>
|
|
<span class="font-bold text-indigo-600 dark:text-indigo-400">${game.winnerScore} p</span>
|
|
</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">${date}</div>
|
|
</li>
|
|
`;
|
|
});
|
|
html += '</ul>';
|
|
container.innerHTML = html;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error rendering recent games:", error);
|
|
container.innerHTML = `<p class="text-red-500">Kunde inte ladda senaste spel.</p>`;
|
|
}
|
|
}
|
|
|
|
|
|
function renderScorecard(gameData) {
|
|
const players = gameData.players;
|
|
const scores = gameData.scores;
|
|
const isFinished = gameData.isFinished;
|
|
const currentPlayerId = isFinished ? null : gameData.players[gameData.currentPlayerIndex]?.id;
|
|
|
|
scorecardHead.innerHTML = `
|
|
<tr class="table-header">
|
|
<th class="p-3 text-left w-1/4 min-w-[100px] sm:min-w-[150px] rounded-tl-lg category-col">Kategori</th>
|
|
${players.map(p => `<th class="p-3 text-center min-w-[100px] ${p.id === currentPlayerId ? 'current-player-col' : ''}">${p.name}</th>`).join('')}
|
|
</tr>
|
|
`;
|
|
|
|
const playerTotals = calculateTotals(gameData);
|
|
|
|
let bodyHtml = '';
|
|
|
|
const renderCategoryRow = (cat) => {
|
|
return `<tr>
|
|
<td class="p-3 border-b border-gray-200 dark:border-gray-700 category-col">${cat.name}</td>
|
|
${players.map(p => {
|
|
const score = scores[p.id]?.[cat.id];
|
|
const scoreText = (score === null || score === undefined) ? '' : score;
|
|
const isFilled = (score !== null && score !== undefined);
|
|
const filledClass = isFilled ? 'filled' : '';
|
|
const disabledClass = isFinished ? 'score-cell-disabled' : '';
|
|
const currentColClass = p.id === currentPlayerId ? 'current-player-col' : '';
|
|
|
|
return `<td class="p-1 border-b border-gray-200 dark:border-gray-700 ${currentColClass}">
|
|
<div class="score-cell ${filledClass} ${disabledClass}" data-player-id="${p.id}" data-category-id="${cat.id}">
|
|
${scoreText}
|
|
</div>
|
|
</td>`;
|
|
}).join('')}
|
|
</tr>`;
|
|
};
|
|
|
|
SCORE_CATEGORIES.upper.forEach(cat => bodyHtml += renderCategoryRow(cat));
|
|
|
|
bodyHtml += `
|
|
<tr class="calculated-field bg-gray-100 dark:bg-gray-700"><td class="p-3 border-b border-gray-300 dark:border-gray-600 category-col">Summa</td>${players.map(p => `<td class="p-3 text-center border-b border-gray-300 dark:border-gray-600 ${p.id === currentPlayerId ? 'current-player-col' : ''}">${playerTotals[p.id].upperSum}</td>`).join('')}</tr>
|
|
<tr class="calculated-field bg-gray-100 dark:bg-gray-700"><td class="p-3 border-b border-gray-300 dark:border-gray-600 category-col">Bonus</td>${players.map(p => `<td class="p-3 text-center border-b border-gray-300 dark:border-gray-600 ${p.id === currentPlayerId ? 'current-player-col' : ''}">${playerTotals[p.id].upperBonus}</td>`).join('')}</tr>
|
|
<tr class="calculated-field bg-gray-100 dark:bg-gray-700"><td class="p-3 border-b-2 border-gray-400 dark:border-gray-500 category-col">Övre Summa</td>${players.map(p => `<td class="p-3 text-center border-b-2 border-gray-400 dark:border-gray-500 ${p.id === currentPlayerId ? 'current-player-col' : ''}">${playerTotals[p.id].upperTotal}</td>`).join('')}</tr>
|
|
`;
|
|
|
|
SCORE_CATEGORIES.lower.forEach(cat => bodyHtml += renderCategoryRow(cat));
|
|
|
|
bodyHtml += `<tr class="calculated-field bg-gray-100 dark:bg-gray-700"><td class="p-3 border-b-2 border-gray-400 dark:border-gray-500 category-col">Nedre Summa</td>${players.map(p => `<td class="p-3 text-center border-b-2 border-gray-400 dark:border-gray-500 ${p.id === currentPlayerId ? 'current-player-col' : ''}">${playerTotals[p.id].lowerTotal}</td>`).join('')}</tr>`;
|
|
|
|
if (isFinished) {
|
|
bodyHtml += `<tr class="grand-total bg-indigo-100 dark:bg-indigo-900"><td class="p-3 rounded-bl-lg category-col">Totalsumma</td>${players.map(p => `<td class="p-3 text-center ${p.id === currentPlayerId ? 'current-player-col' : ''}">${playerTotals[p.id].grandTotal}</td>`).join('')}</tr>`;
|
|
}
|
|
|
|
scorecardBody.innerHTML = bodyHtml;
|
|
}
|
|
|
|
async function loadPlayersForSelection() {
|
|
try {
|
|
const players = await getPlayers();
|
|
players.sort((a, b) => (b.wins || 0) - (a.wins || 0)); // Sort by wins
|
|
if (players.length === 0) {
|
|
playerListContainer.innerHTML = '<p class="text-gray-500 dark:text-gray-400">Inga spelare hittades. Lägg till en ovan.</p>';
|
|
} else {
|
|
playerListContainer.innerHTML = players.map(p => `
|
|
<label class="flex items-center justify-between p-2 bg-white dark:bg-gray-600 rounded-md border dark:border-gray-500 cursor-pointer hover:bg-indigo-50 dark:hover:bg-indigo-900">
|
|
<div>
|
|
<input type="checkbox" class="h-5 w-5 rounded text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-500 bg-gray-200 dark:bg-gray-700" data-player-id="${p.id}" data-player-name="${p.name}">
|
|
<span class="ml-3 text-gray-700 dark:text-gray-200">${p.name}</span>
|
|
</div>
|
|
<span class="text-sm font-semibold text-indigo-600 dark:text-indigo-400">${p.wins || 0} vinster</span>
|
|
</label>
|
|
`).join('');
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading players:", error);
|
|
playerListContainer.innerHTML = `<p class="text-red-500">Kunde inte ladda spelare: ${error.message}</p>`;
|
|
}
|
|
}
|
|
|
|
function updateStartGameButtonState() {
|
|
startGameBtn.disabled = selectedPlayersInOrder.length === 0;
|
|
}
|
|
|
|
// --- MODAL CONTROLS ---
|
|
function openStartGameModal() {
|
|
selectedPlayersInOrder = [];
|
|
loadPlayersForSelection();
|
|
startGameModal.classList.remove('hidden');
|
|
}
|
|
function closeStartGameModal() {
|
|
startGameModal.classList.add('hidden');
|
|
}
|
|
|
|
function openScoreModal(playerId, categoryId) {
|
|
const category = ALL_CATEGORIES.find(c => c.id === categoryId);
|
|
if (!category) return;
|
|
|
|
scoreModalTitle.textContent = `Ange poäng för ${category.name}`;
|
|
scoreModalOptions.innerHTML = ''; // Clear previous options
|
|
|
|
const createButton = (text, value) => {
|
|
const button = document.createElement('button');
|
|
button.textContent = text;
|
|
button.className = 'btn-secondary font-bold py-2 px-4 rounded-lg';
|
|
button.onclick = () => {
|
|
updateScore(playerId, categoryId, value);
|
|
closeScoreModal();
|
|
};
|
|
return button;
|
|
};
|
|
|
|
if (category.inputType === 'count') {
|
|
for (let i = 0; i <= category.maxCount; i++) {
|
|
scoreModalOptions.appendChild(createButton(`${i}`, i));
|
|
}
|
|
} else if (category.inputType === 'options') {
|
|
scoreModalOptions.appendChild(createButton('Stryk (0 p)', 0));
|
|
category.options.forEach(opt => {
|
|
scoreModalOptions.appendChild(createButton(`${opt} p`, opt));
|
|
});
|
|
} else if (category.fixed) {
|
|
scoreModalOptions.appendChild(createButton(`Stryk (0 p)`, 0));
|
|
scoreModalOptions.appendChild(createButton(`${category.fixed} p`, category.fixed));
|
|
} else { // Generic score input
|
|
const input = document.createElement('input');
|
|
input.type = 'number';
|
|
input.className = 'score-input w-32';
|
|
input.min = category.min || 0;
|
|
input.max = category.max || 30;
|
|
|
|
const saveBtn = document.createElement('button');
|
|
saveBtn.textContent = 'Spara';
|
|
saveBtn.className = 'btn-primary font-bold py-2 px-4 rounded-lg';
|
|
saveBtn.onclick = () => {
|
|
updateScore(playerId, categoryId, input.value);
|
|
closeScoreModal();
|
|
};
|
|
|
|
if (category.id !== 'chance') {
|
|
scoreModalOptions.appendChild(createButton('Stryk (0 p)', 0));
|
|
}
|
|
scoreModalOptions.appendChild(input);
|
|
scoreModalOptions.appendChild(saveBtn);
|
|
setTimeout(() => input.focus(), 10);
|
|
}
|
|
|
|
scoreSelectModal.classList.remove('hidden');
|
|
}
|
|
|
|
function closeScoreModal() {
|
|
scoreSelectModal.classList.add('hidden');
|
|
}
|
|
|
|
// --- THEME SWITCHER ---
|
|
function applyTheme(theme) {
|
|
if (theme === 'dark') {
|
|
document.documentElement.classList.add('dark');
|
|
themeIconLight.classList.add('hidden');
|
|
themeIconDark.classList.remove('hidden');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
themeIconLight.classList.remove('hidden');
|
|
themeIconDark.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
themeToggle.addEventListener('click', () => {
|
|
const isDark = document.documentElement.classList.toggle('dark');
|
|
const newTheme = isDark ? 'dark' : 'light';
|
|
localStorage.setItem('theme', newTheme);
|
|
applyTheme(newTheme);
|
|
});
|
|
|
|
// --- EVENT LISTENERS ---
|
|
function attachEventListeners() {
|
|
showStartModalBtn.addEventListener('click', openStartGameModal);
|
|
newGameBtn.addEventListener('click', openStartGameModal);
|
|
backToStartBtn.addEventListener('click', () => {
|
|
if (currentGameId) {
|
|
showConfirm(
|
|
() => { // onSave
|
|
resetToStartScreen();
|
|
},
|
|
async () => { // onDelete
|
|
const { error } = await supabase.from('games').delete().eq('id', currentGameId);
|
|
if (error) console.error("Error deleting game:", error);
|
|
resetToStartScreen();
|
|
}
|
|
);
|
|
} else {
|
|
resetToStartScreen();
|
|
}
|
|
});
|
|
endGameBtn.addEventListener('click', finishGame);
|
|
cancelStartGameBtn.addEventListener('click', closeStartGameModal);
|
|
scoreModalCancelBtn.addEventListener('click', closeScoreModal);
|
|
|
|
addPlayerBtn.addEventListener('click', addPlayer);
|
|
newPlayerNameInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') addPlayer();
|
|
});
|
|
|
|
playerListContainer.addEventListener('click', (e) => {
|
|
if (e.target.type === 'checkbox') {
|
|
const playerId = e.target.dataset.playerId;
|
|
const playerName = e.target.dataset.playerName;
|
|
|
|
if (e.target.checked) {
|
|
if (!selectedPlayersInOrder.find(p => p.id === playerId)) {
|
|
selectedPlayersInOrder.push({ id: playerId, name: playerName });
|
|
}
|
|
} else {
|
|
selectedPlayersInOrder = selectedPlayersInOrder.filter(p => p.id !== playerId);
|
|
}
|
|
updateStartGameButtonState();
|
|
}
|
|
});
|
|
|
|
startGameBtn.addEventListener('click', () => {
|
|
createNewGame(selectedPlayersInOrder);
|
|
});
|
|
|
|
scorecardBody.addEventListener('click', (e) => {
|
|
const cell = e.target.closest('.score-cell');
|
|
if (cell && !cell.classList.contains('score-cell-disabled')) {
|
|
const { playerId, categoryId } = cell.dataset;
|
|
openScoreModal(playerId, categoryId);
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- PASSWORD LOGIC ---
|
|
const CORRECT_PASSWORD = 'Yatzy'; // Ändra detta till önskat lösenord
|
|
|
|
function checkPassword() {
|
|
const enteredPassword = passwordInput.value;
|
|
if (enteredPassword === CORRECT_PASSWORD) {
|
|
sessionStorage.setItem('yatzy-authenticated', 'true');
|
|
passwordModal.classList.add('hidden');
|
|
appContainer.classList.remove('hidden');
|
|
|
|
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
|
document.body.innerHTML = '<div class="text-center p-8 text-red-500"><h1>Fel: Supabase-konfiguration saknas</h1><p>Du måste lägga till din Supabase URL och anon key i HTML-filen för att appen ska fungera.</p></div>';
|
|
return;
|
|
}
|
|
supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
attachEventListeners();
|
|
addDefaultPlayers().then(() => resetToStartScreen());
|
|
} else {
|
|
passwordError.textContent = 'Fel lösenord. Försök igen.';
|
|
passwordInput.value = '';
|
|
}
|
|
}
|
|
|
|
// --- APP ENTRY POINT ---
|
|
(function() {
|
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
applyTheme(savedTheme);
|
|
|
|
passwordSubmitBtn.addEventListener('click', checkPassword);
|
|
passwordInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
checkPassword();
|
|
}
|
|
});
|
|
|
|
if (sessionStorage.getItem('yatzy-authenticated') === 'true') {
|
|
passwordModal.classList.add('hidden');
|
|
appContainer.classList.remove('hidden');
|
|
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
|
document.body.innerHTML = '<div class="text-center p-8 text-red-500"><h1>Fel: Supabase-konfiguration saknas</h1><p>Du måste lägga till din Supabase URL och anon key i HTML-filen för att appen ska fungera.</p></div>';
|
|
return;
|
|
}
|
|
supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
attachEventListeners();
|
|
resetToStartScreen();
|
|
} else {
|
|
passwordModal.classList.remove('hidden');
|
|
appContainer.classList.add('hidden');
|
|
}
|
|
})();
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|