Yatzy/index.html
2025-12-17 17:43:55 +01:00

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>