From fdb6e5e618bb804d2a756cbe506017d82bdada61 Mon Sep 17 00:00:00 2001 From: cpu Date: Sat, 22 Mar 2025 22:31:00 +0100 Subject: [PATCH] Initial commit --- android-chrome-192x192.png | Bin 0 -> 6035 bytes android-chrome-512x512.png | Bin 0 -> 17887 bytes apple-touch-icon.png | Bin 0 -> 5320 bytes apps.js | 603 +++++++++++++++++++++++++++++++++++++ audio.js | 315 +++++++++++++++++++ favicon-16x16.png | Bin 0 -> 398 bytes favicon-32x32.png | Bin 0 -> 770 bytes favicon.ico | Bin 0 -> 15406 bytes index.html | 89 ++++++ manifest.json | 38 +++ site.webmanifest | 19 ++ styles.css | 261 ++++++++++++++++ sw.js | 44 +++ 13 files changed, 1369 insertions(+) create mode 100644 android-chrome-192x192.png create mode 100644 android-chrome-512x512.png create mode 100644 apple-touch-icon.png create mode 100644 apps.js create mode 100644 audio.js create mode 100644 favicon-16x16.png create mode 100644 favicon-32x32.png create mode 100644 favicon.ico create mode 100644 index.html create mode 100644 manifest.json create mode 100644 site.webmanifest create mode 100644 styles.css create mode 100644 sw.js diff --git a/android-chrome-192x192.png b/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..7fabb3868741e613c459cce621525e917d36b76e GIT binary patch literal 6035 zcmb_gS5#9`lubgwgkGgXLhsTP1cXpSCsYCH9YHCErXT@Ds)&em0qMOXU5cRy(o0Zj z(!2CtCeF-S^D!UuGav8WkNx)B=iYbTJ?HL7lrEBtn28tw0FY^@s~F(N_J0?I2!A)2 zcE5!mfX@t&N`Ue~mURGtUPnVk(a7Iw+Y*X4Qf<24IF8?O=W^!&#sfrR3L^vsA{=a{ z2+@J^M`wYd(u0LV1uXT9Jx_=-W2NQ22ts8~orsK3lpS;^eMTx(6-THICFJLa9o^BO z&DNWIdnH|^=S9Sg>urPUA94+k9?n!;|DA0zzTu!WexH5QGI}V(Fk{=>p-T{8&#qDZ^hy%AMl>!+h_e8X-x(E-32RFq90dP(_#*}JJesB83 zBMF`RKyoqSd^sfK(8=)~ty6gcBaol_xPy|JGX5{)8`CPc=iO`(Um}O^0dr~ZpE*8M z?tKI#1y%oVK@V7slxuzVNG;1A28+^mz4jfdn@N;pANr$*d67F(p{J2y%Wy}zFN4*7SB_W> z0R$x1eWrzl28Ht-5sA3S|N(iN=q^T=(VG=I_=R_;j zK*LXmI!7G=JiDoF$^*<4YOJ6`j+Shj=PRmY&B{k$m+6f_6H9BMNUF6gh`N`~x7$Uk znCZ(=cW5K1U4s?fD<`Ce;0Db>BB0Gq4m-Cov_B8DD(Hz;cHv2yF=i&W)L<~3>82M> zD`WWY7B2!XFy+f_X&|6T4)2EWsUnE0#;FpE$R@1 zSXIWfv}$r3XKn@Jh8`xYK}3PnmITz7=P3YaIUw@onKniRhXJ>}UA<>izb9L)6^!e5J1@E!JfC$x z{aQ6^dnLKu7P+OC65gL=JNBK3MDx6PFk$yrNHr6Uk8rn5EY*^)OE)+Iw1lp2_evpm zC}3Pu&NMog_SB9-IK?BL#{-ePQbN34y!-JBk6zyk=`)XHFVPOHz7x?kH<4A@Cog0J zs#OFSoc}S?joDc3j$SO6N>^nKcp;BOP`qX4)%L5 zlv;_9{t%sCcoaB3KBa%V)3~I#@*R}&t}ik#1jjEbj|&UF3A^=kooIAA^h$X#Ir46^ zo0wY}1zlYWIEp~6RcF0>c7oIMr3d}X7sa;-Bv?xiiD4B`jmtAb1*_{X-Om-jC~Guu zqtxQt8(5_BNzscj_UV@#B`7VwgmBNJVgWZ~uL!p-1mSn3A7t4OP)OfsNy;S~K!U7` zzjn-F*YhBsTG`RV&8?!;}V34Z=VlU$e_`iWWl} zOrq@KvzZ1v6gAO^Os2=V`FN($%i{w1C6A&J4(ozpT`f$8Rmh8QZhHr7dY6LmXnFAQ zg|@}lYU1X;@4cN?d1F-V+W_6|KiF^IKD__1Q;Bn*Lf7{DjW3+ApP?LUmh}oAfkr-E z_bEQTT%oO~8~CoO0CL#({L3qUxp-mzsP*z5Y|J$SP}}#Jd#HACx@Bpk;zZ%3vSb?D zR$ht)W(IwcPwO1xhYZiS%@3_|+iQO*VJ*mL<``R*#TnbLADjOUwS56PRKK^|Q`5?j zw)keOa$9cd>3m2QHhOMy{?E`=;#`PjbQu(3l1idhFh6Z?S$er$7&75Q{mz5fG=J!@ z`BU^0C}J}0Br`3>m0$)*7QEldagl_86hbw}QxTTx=Hwi;ch%=8{%)&CdAPsJA5eu0 z&6?Q%RX}Jwjgh zN2}<>{@|!(@7JrbD@AuXO6=r9oa4$SNjycVie=%faAnGE!vr>r_c)am4LFPH? z_x-0|#1^w^K5s*Y2LyLl63~a5u}{oKn34}Uknj8rc0Vn{Tk7k{|Nbqj**UFBpfb~K z5xpmzn6FSDu-^}0yXu@P+G+3tW`C|KgCKUIPAIgx^LV-VLqgg9)}ntJuyo?WL#Mp? zUVCUMP44q#D7o!9?;Ch2W=_>Rx(-~;JIl+RyuI3TTj(fn{rxKnu#|p1)k}*$!up-u zIo2u_F8!7|<1=MWtzw4AuGXFTm6-TIFGf?fURpQDbw+Q6@Q|%tl^yg24rnc$@y*AH zyTv*!>uE8YPbLs*y9*H8b`6>b?i->Wjf0q0OC?&8#inSIYIIx?3;8D{l`pMPb6YhI z7s<+g8q~=W9E1T^zpPA#qxs8b>RE^v(iYWCr zm`lsh(H}p#;GdXG82#Y3UrjsIvD*Y9ZYO!Xm zV0b!51kwhC33&IUSXuZ*AtSNKoAsXQI|P~zRdJJ4HXy#ySdph8!P?q2p{vDw5;!B< zfeabCY}0NkN<3)r?tPHO>1Ho&9Ou_4@dc0NDYu+c8G-UMmtH)I_*8m_f0mMQwFtti z`NmnD~*S2s)>#*-sYHu?HM%pLp$3D%)UiW;#z zB?Wn$_0g#_?A4a3Us^!ctadbzpqfd(lq7^#wl-D1>xH2){J`D(=5OQ(h2n%XwJJFc zY%t}kUouX!{Bv=j87=mQU$MT!n6zxMr^}uD+8F7$e96KJT?4;URpo$^tlRY!2WIjO z7Kfbe;0S`nniu!?9IOIk?+i3sH*jNauBSa5Etay`fjkHn20ixl*T09fP3$h!zQLr_ zHN`Z!=11l6CEX5-FEjem-O)sa(ON3~Z06U);t^UGF#Id2R_zT*@&688k7MD_>s(t@ zBHeS~tOfRIoS3fG_&_p1U)f}C{)d&W5L;5dB$s_Fy(u^O*%=|M+cmO@kp}j-cZ0dG z_R50+6i{b*sg~#4w#;y+(eR!<2rF9Vu-Ew9oSw87HGe*l8yp4pJLz#Jp#7GBoqi3a zBtY}}`q*?Opt1kv4ja6#kk^UTgH?Z8%! zz7fwOe|&POM9x=KTl)K}MC}i6B{=SV@nIq2?pu#&mZ5^rHd_{fu4n$2n*i=$vqOUHm8nuzhri*_|LAw7>Ps42Sy49+Ue=Rm+{7NFg5gyS&N{k* zMk!@VZO1R%T%HeT_A|$5UHr>Lz(L0hYcKn!D0^Do)%`7cct+rs`}(dwJXqw^}9_9lsl<7_cHZ3ydGTN+5m>Qv4;=;w6>-bYt-2x1>Ql2i41CYtfhG-GzW@v(r?<9`AS#l zZCIz&I6mMp%^qi&O0)sfnnqSr0#2RO5?d5!w8->Ujt9Z)qQ6w%qn5Ao$`$J)wL`Ih z6O^Viz+zkrd%-RQM#RDXXOS9AS+}fyn!IY3Ul8RpZ-0y8qsb!6GSSh(c>~mDX4^W# zSEopXl#C|~-$FgX7W}J!y9LD)nicM$115?EN(Gl#VGoK~Q;c}psXUo};=)$D3#BkQ zA_VYAjf#hMFBp4(QQF2w<8~kxAkM7d#a=UfW0d{w_G6E{$o_{W3&=sD3SyS#JiAoQp|O;uy@}ZQ)p(fup*KEcn|lXKT+;z2-iIz~Q+7XN&8IJcvYkjs z(jPFZDB&_qU)#h6WV(JXeN^gwra{G$S&*r{qP__O(Y`;R?fmp9YG%B>B1k`Hu6 z$Bh*OAoL=C0^6$uN;iD9F-{TkpOQkWn5q%?&7?7xq#C90f~AV zJ&{bESyJeO4T`!3AK^?Owy>fvO$?Zd_$AoYKF6E;*&~&WmtkY0XaIQWm}7mWQr$j< zubGk^+e@Bom<*TaEF5S%t5Wm$AnEe zLg1)dP48&z(4^a|+^9vl;po3PoCc!BnQRI+8 zE~fiLqUNh;M4Eh|k}Kv}_G5zknjP90QJW^#KSgQv-~LeY3lS&9hWv|!0)2i_4|Roc ztO-;+spw*tKkj(g10D|Zs>P>|E!Ml4!9U1bW_0EpKSyd7)z>+_o)$9r9tAJQlJp1A zs3ddr03eF*quH*fDJE}XKVM1`^C0|1-W;RN*HJ!nO{)smVeB{GfL=a|EFUa*5DZ_a zf-L<1JUq7prcfu+tw~sWNk$|~8XI!ru_^20XSN+KLEu7(kXs6irE#f;(oR+Q(9w&} zEd0gw*nw+ZC)Z>zg;wI$!~{!X-{7HQ8Hr9q`X$nvo_9f}oqHJcn%58f-)g%i!nW4U zNxv3Nq;JXVwfdZWcIrE@%5Ktxj4TDPeF!cr{n%h{dDq0-fRyYpOS3v&z8TsmA}6Lf zCyV(==k(<6?Cij-O;t(Bc_LmW-@e*1`9v!S%vbT=L>uKBZd8HYQ+EKp9+9oDr-|-f zmxTzHyl+^3^|=<-pI~Ncnxd_xC=W4&->Gw_&mbLGlDPGTDEBi-@e4(xR>{54pNwTEqASi}2hlDmkD07w(K4NN+h0e`emWf|6RaUKd(8gop-&B1-*m$H}DOR z{dTda*XsFW_i1(qUIwzh5+yF0ENIlXnsk462jZsZc77x!WE!+*dCPzs>0lX_9KTm_36K;-8FVA7D!L1bm6>MJ-FR%EjCmDJGA*`V0chIg zpLKCgU;s1-fOdrSg|NA3U7;oPVXjqnQyHGq8Dgr}$pG315 z9#@Lui?#!Ag#0_%-&8Rp zx{a_aC#>BaD3&k>{pFm|K)(!0rrsyv_rhhkHJ7<+Jl}i?I8o;m+#E(E{qYC4b1f^| zhs0oB{*UwO(X{3I>&fp}(YKFs>p=|d;g09dZ(l@^{I`Mo&1Cg`Zk-O`v2b3mrRn65 z*0vBQr?fuo`^I$re%7x~CdBYiR$_7uNyES0`^C3Z<3NoS>(jP2d5jtdm zbw9QP;O~LJJ0G$KzAq5(!C~+}Ngzki6dW@^y)a{=GjqFs%&hjH=?8Jv%XbR#`@vgB W6@G)1C5CvH9iXAAt5U9H6Z#)KUH5hX literal 0 HcmV?d00001 diff --git a/android-chrome-512x512.png b/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..6c47bef641900744a3fb17630b5af64549b71cd2 GIT binary patch literal 17887 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 V4oQ0eez678xU6%j=z`U~{{<5pF}nZ& literal 0 HcmV?d00001 diff --git a/apple-touch-icon.png b/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6534042639016820743d3db5c205ccbf5b980944 GIT binary patch literal 5320 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8d52034c62a6445ee292243a68123ff04849b538 GIT binary patch literal 398 zcmV;90df9`P)Px$NJ&INR5(v#l$-Omj){S>nt_3Vje#`q>mS4aRz`(6?|zb@8D`ZlkOBX(SuDZD z#2~@V2$uQ>!v7)o@#lXGpMN2=U>IO1#==l%!O37C%7WEHG21^g9DVi&E{9@(t_U;3 zEEgVzwPx%yh%hsR9HvVS4n6SVHkb?WSXT(8a39o#uD+Ol2SxOD;A+r>cLj*#a$HbLA-hp zYeNrWy%j_#7>b}?S~nDmT32XQirOLsal05}8^t7=t%;dtMrI}%YiXLyq)haKVK#o= z`@Z+Rex zz@@GHzY^hj`|U8?{*)|f@^XPiM=;57U_?wQcv*&bSwIo-Q|*|LIVnQQ`OT7uDi3qYs)(&bLz z?2C9V9A!;7+o&dc2`)66q+iE}Vz~J+sUCsOIs*bPQf~fa6GpK+6krd@tkFQJmc1`-$&aez6xU0LjV8(07*qoM6N<$f;~H9 A6951J literal 0 HcmV?d00001 diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d8045d922e571db84ff130ce2bf1309d55f3c119 GIT binary patch literal 15406 zcmeHOYitx%6doV{#UK8~Em6d%JbVE0iDF{Jx6#BzOo&esAMvgDXd0tGF#5-+3DA~T zEm&G;iwG@HD32nK2sTZrJfyrzsqH*=W_EXGcjkD`-OjW-yRe8CZ2I)!_merU<2sI;f^Col zDU#2z%28ox!a z9dF2qUf%i!;9Ota~6Q`p6$& zGkSUJ>tm;9^IgW;6JG!mB8R>oGriRK1Q^N%(2lPMb@w7L!e@ZSA-BE?^7c1bolOSe z=4T*2aF}7w^e6oLi(|qg*3M=+OnNLeJqc3d;~;IA2g0h`ob(yjKsKDi@AFxMj5ecD zaPg4rObD+zc#(a&RDlZa%E6`Ae}TCB#yB>ooO1cyM?4wtO2#{vKJum+O!D?~+)m8J z#^1s8I=cC>E4wUu}L8!Uicf5(AZrk*j(IK=M(|pBXsQos7x*@yjI=U$B z@B7MMSTz-OAZ|m5b}sRiKds~Vh5a^v+D8{+ZM*419rhsz1nukH(7I_${w802V_N^> zn!C{jF>D!R*t@6ExZ!KH)q7i+uSWi&+EEVTulL#gT7DRT@aj7(uDyrJ*B&U|&^MUA zL-GED_G9f-3&gr>Ks#0s+CRU6hPd@J+aY@R$DI94O9mwC(Y$P?W4s}s+v+Rlx~dG6 z9q*w3_tzM?jx;OuH-h9rHi-0r)`7b3E4(W3_m`mV{T!4%i$K}^2`IZh!hPxo5Z?G) z{^NHa0}2w)V){VL56xLG^PCrl5A3HL`pEYs|NByLf&a^Mij!c7mRo?=<>Dmxn&X#D zx@K_M#QCT%9eh?eL%D3+BzPrWW!{|qZ)w&Wfp?3N4> zv#m4Mv*6XGt?$|7S;}iQ8}{+{LE6h)601-%1GE#3kQh1&dfz^Xp&u<_JfmQ?(qlc5 zTV4h;CXQ^Z@y?8^c%JiV-advOGN3pC;dK`BmxPtKmi#=BUqR=F&O*K*^OHTE|9t02 zK|L`OQ73%R&+W!o{*2O>|IYXD>^5D0$Tt>$xwGW)%dM}0sYz+_PYiYgor&fSF=_MH$S&eq&Qxt z;wl%Sh;}x5}YKu@C#@sN5`G5!RQ!|_uBpys((W|ZNau{ zBRYuqlleV`;^S36vA^qV+#@$82(>cYeiOuw)}-N2@I4vN$_mN5P<>Gzfzy{UWZcJ%I$tXk zFE9F$nw|nfy_hlwCfd;OrWXRCLpKl(Nb`Z!+5%ZonM1B+Aeqe7t#`WEpQ z`8!rU8N_uDWz7u}YiHqBLQfe(ta~-^%O@9g4oHVdmnp{~e(?}QaBnF!Jd*Tr9oV#! z%_;b`Q(JJ{Q;UjUSa}PcgEyq$qPP#)e3CDn+sa!OfVBQmP}<(cm{7!S%Y6B|IT(+) z1H?76(H`9oQvF;ue#&!^8ZloOWkKu53!S$5qR@q1{aicx_-(j$wt;@S74u-WU_Q(y&`xe-v3L6agBa>0`4Evmf55oc zEZ^~8ZfxW4T>r0AaON+_XOP4B1Oe91WG+OI^9L{oBFOn0Rt}1t3sK7JVOZZs=g$!S L@iH!fTqW=yU4u4Y literal 0 HcmV?d00001 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