Initial commit
This commit is contained in:
commit
83f00df292
16
Work in progress/auth.js
Executable file
16
Work in progress/auth.js
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
export const CORRECT_PASSWORD = 'Yatzy';
|
||||||
|
|
||||||
|
export function checkPassword(passwordInput, passwordError, passwordModal, appContainer) {
|
||||||
|
const enteredPassword = passwordInput.value;
|
||||||
|
if (enteredPassword === CORRECT_PASSWORD) {
|
||||||
|
sessionStorage.setItem('yatzy-authenticated', 'true');
|
||||||
|
passwordModal.classList.add('hidden');
|
||||||
|
appContainer.classList.remove('hidden');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
passwordError.textContent = 'Fel lösenord. Försök igen.';
|
||||||
|
passwordInput.value = '';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
152
Work in progress/gameLogic.js
Executable file
152
Work in progress/gameLogic.js
Executable file
@ -0,0 +1,152 @@
|
|||||||
|
export 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' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ALL_CATEGORIES = [...SCORE_CATEGORIES.upper, ...SCORE_CATEGORIES.lower];
|
||||||
|
|
||||||
|
// Hjälpfunktion för att räkna förekomsten av varje tärningsvärde
|
||||||
|
function countDice(dice) {
|
||||||
|
const counts = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 };
|
||||||
|
dice.forEach(die => {
|
||||||
|
counts[die]++;
|
||||||
|
});
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hjälpfunktion för att beräkna poäng för en specifik kategori
|
||||||
|
function calculateCategoryScore(dice, categoryId) {
|
||||||
|
const counts = countDice(dice);
|
||||||
|
|
||||||
|
switch (categoryId) {
|
||||||
|
case 'ones':
|
||||||
|
return counts[1] * 1;
|
||||||
|
case 'twos':
|
||||||
|
return counts[2] * 2;
|
||||||
|
case 'threes':
|
||||||
|
return counts[3] * 3;
|
||||||
|
case 'fours':
|
||||||
|
return counts[4] * 4;
|
||||||
|
case 'fives':
|
||||||
|
return counts[5] * 5;
|
||||||
|
case 'sixes':
|
||||||
|
return counts[6] * 6;
|
||||||
|
case 'pair':
|
||||||
|
for (let i = 6; i >= 1; i--) {
|
||||||
|
if (counts[i] >= 2) return i * 2;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case 'twoPair':
|
||||||
|
const pairs = [];
|
||||||
|
for (let i = 6; i >= 1; i--) {
|
||||||
|
if (counts[i] >= 2) pairs.push(i * 2);
|
||||||
|
}
|
||||||
|
if (pairs.length >= 2) {
|
||||||
|
return pairs[0] + pairs[1];
|
||||||
|
} else if (pairs.length === 1) {
|
||||||
|
// Om det bara finns ett par, ta det högsta möjliga paret och lägg till högsta enkeltärning
|
||||||
|
const highestSingle = Math.max(...dice.filter(die => counts[die] === 1));
|
||||||
|
return pairs[0] + (highestSingle || 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case 'threeOfAKind':
|
||||||
|
for (let i = 6; i >= 1; i--) {
|
||||||
|
if (counts[i] >= 3) return i * 3;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case 'fourOfAKind':
|
||||||
|
for (let i = 6; i >= 1; i--) {
|
||||||
|
if (counts[i] >= 4) return i * 4;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case 'smallStraight':
|
||||||
|
const hasSmallStraight = dice.some(die => {
|
||||||
|
const has1234 = dice.includes(1) && dice.includes(2) && dice.includes(3) && dice.includes(4);
|
||||||
|
const has2345 = dice.includes(2) && dice.includes(3) && dice.includes(4) && dice.includes(5);
|
||||||
|
const has3456 = dice.includes(3) && dice.includes(4) && dice.includes(5) && dice.includes(6);
|
||||||
|
return has1234 || has2345 || has3456;
|
||||||
|
});
|
||||||
|
return hasSmallStraight ? 15 : 0;
|
||||||
|
case 'largeStraight':
|
||||||
|
const has12345 = dice.includes(1) && dice.includes(2) && dice.includes(3) && dice.includes(4) && dice.includes(5);
|
||||||
|
const has23456 = dice.includes(2) && dice.includes(3) && dice.includes(4) && dice.includes(5) && dice.includes(6);
|
||||||
|
return (has12345 || has23456) ? 20 : 0;
|
||||||
|
case 'fullHouse':
|
||||||
|
const hasThreeOfAKind = Object.values(counts).some(count => count >= 3);
|
||||||
|
const hasPair = Object.values(counts).some(count => count >= 2);
|
||||||
|
return (hasThreeOfAKind && hasPair) ? dice.reduce((sum, die) => sum + die, 0) : 0;
|
||||||
|
case 'chance':
|
||||||
|
return dice.reduce((sum, die) => sum + die, 0);
|
||||||
|
case 'yatzee':
|
||||||
|
return Object.values(counts).some(count => count === 5) ? 50 : 0;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att beräkna möjliga poäng för en kategori
|
||||||
|
export function calculatePossibleScores(gameData, playerId, categoryId) {
|
||||||
|
// Hämta de senast kastade tärningarna för spelaren (detta måste implementeras i main.js)
|
||||||
|
const currentDice = gameData.currentDice || [1, 1, 1, 1, 1]; // Exempelvärden, ersätt med riktiga tärningar
|
||||||
|
|
||||||
|
const category = ALL_CATEGORIES.find(cat => cat.id === categoryId);
|
||||||
|
if (!category) return [];
|
||||||
|
|
||||||
|
const score = calculateCategoryScore(currentDice, categoryId);
|
||||||
|
|
||||||
|
if (category.inputType === 'count') {
|
||||||
|
return [score];
|
||||||
|
} else if (category.inputType === 'options') {
|
||||||
|
return category.options.includes(score) ? [score] : [0];
|
||||||
|
} else if (category.inputType === 'score') {
|
||||||
|
if (category.fixed) {
|
||||||
|
return [category.fixed];
|
||||||
|
} else if (category.min !== undefined && category.max !== undefined) {
|
||||||
|
return score >= category.min && score <= category.max ? [score] : [0];
|
||||||
|
} else {
|
||||||
|
return [score];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [score];
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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 && gameData.scores[p.id]?.[cat.id] !== undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
183
Work in progress/index.html
Executable file
183
Work in progress/index.html
Executable file
@ -0,0 +1,183 @@
|
|||||||
|
<!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">
|
||||||
|
|
||||||
|
<!-- Länka till CSS -->
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
|
||||||
|
<!-- Länka till Tailwind CSS -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Länka till Google Fonts -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="transition-colors duration-300">
|
||||||
|
<!-- Password Modal -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- App Container -->
|
||||||
|
<div id="app" class="container mx-auto p-2 sm:p-4 max-w-7xl hidden">
|
||||||
|
<!-- Header -->
|
||||||
|
<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 Container -->
|
||||||
|
<div id="game-container" class="hidden">
|
||||||
|
<!-- Game Controls -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Scorecard -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Länka till JavaScript-filer -->
|
||||||
|
<script type="module" src="supabase.js"></script>
|
||||||
|
<script type="module" src="gameLogic.js"></script>
|
||||||
|
<script type="module" src="ui.js"></script>
|
||||||
|
<script type="module" src="auth.js"></script>
|
||||||
|
<script type="module" src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
534
Work in progress/main.js
Executable file
534
Work in progress/main.js
Executable file
@ -0,0 +1,534 @@
|
|||||||
|
import { supabase, getPlayers, addPlayer, getRecentGames, getHighScores } from './supabase.js';
|
||||||
|
import { SCORE_CATEGORIES, ALL_CATEGORIES, calculateTotals, isGameComplete, calculatePossibleScores } from './gameLogic.js';
|
||||||
|
import { renderScorecard, renderPlayerLeaderboard, renderRecentGames, renderHighScores } from './ui.js';
|
||||||
|
import { checkPassword, CORRECT_PASSWORD } from './auth.js';
|
||||||
|
|
||||||
|
// DOM-element
|
||||||
|
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');
|
||||||
|
const diceContainer = document.getElementById('dice-container');
|
||||||
|
const rollDiceBtn = document.getElementById('roll-dice-btn');
|
||||||
|
|
||||||
|
// Global state
|
||||||
|
let currentGameId = null;
|
||||||
|
let currentGameData = null;
|
||||||
|
let gameUnsubscribe = null;
|
||||||
|
let selectedPlayersInOrder = [];
|
||||||
|
let currentDice = [1, 1, 1, 1, 1]; // Standardvärden för tärningar
|
||||||
|
let rollsLeft = 3; // Antal kast kvar
|
||||||
|
const appId = 'yatzy-app';
|
||||||
|
|
||||||
|
// Funktion för att visa meddelanden
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att visa bekräftelsedialog
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att slå tärningar
|
||||||
|
function rollDice() {
|
||||||
|
if (rollsLeft <= 0) {
|
||||||
|
showAlert("Du har inga kast kvar!", "Inga kast kvar");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockedDice = Array.from(document.querySelectorAll('.dice.locked'));
|
||||||
|
const lockedIndices = lockedDice.map(die => parseInt(die.dataset.index));
|
||||||
|
|
||||||
|
currentDice = currentDice.map((die, index) => {
|
||||||
|
if (lockedIndices.includes(index)) {
|
||||||
|
return die;
|
||||||
|
} else {
|
||||||
|
return Math.floor(Math.random() * 6) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rollsLeft--;
|
||||||
|
updateDiceUI();
|
||||||
|
updateRollButton();
|
||||||
|
|
||||||
|
console.log(`Tärningar efter kast: ${currentDice}, Kast kvar: ${rollsLeft}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att uppdatera tärnings-UI
|
||||||
|
function updateDiceUI() {
|
||||||
|
diceContainer.innerHTML = currentDice.map((die, index) => `
|
||||||
|
<div class="dice" data-index="${index}">
|
||||||
|
<div class="dice-face">${die}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Lägg till event listeners för att låsa/upplåsa tärningar
|
||||||
|
document.querySelectorAll('.dice').forEach(die => {
|
||||||
|
die.addEventListener('click', () => {
|
||||||
|
die.classList.toggle('locked');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att uppdatera kast-knappen
|
||||||
|
function updateRollButton() {
|
||||||
|
rollDiceBtn.textContent = `Slå tärningar (${rollsLeft} kvar)`;
|
||||||
|
if (rollsLeft <= 0) {
|
||||||
|
rollDiceBtn.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att återställa tärningar
|
||||||
|
function resetDice() {
|
||||||
|
currentDice = [1, 1, 1, 1, 1];
|
||||||
|
rollsLeft = 3;
|
||||||
|
updateDiceUI();
|
||||||
|
updateRollButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att lägga till spelare
|
||||||
|
async function addPlayerHandler() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addPlayer(name, initialWins);
|
||||||
|
newPlayerNameInput.value = '';
|
||||||
|
newPlayerWinsInput.value = '';
|
||||||
|
loadPlayersForSelection();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(`Kunde inte lägga till spelare: ${error.message}`, "Databasfel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att ladda spelare
|
||||||
|
async function loadPlayersForSelection() {
|
||||||
|
try {
|
||||||
|
const players = await getPlayers();
|
||||||
|
players.sort((a, b) => (b.wins || 0) - (a.wins || 0));
|
||||||
|
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('');
|
||||||
|
|
||||||
|
// Lägg till event listeners för checkboxarna
|
||||||
|
const checkboxes = playerListContainer.querySelectorAll('input[type="checkbox"]');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', (e) => {
|
||||||
|
const playerId = e.target.dataset.playerId;
|
||||||
|
const playerName = e.target.dataset.playerName;
|
||||||
|
|
||||||
|
if (e.target.checked) {
|
||||||
|
if (!selectedPlayersInOrder.some(p => p.id === playerId)) {
|
||||||
|
selectedPlayersInOrder.push({ id: playerId, name: playerName });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedPlayersInOrder = selectedPlayersInOrder.filter(p => p.id !== playerId);
|
||||||
|
}
|
||||||
|
updateStartGameButtonState();
|
||||||
|
console.log("Valda spelare:", selectedPlayersInOrder);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading players:", error);
|
||||||
|
playerListContainer.innerHTML = `<p class="text-red-500">Kunde inte ladda spelare: ${error.message}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att uppdatera spelknappens status
|
||||||
|
function updateStartGameButtonState() {
|
||||||
|
console.log("Uppdaterar spelknappens status. Valda spelare:", selectedPlayersInOrder);
|
||||||
|
startGameBtn.disabled = selectedPlayersInOrder.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att öppna/stänga modaler
|
||||||
|
function openStartGameModal() {
|
||||||
|
selectedPlayersInOrder = [];
|
||||||
|
loadPlayersForSelection();
|
||||||
|
startGameModal.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeStartGameModal() {
|
||||||
|
startGameModal.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att applicera tema
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att återställa till startsidan
|
||||||
|
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');
|
||||||
|
await renderAllLeaderboards();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att rendera alla topplistor
|
||||||
|
async function renderAllLeaderboards() {
|
||||||
|
await Promise.all([
|
||||||
|
renderPlayerLeaderboard(),
|
||||||
|
renderRecentGames(),
|
||||||
|
renderHighScores()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att lägga till standardspelare
|
||||||
|
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) {
|
||||||
|
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.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att skapa ett nytt spel
|
||||||
|
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,
|
||||||
|
currentDice: currentDice,
|
||||||
|
rollsLeft: rollsLeft
|
||||||
|
}])
|
||||||
|
.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att sätta det aktuella spelet
|
||||||
|
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;
|
||||||
|
currentDice = payload.new.currentDice || [1, 1, 1, 1, 1];
|
||||||
|
rollsLeft = payload.new.rollsLeft || 3;
|
||||||
|
renderGame(currentGameData);
|
||||||
|
})
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
// Initial fetch
|
||||||
|
supabase.from('games').select('*').eq('id', gameId).single().then(({ data, error }) => {
|
||||||
|
if (data) {
|
||||||
|
currentGameData = data;
|
||||||
|
currentDice = data.currentDice || [1, 1, 1, 1, 1];
|
||||||
|
rollsLeft = data.rollsLeft || 3;
|
||||||
|
renderGame(currentGameData);
|
||||||
|
} else {
|
||||||
|
resetToStartScreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resetToStartScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att rendera spelet
|
||||||
|
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, scorecardHead, scorecardBody);
|
||||||
|
updateDiceUI();
|
||||||
|
updateRollButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att visa modalen för att välja poäng
|
||||||
|
function showScoreSelectModal(playerId, categoryId, possibleScores) {
|
||||||
|
console.log(`Visar poängval för spelare ${playerId}, kategori ${categoryId}, möjliga poäng:`, possibleScores);
|
||||||
|
|
||||||
|
const category = ALL_CATEGORIES.find(cat => cat.id === categoryId);
|
||||||
|
scoreModalTitle.textContent = `Välj poäng för ${category.name}`;
|
||||||
|
|
||||||
|
scoreModalOptions.innerHTML = possibleScores.map(score => `
|
||||||
|
<button class="score-option btn-secondary font-bold py-2 px-4 rounded-md m-1"
|
||||||
|
data-score="${score}"
|
||||||
|
data-player-id="${playerId}"
|
||||||
|
data-category-id="${categoryId}">
|
||||||
|
${score}
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
scoreSelectModal.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Lägg till event listeners för poängalternativen
|
||||||
|
const scoreOptionButtons = document.querySelectorAll('.score-option');
|
||||||
|
scoreOptionButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', async (e) => {
|
||||||
|
const score = e.currentTarget.dataset.score;
|
||||||
|
const playerId = e.currentTarget.dataset.playerId;
|
||||||
|
const categoryId = e.currentTarget.dataset.categoryId;
|
||||||
|
|
||||||
|
console.log(`Poäng vald: Spelare ${playerId}, Kategori ${categoryId}, Poäng ${score}`);
|
||||||
|
|
||||||
|
// Uppdatera poängen i databasen
|
||||||
|
await updateScore(playerId, categoryId, parseInt(score));
|
||||||
|
|
||||||
|
scoreSelectModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att uppdatera poängen i databasen
|
||||||
|
async function updateScore(playerId, categoryId, score) {
|
||||||
|
try {
|
||||||
|
const updatedScores = { ...currentGameData.scores };
|
||||||
|
updatedScores[playerId][categoryId] = score;
|
||||||
|
|
||||||
|
const nextPlayerIndex = isGameComplete(currentGameData) ?
|
||||||
|
currentGameData.currentPlayerIndex :
|
||||||
|
(currentGameData.currentPlayerIndex + 1) % currentGameData.players.length;
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('games')
|
||||||
|
.update({
|
||||||
|
scores: updatedScores,
|
||||||
|
currentPlayerIndex: nextPlayerIndex,
|
||||||
|
rollsLeft: 3,
|
||||||
|
currentDice: [1, 1, 1, 1, 1]
|
||||||
|
})
|
||||||
|
.eq('id', currentGameId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Fel vid uppdatering av poäng:", error);
|
||||||
|
showAlert("Kunde inte uppdatera poängen. Försök igen.", "Fel");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Oväntat fel vid uppdatering av poäng:", error);
|
||||||
|
showAlert("Ett oväntat fel uppstod. Försök igen.", "Fel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att hantera klick på poängfälten
|
||||||
|
function attachScorecardEventListeners() {
|
||||||
|
const scoreCells = document.querySelectorAll('.score-cell:not(.filled):not(.score-cell-disabled)');
|
||||||
|
scoreCells.forEach(cell => {
|
||||||
|
cell.addEventListener('click', async (e) => {
|
||||||
|
const playerId = e.currentTarget.dataset.playerId;
|
||||||
|
const currentPlayerId = currentGameData.players[currentGameData.currentPlayerIndex].id;
|
||||||
|
|
||||||
|
if (playerId !== currentPlayerId) {
|
||||||
|
showAlert("Det är inte din tur!", "Fel spelare");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryId = e.currentTarget.dataset.categoryId;
|
||||||
|
console.log(`Poängfält klickat: Spelare ${playerId}, Kategori ${categoryId}`);
|
||||||
|
|
||||||
|
// Hämta möjliga poäng för den valda kategorin
|
||||||
|
const possibleScores = calculatePossibleScores({...currentGameData, currentDice}, playerId, categoryId);
|
||||||
|
|
||||||
|
// Visa modal för att välja poäng
|
||||||
|
showScoreSelectModal(playerId, categoryId, possibleScores);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att koppla event listeners
|
||||||
|
function attachEventListeners() {
|
||||||
|
if (showStartModalBtn) showStartModalBtn.addEventListener('click', openStartGameModal);
|
||||||
|
if (newGameBtn) newGameBtn.addEventListener('click', openStartGameModal);
|
||||||
|
if (cancelStartGameBtn) cancelStartGameBtn.addEventListener('click', closeStartGameModal);
|
||||||
|
if (addPlayerBtn) addPlayerBtn.addEventListener('click', addPlayerHandler);
|
||||||
|
if (scoreModalCancelBtn) scoreModalCancelBtn.addEventListener('click', () => scoreSelectModal.classList.add('hidden'));
|
||||||
|
if (rollDiceBtn) rollDiceBtn.addEventListener('click', rollDice);
|
||||||
|
|
||||||
|
if (newPlayerNameInput) {
|
||||||
|
newPlayerNameInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') addPlayerHandler();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', () => {
|
||||||
|
const isDark = document.documentElement.classList.toggle('dark');
|
||||||
|
const newTheme = isDark ? 'dark' : 'light';
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
applyTheme(newTheme);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordSubmitBtn) {
|
||||||
|
passwordSubmitBtn.addEventListener('click', () => {
|
||||||
|
checkPassword(passwordInput, passwordError, passwordModal, appContainer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordInput) {
|
||||||
|
passwordInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
checkPassword(passwordInput, passwordError, passwordModal, appContainer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startGameBtn) {
|
||||||
|
startGameBtn.addEventListener('click', () => {
|
||||||
|
console.log("Starta spel-knappen klickad! Valda spelare:", selectedPlayersInOrder);
|
||||||
|
createNewGame(selectedPlayersInOrder);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att starta appen
|
||||||
|
(function() {
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
applyTheme(savedTheme);
|
||||||
|
|
||||||
|
attachEventListeners();
|
||||||
|
|
||||||
|
if (sessionStorage.getItem('yatzy-authenticated') === 'true') {
|
||||||
|
passwordModal.classList.add('hidden');
|
||||||
|
appContainer.classList.remove('hidden');
|
||||||
|
addDefaultPlayers().then(() => resetToStartScreen());
|
||||||
|
} else {
|
||||||
|
passwordModal.classList.remove('hidden');
|
||||||
|
appContainer.classList.add('hidden');
|
||||||
|
}
|
||||||
|
})();
|
||||||
91
Work in progress/styles.css
Normal file
91
Work in progress/styles.css
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
: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;
|
||||||
|
--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;
|
||||||
|
--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);
|
||||||
|
}
|
||||||
64
Work in progress/supabase.js
Executable file
64
Work in progress/supabase.js
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
||||||
|
|
||||||
|
const SUPABASE_URL = 'https://db.tvalen.lol';
|
||||||
|
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzUzOTk5MjAwLCJleHAiOjE5MTE3NjU2MDB9.rqUu8VzDYqPs28uxZUCqmK55abGvB_zMdS-LCb8KA8c';
|
||||||
|
|
||||||
|
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||||
|
realtime: {
|
||||||
|
params: {
|
||||||
|
eventsPerSecond: 10,
|
||||||
|
},
|
||||||
|
endpoint: 'realtime/v1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Funktion för att hämta spelare
|
||||||
|
export async function getPlayers() {
|
||||||
|
const { data, error } = await supabase.from('players').select('*');
|
||||||
|
if (error) {
|
||||||
|
console.error("Error fetching players:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att lägga till en spelare
|
||||||
|
export async function addPlayer(name, wins = 0) {
|
||||||
|
const { error } = await supabase.from('players').insert([{ name, wins }]);
|
||||||
|
if (error) {
|
||||||
|
console.error("Error adding player:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att hämta de senaste spelen
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att hämta de högsta poängen
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
158
Work in progress/ui.js
Executable file
158
Work in progress/ui.js
Executable file
@ -0,0 +1,158 @@
|
|||||||
|
import { SCORE_CATEGORIES, ALL_CATEGORIES, calculateTotals, isGameComplete } from './gameLogic.js';
|
||||||
|
import { getPlayers, getRecentGames, getHighScores } from './supabase.js';
|
||||||
|
|
||||||
|
// Funktion för att rendera poängkortet
|
||||||
|
export function renderScorecard(gameData, scorecardHead, scorecardBody) {
|
||||||
|
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;
|
||||||
|
// Anropa funktionen för att koppla event listeners efter att poängkortet har renderats
|
||||||
|
if (typeof attachScorecardEventListeners === 'function') {
|
||||||
|
attachScorecardEventListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att rendera topplistan för spelare
|
||||||
|
export 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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att rendera topplistan för högsta poäng
|
||||||
|
export 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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion för att rendera de senaste spelen
|
||||||
|
export 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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1067
index.html
Executable file
1067
index.html
Executable file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user