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