diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d29575 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/App.jsx b/App.jsx new file mode 100644 index 0000000..b19d8bb --- /dev/null +++ b/App.jsx @@ -0,0 +1,324 @@ +import React, { useState, useEffect, useRef } from 'react'; + +const WEEKLY_SOLO_POINTS = 5; +const WEEKLY_TOGETHER_POINTS = 5; +const EARNED_POINTS_PASSWORD = 'password'; + +// Timer durations (in milliseconds) +const FIFTEEN_MINUTES = 15 * 60 * 1000; +const TWO_MINUTES = 2 * 60 * 1000; + +function App() { + const [soloPoints, setSoloPoints] = useState(WEEKLY_SOLO_POINTS); + const [togetherPoints, setTogetherPoints] = useState(WEEKLY_TOGETHER_POINTS); + const [earnedPoints, setEarnedPoints] = useState(0); + const [log, setLog] = useState([]); // e.g., [{ type: 'solo', usedAt: 'ISOString' }, ...] + const [lastReset, setLastReset] = useState(null); // e.g., 'YYYY-MM-DD' + + // Timer states + const [timerRunning, setTimerRunning] = useState(false); + const [timeRemaining, setTimeRemaining] = useState(0); + + // For storing the active timer ID (so we can clear it on unmount) + const timerRef = useRef(null); + const intervalRef = useRef(null); + + // ----------------------------- + // LOAD / SAVE localStorage + // ----------------------------- + useEffect(() => { + loadData(); + }, []); + + // Check for weekly reset once data is loaded + useEffect(() => { + if (lastReset) { + resetIfNewWeek(); + } + // eslint-disable-next-line + }, [lastReset]); + + // Cleanup timers on unmount + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, []); + + const loadData = () => { + try { + const storedData = localStorage.getItem('pointsData'); + if (storedData) { + const dataObj = JSON.parse(storedData); + setSoloPoints(dataObj.soloPoints ?? WEEKLY_SOLO_POINTS); + setTogetherPoints(dataObj.togetherPoints ?? WEEKLY_TOGETHER_POINTS); + setEarnedPoints(dataObj.earnedPoints ?? 0); + setLog(dataObj.log ?? []); + setLastReset(dataObj.lastReset ?? null); + } else { + // If nothing in localStorage, use defaults + saveData( + WEEKLY_SOLO_POINTS, + WEEKLY_TOGETHER_POINTS, + 0, + [], + null + ); + } + } catch (err) { + console.error('Error reading localStorage:', err); + } + }; + + const saveData = (newSolo, newTogether, newEarned, newLog, newLastReset) => { + const dataObj = { + soloPoints: newSolo, + togetherPoints: newTogether, + earnedPoints: newEarned, + log: newLog, + lastReset: newLastReset + }; + localStorage.setItem('pointsData', JSON.stringify(dataObj)); + }; + + // ----------------------------- + // WEEKLY RESET LOGIC + // ----------------------------- + // Helper: get Monday of current week as 'YYYY-MM-DD' + const getMonday = (date = new Date()) => { + const d = new Date(date); + const day = d.getDay(); // 0=Sun, 1=Mon, ... + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + return d.toISOString().split('T')[0]; + }; + + const resetIfNewWeek = () => { + const currentMonday = getMonday(); + if (!lastReset || lastReset !== currentMonday) { + // Reset solo & together points. Earned points can remain or reset, your choice. + setSoloPoints(WEEKLY_SOLO_POINTS); + setTogetherPoints(WEEKLY_TOGETHER_POINTS); + // setEarnedPoints(0); // Uncomment if you want to reset earned each Monday + setLastReset(currentMonday); + + saveData( + WEEKLY_SOLO_POINTS, + WEEKLY_TOGETHER_POINTS, + earnedPoints, // or 0 if resetting + log, + currentMonday + ); + } + }; + + // ----------------------------- + // USE A POINT + // ----------------------------- + const usePoint = (pointType) => { + if (pointType === 'solo' && soloPoints <= 0) { + alert('No Solo points left!'); + return; + } + if (pointType === 'together' && togetherPoints <= 0) { + alert('No Together points left!'); + return; + } + if (pointType === 'earned' && earnedPoints <= 0) { + alert('No Earned points left!'); + return; + } + + let newSolo = soloPoints; + let newTogether = togetherPoints; + let newEarned = earnedPoints; + + if (pointType === 'solo') newSolo = soloPoints - 1; + if (pointType === 'together') newTogether = togetherPoints - 1; + if (pointType === 'earned') newEarned = earnedPoints - 1; + + // Log usage + const usedAt = new Date().toISOString(); + const newLog = [...log, { type: pointType, usedAt }]; + + // Update states + setSoloPoints(newSolo); + setTogetherPoints(newTogether); + setEarnedPoints(newEarned); + setLog(newLog); + + // Persist + saveData(newSolo, newTogether, newEarned, newLog, lastReset); + + // Start 15-minute timer + alert( + `Using 1 ${pointType.toUpperCase()} point at ${usedAt}. \n15-minute timer started.` + ); + startTimer(FIFTEEN_MINUTES); + }; + + // ----------------------------- + // TIMERS (Client) + // ----------------------------- + const startTimer = (duration) => { + // If a timer is already running, clear it + if (timerRef.current) clearTimeout(timerRef.current); + if (intervalRef.current) clearInterval(intervalRef.current); + + setTimerRunning(true); + setTimeRemaining(duration); + + // Set up a 1-second interval to update the countdown display + let remaining = duration; + intervalRef.current = setInterval(() => { + remaining -= 1000; + setTimeRemaining(remaining); + if (remaining <= 0) { + clearInterval(intervalRef.current); + } + }, 1000); + + // When the timer finishes: + timerRef.current = setTimeout(() => { + clearInterval(intervalRef.current); + setTimerRunning(false); + setTimeRemaining(0); + ring(); + }, duration); + }; + + const ring = () => { + const wantsFinishUp = window.confirm( + '15 minutes are up! \nClick "OK" to start a 2-minute finish-up timer, or "Cancel" to stop.' + ); + if (wantsFinishUp) { + startTimer(TWO_MINUTES); + } + }; + + // ----------------------------- + // ADD EARNED POINT + // ----------------------------- + const addEarnedPoint = () => { + const pw = window.prompt('Enter password to add an earned point:'); + if (pw === EARNED_POINTS_PASSWORD) { + const newEarned = earnedPoints + 1; + setEarnedPoints(newEarned); + saveData(soloPoints, togetherPoints, newEarned, log, lastReset); + alert('1 Earned Point added!'); + } else { + alert('Incorrect password!'); + } + }; + + // ----------------------------- + // DISPLAY TIMER FORMAT + // ----------------------------- + const formatTime = (ms) => { + if (ms <= 0) return '00:00'; + const totalSeconds = Math.floor(ms / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + const mm = minutes < 10 ? `0${minutes}` : minutes; + const ss = seconds < 10 ? `0${seconds}` : seconds; + return `${mm}:${ss}`; + }; + + // ----------------------------- + // RENDER + // ----------------------------- + return ( +
Solo Points: {soloPoints}
+Together Points: {togetherPoints}
+Earned Points: {earnedPoints}
++ [{entry.type}] used at {entry.usedAt} +
+ ))} +Select exactly 4 points => Movie
+ + +Select exactly 2 points => Episode
+ + +Timer ended! Alarm is ringing!
+ + ++ [{entry.type.toUpperCase()}] + {entry.index!=null && ` #${entry.index+1}`} + {' '}at {formatLogDate(entry.usedAt)} +
+ ))} +