325 lines
10 KiB
JavaScript
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;
|