PointTracker/App.jsx
2025-01-15 16:31:38 -05:00

325 lines
10 KiB
JavaScript

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 (
<div style={styles.container}>
<h1>Screen-Time Points</h1>
<div style={styles.pointsRow}>
<div style={styles.pointsBox}>
<p>Solo Points: {soloPoints}</p>
</div>
<div style={styles.pointsBox}>
<p>Together Points: {togetherPoints}</p>
</div>
<div style={styles.pointsBox}>
<p>Earned Points: {earnedPoints}</p>
</div>
</div>
<div style={styles.buttonsRow}>
<button style={styles.button} onClick={() => usePoint('solo')}>
Use SOLO Point
</button>
<button style={styles.button} onClick={() => usePoint('together')}>
Use TOGETHER Point
</button>
<button style={styles.button} onClick={addEarnedPoint}>
Add EARNED Point
</button>
<button style={styles.button} onClick={() => usePoint('earned')}>
Use EARNED Point
</button>
</div>
{/* Timer display */}
{timerRunning && (
<div style={{ margin: '20px', fontSize: '18px' }}>
<strong>Timer Running: {formatTime(timeRemaining)}</strong>
</div>
)}
{/* Recent Logs */}
<div style={styles.logSection}>
<h3>Recent Log (last 5)</h3>
{log.slice(-5).reverse().map((entry, idx) => (
<p key={idx}>
[{entry.type}] used at {entry.usedAt}
</p>
))}
</div>
</div>
);
}
// -----------------------------
// Basic Styles
// -----------------------------
const styles = {
container: {
maxWidth: 600,
margin: '40px auto',
fontFamily: 'sans-serif',
textAlign: 'center',
},
pointsRow: {
display: 'flex',
justifyContent: 'space-around',
marginBottom: 20,
},
pointsBox: {
border: '1px solid #aaa',
borderRadius: 4,
padding: '10px 20px',
minWidth: 100,
backgroundColor: '#f9f9f9'
},
buttonsRow: {
display: 'flex',
justifyContent: 'space-around',
marginBottom: 30,
},
button: {
padding: '10px 15px',
fontSize: '14px',
cursor: 'pointer',
backgroundColor: '#0066cc',
color: '#fff',
border: 'none',
borderRadius: 4,
},
logSection: {
textAlign: 'left',
marginTop: 30,
padding: '0 20px',
},
};
export default App;