diff --git a/android-chrome-192x192.png b/android-chrome-192x192.png new file mode 100644 index 0000000..7fabb38 Binary files /dev/null and b/android-chrome-192x192.png differ diff --git a/android-chrome-512x512.png b/android-chrome-512x512.png new file mode 100644 index 0000000..6c47bef Binary files /dev/null and b/android-chrome-512x512.png differ diff --git a/apple-touch-icon.png b/apple-touch-icon.png new file mode 100644 index 0000000..6534042 Binary files /dev/null and b/apple-touch-icon.png differ diff --git a/apps.js b/apps.js new file mode 100644 index 0000000..6da3b43 --- /dev/null +++ b/apps.js @@ -0,0 +1,603 @@ +// Import the audio manager +import audioManager from './audio.js'; + +// Initialize variables +let players = []; +let currentPlayerIndex = 0; +let gameState = 'setup'; // setup, running, paused, over +let carouselPosition = 0; +let startX = 0; +let currentX = 0; + +// DOM Elements +const carousel = document.getElementById('carousel'); +const gameButton = document.getElementById('gameButton'); +const setupButton = document.getElementById('setupButton'); +const addPlayerButton = document.getElementById('addPlayerButton'); +const resetButton = document.getElementById('resetButton'); +const playerModal = document.getElementById('playerModal'); +const resetModal = document.getElementById('resetModal'); +const playerForm = document.getElementById('playerForm'); +const cancelButton = document.getElementById('cancelButton'); +const deletePlayerButton = document.getElementById('deletePlayerButton'); +const resetCancelButton = document.getElementById('resetCancelButton'); +const resetConfirmButton = document.getElementById('resetConfirmButton'); +const playerImage = document.getElementById('playerImage'); +const imagePreview = document.getElementById('imagePreview'); +const playerTimeContainer = document.getElementById('playerTimeContainer'); +const remainingTimeContainer = document.getElementById('remainingTimeContainer'); +const playerRemainingTime = document.getElementById('playerRemainingTime'); + +// Add sound toggle button +const createSoundToggleButton = () => { + const headerButtons = document.querySelector('.header-buttons'); + const soundButton = document.createElement('button'); + soundButton.id = 'soundToggleButton'; + soundButton.className = 'header-button'; + soundButton.title = 'Toggle Sound'; + soundButton.innerHTML = ''; + + soundButton.addEventListener('click', () => { + const isMuted = audioManager.toggleMute(); + soundButton.innerHTML = isMuted ? + '' : + ''; + + // Play feedback sound if unmuting + if (!isMuted) { + audioManager.play('buttonClick'); + } + }); + + headerButtons.prepend(soundButton); + + // Set initial icon state based on mute setting + if (audioManager.muted) { + soundButton.innerHTML = ''; + } +}; + +// Create the sound toggle button when page loads +createSoundToggleButton(); + +// Load data from localStorage or use defaults +function loadData() { + const savedData = localStorage.getItem('chessTimerData'); + if (savedData) { + const parsedData = JSON.parse(savedData); + players = parsedData.players; + gameState = parsedData.gameState || 'setup'; + currentPlayerIndex = parsedData.currentPlayerIndex || 0; + } else { + // Default players if no saved data + players = [ + { id: 1, name: 'Player 1', timeInSeconds: 300, remainingTime: 300, image: null }, + { id: 2, name: 'Player 2', timeInSeconds: 300, remainingTime: 300, image: null } + ]; + saveData(); + } + renderPlayers(); + updateGameButton(); +} + +// Save data to localStorage +function saveData() { + const dataToSave = { + players, + gameState, + currentPlayerIndex + }; + localStorage.setItem('chessTimerData', JSON.stringify(dataToSave)); +} + +// Render players to carousel +function renderPlayers() { + carousel.innerHTML = ''; + + players.forEach((player, index) => { + const card = document.createElement('div'); + card.className = `player-card ${index === currentPlayerIndex ? 'active-player' : 'inactive-player'}`; + + const minutes = Math.floor(player.remainingTime / 60); + const seconds = player.remainingTime % 60; + const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + + // Create timer element with appropriate classes + const timerClasses = []; + + // Add timer-active class if this is current player and game is running + if (index === currentPlayerIndex && gameState === 'running') { + timerClasses.push('timer-active'); + } + + // Add timer-finished class if player has no time left + if (player.remainingTime <= 0) { + timerClasses.push('timer-finished'); + } + + card.innerHTML = ` +
+ ${player.image ? `${player.name}` : ''} +
+
${player.name}
+
${timeString}
+ `; + + carousel.appendChild(card); + }); + + // Update carousel position + carousel.style.transform = `translateX(${-100 * currentPlayerIndex}%)`; +} + +// Update game button text based on game state +function updateGameButton() { + switch (gameState) { + case 'setup': + gameButton.textContent = 'Start Game'; + break; + case 'running': + gameButton.textContent = 'Pause Game'; + break; + case 'paused': + gameButton.textContent = 'Resume Game'; + break; + case 'over': + gameButton.textContent = 'Game Over'; + break; + } +} + +// Handle game button click +gameButton.addEventListener('click', () => { + // Play button click sound + audioManager.play('buttonClick'); + + if (players.length < 2) { + alert('You need at least 2 players to start a game.'); + return; + } + + switch (gameState) { + case 'setup': + gameState = 'running'; + audioManager.play('gameStart'); + startTimer(); + break; + case 'running': + gameState = 'paused'; + audioManager.play('gamePause'); + stopTimer(); + break; + case 'paused': + gameState = 'running'; + audioManager.play('gameResume'); + startTimer(); + break; + case 'over': + // Reset timers and start new game + players.forEach(player => { + player.remainingTime = player.timeInSeconds; + }); + gameState = 'setup'; + // No specific sound for this state change + break; + } + + updateGameButton(); + renderPlayers(); // Make sure to re-render after state change + saveData(); +}); + +// Timer variables +let timerInterval = null; + +// Check if all timers have reached zero +function areAllTimersFinished() { + return players.every(player => player.remainingTime <= 0); +} + +// Find a player who still has time left +function findNextPlayerWithTime() { + const startIndex = (currentPlayerIndex + 1) % players.length; + let index = startIndex; + + do { + if (players[index].remainingTime > 0) { + return index; + } + index = (index + 1) % players.length; + } while (index !== startIndex); + + return -1; // No player has time left +} + +// Start the timer for current player +function startTimer() { + if (timerInterval) clearInterval(timerInterval); + + // Stop any ongoing sounds when starting timer + audioManager.stopAllSounds(); + + // Immediately render to show the active timer effect + renderPlayers(); + + timerInterval = setInterval(() => { + const currentPlayer = players[currentPlayerIndex]; + + // Only decrease time if the current player has time left + if (currentPlayer.remainingTime > 0) { + currentPlayer.remainingTime--; + + // Play appropriate timer sounds based on remaining time + audioManager.playTimerSound(currentPlayer.remainingTime); + } + + // Check if current player's time is up + if (currentPlayer.remainingTime <= 0) { + currentPlayer.remainingTime = 0; + + // Play time expired sound + audioManager.playTimerExpired(); + + // Check if all timers are at zero + if (areAllTimersFinished()) { + gameState = 'over'; + audioManager.play('gameOver'); + updateGameButton(); + stopTimer(); + } else { + // Find the next player who still has time + const nextPlayerIndex = findNextPlayerWithTime(); + if (nextPlayerIndex !== -1) { + currentPlayerIndex = nextPlayerIndex; + // Play switch player sound + audioManager.play('playerSwitch'); + } + } + } + + renderPlayers(); + saveData(); + }, 1000); +} + +// Stop the timer +function stopTimer() { + clearInterval(timerInterval); + timerInterval = null; + renderPlayers(); // Make sure to re-render after stopping timer +} + +// Carousel touch events +let isDragging = false; + +carousel.addEventListener('touchstart', (e) => { + startX = e.touches[0].clientX; + currentX = startX; + isDragging = true; +}); + +carousel.addEventListener('touchmove', (e) => { + if (!isDragging) return; + + currentX = e.touches[0].clientX; + const diff = currentX - startX; + const currentTranslate = -100 * currentPlayerIndex + (diff / carousel.offsetWidth * 100); + + carousel.style.transform = `translateX(${currentTranslate}%)`; +}); + +carousel.addEventListener('touchend', (e) => { + if (!isDragging) return; + + isDragging = false; + const diff = currentX - startX; + + // If dragged more than 10% of width, change player + if (Math.abs(diff) > carousel.offsetWidth * 0.1) { + const previousIndex = currentPlayerIndex; + + // Only change players that have remaining time during a running game + if (gameState === 'running') { + let newIndex; + if (diff < 0) { + // Try to go to next player with time + newIndex = findNextPlayerWithTimeCircular(currentPlayerIndex, 1); + } else { + // Try to go to previous player with time + newIndex = findNextPlayerWithTimeCircular(currentPlayerIndex, -1); + } + + if (newIndex !== -1) { + currentPlayerIndex = newIndex; + } + } else { + // Normal navigation when game not running + if (diff < 0) { + currentPlayerIndex = (currentPlayerIndex + 1) % players.length; + } else if (diff > 0) { + currentPlayerIndex = (currentPlayerIndex - 1 + players.length) % players.length; + } + } + + // Play player switch sound if player actually changed + if (previousIndex !== currentPlayerIndex) { + audioManager.play('playerSwitch'); + } + } + + // Reset carousel to proper position + carousel.style.transform = `translateX(${-100 * currentPlayerIndex}%)`; + renderPlayers(); + saveData(); +}); + +// Find next player with time in specified direction +function findNextPlayerWithTimeCircular(startIndex, direction) { + let index = startIndex; + + for (let i = 0; i < players.length; i++) { + index = (index + direction + players.length) % players.length; + if (players[index].remainingTime > 0) { + return index; + } + } + + return -1; // No player has time left +} + +// Setup button +setupButton.addEventListener('click', () => { + audioManager.play('buttonClick'); + + if (gameState === 'running') { + alert('Please pause the game before editing players.'); + return; + } + + const currentPlayer = players[currentPlayerIndex]; + document.getElementById('modalTitle').textContent = 'Edit Player'; + document.getElementById('playerName').value = currentPlayer.name; + document.getElementById('playerTime').value = currentPlayer.timeInSeconds / 60; + + // Show or hide remaining time edit field based on game state + if (gameState === 'paused' || gameState === 'over') { + remainingTimeContainer.style.display = 'block'; + const minutes = Math.floor(currentPlayer.remainingTime / 60); + const seconds = currentPlayer.remainingTime % 60; + playerRemainingTime.value = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } else { + remainingTimeContainer.style.display = 'none'; + } + + if (currentPlayer.image) { + imagePreview.innerHTML = `${currentPlayer.name}`; + } else { + imagePreview.innerHTML = ''; + } + + playerModal.classList.add('active'); + deletePlayerButton.style.display = 'block'; + + // Play modal open sound + audioManager.play('modalOpen'); +}); + +// Add player button +addPlayerButton.addEventListener('click', () => { + audioManager.play('buttonClick'); + + if (gameState === 'running') { + alert('Please pause the game before adding players.'); + return; + } + + document.getElementById('modalTitle').textContent = 'Add New Player'; + document.getElementById('playerName').value = `Player ${players.length + 1}`; + document.getElementById('playerTime').value = 5; + remainingTimeContainer.style.display = 'none'; + imagePreview.innerHTML = ''; + + playerModal.classList.add('active'); + deletePlayerButton.style.display = 'none'; + + // Play modal open sound + audioManager.play('modalOpen'); +}); + +// Reset button +resetButton.addEventListener('click', () => { + audioManager.play('buttonClick'); + + if (gameState === 'running') { + alert('Please pause the game before resetting.'); + return; + } + + resetModal.classList.add('active'); + audioManager.play('modalOpen'); +}); + +// Cancel reset +resetCancelButton.addEventListener('click', () => { + audioManager.play('buttonClick'); + resetModal.classList.remove('active'); + audioManager.play('modalClose'); +}); + +// Confirm reset +resetConfirmButton.addEventListener('click', () => { + audioManager.play('buttonClick'); + + players = [ + { id: 1, name: 'Player 1', timeInSeconds: 300, remainingTime: 300, image: null }, + { id: 2, name: 'Player 2', timeInSeconds: 300, remainingTime: 300, image: null } + ]; + gameState = 'setup'; + currentPlayerIndex = 0; + + renderPlayers(); + updateGameButton(); + saveData(); + resetModal.classList.remove('active'); + + // Play reset sound (use game over as it's a complete reset) + audioManager.play('gameOver'); +}); + +// Player image upload preview +playerImage.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + imagePreview.innerHTML = `Player`; + }; + reader.readAsDataURL(file); + } +}); + +// Parse time string (MM:SS) to seconds +function parseTimeString(timeString) { + const [minutes, seconds] = timeString.split(':').map(part => parseInt(part, 10)); + return (minutes * 60) + seconds; +} + +// Player form submit +playerForm.addEventListener('submit', (e) => { + e.preventDefault(); + + const name = document.getElementById('playerName').value; + const timeInMinutes = parseInt(document.getElementById('playerTime').value); + const timeInSeconds = timeInMinutes * 60; + + // Get remaining time if it's visible + let remainingTimeValue = timeInSeconds; + if (remainingTimeContainer.style.display === 'block') { + const remainingTimeString = playerRemainingTime.value; + // Validate the time format + if (!/^\d{2}:\d{2}$/.test(remainingTimeString)) { + alert('Please enter time in MM:SS format (e.g., 05:30)'); + return; + } + remainingTimeValue = parseTimeString(remainingTimeString); + } + + // Get image if uploaded + const imageFile = document.getElementById('playerImage').files[0]; + let playerImageData = null; + + // Function to proceed with saving player + const savePlayer = () => { + const isNewPlayer = document.getElementById('modalTitle').textContent === 'Add New Player'; + + if (isNewPlayer) { + // Add new player + const newId = Date.now(); + players.push({ + id: newId, + name: name, + timeInSeconds: timeInSeconds, + remainingTime: timeInSeconds, + image: playerImageData + }); + currentPlayerIndex = players.length - 1; + + // Play player added sound + audioManager.play('playerAdded'); + } else { + // Update existing player + const player = players[currentPlayerIndex]; + player.name = name; + player.timeInSeconds = timeInSeconds; + + // Update remaining time based on game state and form input + if (gameState === 'setup') { + player.remainingTime = timeInSeconds; + } else if (gameState === 'paused' || gameState === 'over') { + player.remainingTime = remainingTimeValue; + } + + if (playerImageData !== null) { + player.image = playerImageData; + } + + // Play player edited sound + audioManager.play('playerEdited'); + } + + renderPlayers(); + saveData(); + playerModal.classList.remove('active'); + document.getElementById('playerImage').value = ''; + + // Also play modal close sound + audioManager.play('modalClose'); + }; + + // Process image if there is one + if (imageFile) { + const reader = new FileReader(); + reader.onload = (event) => { + playerImageData = event.target.result; + savePlayer(); + }; + reader.readAsDataURL(imageFile); + } else { + // Current player's existing image or null + if (document.getElementById('modalTitle').textContent !== 'Add New Player') { + playerImageData = players[currentPlayerIndex].image; + } + savePlayer(); + } +}); + +// Cancel button +cancelButton.addEventListener('click', () => { + audioManager.play('buttonClick'); + playerModal.classList.remove('active'); + document.getElementById('playerImage').value = ''; + audioManager.play('modalClose'); +}); + +// Delete player button +deletePlayerButton.addEventListener('click', () => { + audioManager.play('buttonClick'); + + if (players.length <= 2) { + alert('You need at least 2 players. Add another player before deleting this one.'); + return; + } + + players.splice(currentPlayerIndex, 1); + + if (currentPlayerIndex >= players.length) { + currentPlayerIndex = players.length - 1; + } + + renderPlayers(); + saveData(); + playerModal.classList.remove('active'); + + // Play player deleted sound + audioManager.play('playerDeleted'); + // Also play modal close sound + audioManager.play('modalClose'); +}); + +// Service Worker Registration +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js') + .then(registration => { + console.log('ServiceWorker registered: ', registration); + }) + .catch(error => { + console.log('ServiceWorker registration failed: ', error); + }); + }); +} + +// Initialize the app +loadData(); \ No newline at end of file diff --git a/audio.js b/audio.js new file mode 100644 index 0000000..190ebe6 --- /dev/null +++ b/audio.js @@ -0,0 +1,315 @@ +// Audio Manager using Web Audio API +const audioManager = { + audioContext: null, + muted: false, + sounds: {}, + lowTimeThreshold: 10, // Seconds threshold for low time warning + lastTickTime: 0, // Track when we started continuous ticking + tickFadeoutTime: 3, // Seconds after which tick sound fades out + + // Initialize the audio context + init() { + try { + // Create AudioContext (with fallback for older browsers) + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + + // Check for saved mute preference + const savedMute = localStorage.getItem('chessTimerMuted'); + this.muted = savedMute === 'true'; + + // Create all the sounds + this.createSounds(); + + console.log('Web Audio API initialized successfully'); + return true; + } catch (error) { + console.error('Web Audio API initialization failed:', error); + return false; + } + }, + + // Create all the sound generators + createSounds() { + // Game sounds + this.sounds.tick = this.createTickSound(); + this.sounds.lowTime = this.createLowTimeSound(); + this.sounds.timeUp = this.createTimeUpSound(); + this.sounds.gameStart = this.createGameStartSound(); + this.sounds.gamePause = this.createGamePauseSound(); + this.sounds.gameResume = this.createGameResumeSound(); + this.sounds.gameOver = this.createGameOverSound(); + this.sounds.playerSwitch = this.createPlayerSwitchSound(); + + // UI sounds + this.sounds.buttonClick = this.createButtonClickSound(); + this.sounds.modalOpen = this.createModalOpenSound(); + this.sounds.modalClose = this.createModalCloseSound(); + this.sounds.playerAdded = this.createPlayerAddedSound(); + this.sounds.playerEdited = this.createPlayerEditedSound(); + this.sounds.playerDeleted = this.createPlayerDeletedSound(); + }, + + // Helper function to create an oscillator + createOscillator(type, frequency, startTime, duration, gain = 1.0, ramp = false) { + if (this.audioContext === null) this.init(); + + const oscillator = this.audioContext.createOscillator(); + const gainNode = this.audioContext.createGain(); + + oscillator.type = type; + oscillator.frequency.value = frequency; + gainNode.gain.value = gain; + + oscillator.connect(gainNode); + gainNode.connect(this.audioContext.destination); + + oscillator.start(startTime); + + if (ramp) { + gainNode.gain.exponentialRampToValueAtTime(0.001, startTime + duration); + } + + oscillator.stop(startTime + duration); + + return { oscillator, gainNode }; + }, + + // Sound creators + createTickSound() { + return () => { + const now = this.audioContext.currentTime; + const currentTime = Date.now() / 1000; + + // Initialize lastTickTime if it's not set + if (this.lastTickTime === 0) { + this.lastTickTime = currentTime; + } + + // Calculate how long we've been ticking continuously + const tickDuration = currentTime - this.lastTickTime; + + // Determine volume based on duration + let volume = 0.1; // Default/initial volume + + if (tickDuration <= this.tickFadeoutTime) { + // Linear fade from 0.1 to 0 over tickFadeoutTime seconds + volume = 0.1 * (1 - (tickDuration / this.tickFadeoutTime)); + } else { + // After tickFadeoutTime, don't play any sound + return; // Exit without playing sound + } + + // Only play if volume is significant + if (volume > 0.001) { + this.createOscillator('sine', 800, now, 0.03, volume); + } + }; + }, + + createLowTimeSound() { + return () => { + const now = this.audioContext.currentTime; + // Low time warning is always audible + this.createOscillator('triangle', 660, now, 0.1, 0.2); + // Reset tick fade timer on low time warning + this.lastTickTime = 0; + }; + }, + + createTimeUpSound() { + return () => { + const now = this.audioContext.currentTime; + + // First note + this.createOscillator('sawtooth', 440, now, 0.2, 0.3); + + // Second note (lower) + this.createOscillator('sawtooth', 220, now + 0.25, 0.3, 0.4); + + // Reset tick fade timer + this.lastTickTime = 0; + }; + }, + + createGameStartSound() { + return () => { + const now = this.audioContext.currentTime; + + // Rising sequence + this.createOscillator('sine', 440, now, 0.1, 0.3); + this.createOscillator('sine', 554, now + 0.1, 0.1, 0.3); + this.createOscillator('sine', 659, now + 0.2, 0.3, 0.3, true); + + // Reset tick fade timer + this.lastTickTime = 0; + }; + }, + + createGamePauseSound() { + return () => { + const now = this.audioContext.currentTime; + + // Two notes pause sound + this.createOscillator('sine', 659, now, 0.1, 0.3); + this.createOscillator('sine', 523, now + 0.15, 0.2, 0.3, true); + + // Reset tick fade timer + this.lastTickTime = 0; + }; + }, + + createGameResumeSound() { + return () => { + const now = this.audioContext.currentTime; + + // Rising sequence (opposite of pause) + this.createOscillator('sine', 523, now, 0.1, 0.3); + this.createOscillator('sine', 659, now + 0.15, 0.2, 0.3, true); + + // Reset tick fade timer + this.lastTickTime = 0; + }; + }, + + createGameOverSound() { + return () => { + const now = this.audioContext.currentTime; + + // Fanfare + this.createOscillator('square', 440, now, 0.1, 0.3); + this.createOscillator('square', 554, now + 0.1, 0.1, 0.3); + this.createOscillator('square', 659, now + 0.2, 0.1, 0.3); + this.createOscillator('square', 880, now + 0.3, 0.4, 0.3, true); + + // Reset tick fade timer + this.lastTickTime = 0; + }; + }, + + createPlayerSwitchSound() { + return () => { + const now = this.audioContext.currentTime; + this.createOscillator('sine', 1200, now, 0.05, 0.2); + + // Reset tick fade timer on player switch + this.lastTickTime = 0; + }; + }, + + createButtonClickSound() { + return () => { + const now = this.audioContext.currentTime; + this.createOscillator('sine', 700, now, 0.04, 0.1); + }; + }, + + createModalOpenSound() { + return () => { + const now = this.audioContext.currentTime; + + // Ascending sound + this.createOscillator('sine', 400, now, 0.1, 0.2); + this.createOscillator('sine', 600, now + 0.1, 0.1, 0.2); + }; + }, + + createModalCloseSound() { + return () => { + const now = this.audioContext.currentTime; + + // Descending sound + this.createOscillator('sine', 600, now, 0.1, 0.2); + this.createOscillator('sine', 400, now + 0.1, 0.1, 0.2); + }; + }, + + createPlayerAddedSound() { + return () => { + const now = this.audioContext.currentTime; + + // Positive ascending notes + this.createOscillator('sine', 440, now, 0.1, 0.2); + this.createOscillator('sine', 523, now + 0.1, 0.1, 0.2); + this.createOscillator('sine', 659, now + 0.2, 0.2, 0.2, true); + }; + }, + + createPlayerEditedSound() { + return () => { + const now = this.audioContext.currentTime; + + // Two note confirmation + this.createOscillator('sine', 440, now, 0.1, 0.2); + this.createOscillator('sine', 523, now + 0.15, 0.15, 0.2); + }; + }, + + createPlayerDeletedSound() { + return () => { + const now = this.audioContext.currentTime; + + // Descending notes + this.createOscillator('sine', 659, now, 0.1, 0.2); + this.createOscillator('sine', 523, now + 0.1, 0.1, 0.2); + this.createOscillator('sine', 392, now + 0.2, 0.2, 0.2, true); + }; + }, + + // Play a sound if not muted + play(soundName) { + if (this.muted || !this.sounds[soundName]) return; + + // Resume audio context if it's suspended (needed for newer browsers) + if (this.audioContext.state === 'suspended') { + this.audioContext.resume(); + } + + this.sounds[soundName](); + }, + + // Toggle mute state + toggleMute() { + this.muted = !this.muted; + localStorage.setItem('chessTimerMuted', this.muted); + return this.muted; + }, + + // Play timer sounds based on remaining time + playTimerSound(remainingSeconds) { + if (remainingSeconds <= 0) { + // Reset tick fade timer when timer stops + this.lastTickTime = 0; + return; // Don't play sounds for zero time + } + + if (remainingSeconds <= this.lowTimeThreshold) { + // Play low time warning sound (this resets the tick fade timer) + this.play('lowTime'); + } else if (remainingSeconds % 1 === 0) { + // Normal tick sound on every second + this.play('tick'); + } + }, + + // Play timer expired sound + playTimerExpired() { + this.play('timeUp'); + }, + + // Stop all sounds and reset the tick fading + stopAllSounds() { + // Reset tick fade timer when stopping sounds + this.lastTickTime = 0; + }, + + // Reset the tick fading (call this when timer is paused or player changes) + resetTickFade() { + this.lastTickTime = 0; + } +}; + +// Initialize audio on module load +audioManager.init(); + +// Export the audio manager +export default audioManager; \ No newline at end of file diff --git a/favicon-16x16.png b/favicon-16x16.png new file mode 100644 index 0000000..8d52034 Binary files /dev/null and b/favicon-16x16.png differ diff --git a/favicon-32x32.png b/favicon-32x32.png new file mode 100644 index 0000000..ec20525 Binary files /dev/null and b/favicon-32x32.png differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..d8045d9 Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..dd3e1c5 --- /dev/null +++ b/index.html @@ -0,0 +1,89 @@ + + + + + + + Multi-Player Chess Timer + + + + + +
+
+
+ +
+
+ + + +
+
+ + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..e32f42b --- /dev/null +++ b/manifest.json @@ -0,0 +1,38 @@ +{ + "name": "Chess Timer PWA", + "short_name": "Chess Timer", + "description": "Multi-player chess timer with carousel navigation", + "start_url": "/", + "display": "standalone", + "background_color": "#f5f5f5", + "theme_color": "#2c3e50", + "icons": [ + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "apple-touch-icon.png", + "sizes": "180x180", + "type": "image/png" + }, + { + "src": "favicon-32x32.png", + "sizes": "32x32", + "type": "image/png" + }, + { + "src": "favicon-16x16.png", + "sizes": "16x16", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/site.webmanifest b/site.webmanifest new file mode 100644 index 0000000..e2cf762 --- /dev/null +++ b/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Game Timer", + "short_name": "Game Timer", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..f61c785 --- /dev/null +++ b/styles.css @@ -0,0 +1,261 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: Arial, sans-serif; +} + +body { + background-color: #f5f5f5; + color: #333; + overflow-x: hidden; +} + +.app-container { + max-width: 100%; + margin: 0 auto; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.header { + background-color: #2c3e50; + color: white; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + position: fixed; + top: 0; + width: 100%; + z-index: 10; +} + +.game-controls { + text-align: center; + flex: 1; +} + +.game-button { + background-color: #3498db; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + font-size: 1rem; + cursor: pointer; +} + +.header-buttons { + display: flex; + gap: 0.5rem; +} + +.header-button { + background-color: transparent; + color: white; + border: none; + font-size: 1.2rem; + cursor: pointer; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.header-button:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.carousel-container { + margin-top: 70px; + width: 100%; + overflow: hidden; + flex: 1; + touch-action: pan-x; +} + +.carousel { + display: flex; + transition: transform 0.3s ease; + height: calc(100vh - 70px); +} + +.player-card { + min-width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; + transition: all 0.3s ease; +} + +.active-player { + opacity: 1; +} + +.inactive-player { + opacity: 0.6; +} + +/* New styles for timer active state */ +.player-timer { + font-size: 4rem; + font-weight: bold; + margin: 1rem 0; + padding: 0.5rem 1.5rem; + border-radius: 12px; + position: relative; +} + +/* Timer background effect when game is running */ +.timer-active { + background-color: #ffecee; /* Light red base color */ + box-shadow: 0 0 15px rgba(231, 76, 60, 0.5); + animation: pulsate 1.5s ease-out infinite; +} + +/* Timer of a player that has run out of time */ +.timer-finished { + color: #e74c3c; + text-decoration: line-through; + opacity: 0.7; +} + +/* Pulsating animation */ +@keyframes pulsate { + 0% { + box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7); + background-color: #ffecee; + } + 50% { + box-shadow: 0 0 20px 0 rgba(231, 76, 60, 0.5); + background-color: #ffe0e0; + } + 100% { + box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7); + background-color: #ffecee; + } +} + +.player-image { + width: 120px; + height: 120px; + border-radius: 50%; + background-color: #ddd; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1rem; + overflow: hidden; +} + +.player-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.player-image i { + font-size: 3rem; + color: #888; +} + +.player-name { + font-size: 1.5rem; + margin-bottom: 0.5rem; + font-weight: bold; +} + +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 20; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; +} + +.modal.active { + opacity: 1; + pointer-events: auto; +} + +.modal-content { + background-color: white; + padding: 2rem; + border-radius: 8px; + width: 90%; + max-width: 500px; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: bold; +} + +.form-group input { + width: 100%; + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; +} + +.form-buttons { + display: flex; + justify-content: space-between; + margin-top: 1rem; +} + +.form-buttons button { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + flex: 1; + margin: 0 0.5rem; +} + +.form-buttons button:first-child { + margin-left: 0; +} + +.form-buttons button:last-child { + margin-right: 0; +} + +.delete-button-container { + margin-top: 1rem; +} + +.save-button { + background-color: #27ae60; + color: white; +} + +.cancel-button { + background-color: #e74c3c; + color: white; +} + +.delete-button { + background-color: #e74c3c; + color: white; + width: 100%; +} \ No newline at end of file diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..92ef7a8 --- /dev/null +++ b/sw.js @@ -0,0 +1,44 @@ +// Updated service worker code - sw.js +const CACHE_NAME = 'chess-timer-cache-v1'; +const urlsToCache = [ + '/', + '/index.html', + '/styles.css', + '/script.js' + // Only include files that you're sure exist +]; + +self.addEventListener('install', event => { + event.waitUntil( + caches.open(CACHE_NAME) + .then(cache => { + console.log('Opened cache'); + // Use individual cache.add calls in a Promise.all to handle failures better + return Promise.all( + urlsToCache.map(url => { + return cache.add(url).catch(err => { + console.log('Failed to cache:', url, err); + // Continue despite individual failures + return Promise.resolve(); + }); + }) + ); + }) + ); +}); + +self.addEventListener('fetch', event => { + event.respondWith( + caches.match(event.request) + .then(response => { + // Cache hit - return response + if (response) { + return response; + } + return fetch(event.request); + }) + .catch(err => { + console.log('Fetch handler failed:', err); + }) + ); +}); \ No newline at end of file