Initial commit

This commit is contained in:
emanuel 2025-12-17 17:43:55 +01:00
commit 83f00df292
8 changed files with 2265 additions and 0 deletions

16
Work in progress/auth.js Executable file
View 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
View 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
View 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
View 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');
}
})();

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff