push notifs

This commit is contained in:
2025-01-17 14:06:07 -05:00
parent ab91a53b15
commit 49b71ae1c7
5 changed files with 700 additions and 409 deletions

View File

@@ -1,68 +1,75 @@
// src/App.js
import React, { useState, useEffect, useRef } from 'react';
const SERVER_URL = 'http://10.1.0.100:80';
// Replace with your actual public VAPID key
const VAPID_PUBLIC_KEY = 'BCn73fh1YZV3rFbK9H234he4nNWhhEKnYiQ_UZ1U0nxR6Q6cKvTG6v05Uyq7KXh0FxwjPOV3jHR3DPLqF5Lyfm4';
export default function App() {
function App() {
// -----------------------------
// SERVER STATE
// BASIC STATE & MESSAGING
// -----------------------------
const [statusMsg, setStatusMsg] = useState('');
// -----------------------------
// SERVER TIMER STATE
// -----------------------------
const [soloUsed, setSoloUsed] = useState([]);
const [togetherUsed, setTogetherUsed] = useState([]);
const [logs, setLogs] = useState([]);
const [timerRunning, setTimerRunning] = useState(false);
const [timeRemaining, setTimeRemaining] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [ringing, setRinging] = useState(false);
// Admin
const [adminMode, setAdminMode] = useState(false);
// For status/error messages
const [statusMessage, setStatusMessage] = useState('');
// For Movie/Episode selection
// For managing “Movie” (4 points) or “Episode” (2 points)
const [chooseMovieMode, setChooseMovieMode] = useState(false);
const [chooseEpisodeMode, setChooseEpisodeMode] = useState(false);
const [chosen, setChosen] = useState([]); // e.g. [{ category:'solo', index }, ...]
const [chosenPoints, setChosenPoints] = useState([]); // e.g. [{ category:'solo', index:2 }, ...]
// -----------------------------
// "ADD POINTS" MULTI-STEP UI
// -----------------------------
// Step 1 => enter password + immediate check
// Step 2 => pick category
// Step 3 => pick amount
// For the multi-step “Add Points” flow
const [showAddPointsOverlay, setShowAddPointsOverlay] = useState(false);
const [addPointsStep, setAddPointsStep] = useState(1);
const [addPointsStep, setAddPointsStep] = useState(1); // 1 => password, 2 => category, 3 => amount
const [pointsPassword, setPointsPassword] = useState('');
const [pointsCategory, setPointsCategory] = useState(''); // 'solo' or 'together'
const [pointsCategory, setPointsCategory] = useState('');
const [pointsAmount, setPointsAmount] = useState(1);
// Audio ref for the alarm
// Admin mode
const [adminMode, setAdminMode] = useState(false);
// Audio ref for local “alarm” (only if the tab is foreground)
const alarmRef = useRef(null);
// Poll server
// -----------------------------
// ON MOUNT: FETCH STATE
// & REGISTER PUSH
// -----------------------------
useEffect(() => {
// Poll the server for timer state
fetchState();
const id = setInterval(fetchState, 1000);
return () => clearInterval(id);
const intervalId = setInterval(fetchState, 1000);
return () => clearInterval(intervalId);
}, []);
// If ringing changes => play/stop audio
useEffect(() => {
// Register service worker + request push
registerSWAndSubscribeToPush();
}, []);
// If ringing changes, try to play local alarm (if tab is foreground)
useEffect(() => {
if (ringing) {
playAlarm();
playLocalAlarm();
} else {
stopAlarm();
stopLocalAlarm();
}
}, [ringing]);
// -----------------------------
// FETCH STATE
// FUNCTIONS
// -----------------------------
async function fetchState() {
try {
const res = await fetch(`${SERVER_URL}/state`);
const res = await fetch('/state');
const data = await res.json();
setSoloUsed(data.soloUsed || []);
setTogetherUsed(data.togetherUsed || []);
@@ -72,295 +79,377 @@ export default function App() {
setIsPaused(data.isPaused);
setRinging(data.ringing);
} catch (err) {
console.error(err);
setStatusMessage('Error: cannot reach server');
console.error('Fetch state error:', err);
setStatusMsg('Error: cannot reach server');
}
}
// ALARM
function playAlarm() {
if (alarmRef.current) {
alarmRef.current.loop = true;
alarmRef.current.currentTime = 0;
alarmRef.current.play().catch(e => {
console.log('Alarm might be blocked until user interacts:', e);
});
}
// ----- Local alarm (only if foreground) -----
function playLocalAlarm() {
if (!alarmRef.current) return;
alarmRef.current.loop = true;
alarmRef.current.currentTime = 0;
alarmRef.current.play().catch((err) => {
console.log('Alarm play might be blocked until user interacts:', err);
});
}
function stopAlarm() {
if (alarmRef.current) {
alarmRef.current.pause();
alarmRef.current.currentTime = 0;
alarmRef.current.loop = false;
}
function stopLocalAlarm() {
if (!alarmRef.current) return;
alarmRef.current.pause();
alarmRef.current.currentTime = 0;
alarmRef.current.loop = false;
}
// SINGLE BUBBLE => 15 MIN
// ----- Single bubble => 15-min -----
async function handleBubbleClick(category, index) {
try {
const res = await fetch(`${SERVER_URL}/use-bubble`, {
const res = await fetch('/use-bubble', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category, index }),
});
const data = await res.json();
if (data.error) {
setStatusMessage(`Error: ${data.error}`);
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMessage(`Used 1 ${category} (#${index+1}) => 15-min timer`);
setStatusMsg(`Using 1 ${category} bubble => 15-min timer started`);
}
} catch(err) {
} catch (err) {
console.error(err);
setStatusMessage('Error using bubble');
setStatusMsg('Error using bubble');
}
}
// TIMER
// ----- Pause/Resume -----
async function handlePauseResume() {
try {
const res=await fetch(`${SERVER_URL}/pause-resume`,{ method:'POST' });
const data=await res.json();
if(data.error) setStatusMessage(`Error: ${data.error}`);
else setStatusMessage(`Timer paused => ${data.isPaused}`);
} catch(err){
const res = await fetch('/pause-resume', { method: 'POST' });
const data = await res.json();
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg(`Timer paused => ${data.isPaused}`);
}
} catch (err) {
console.error(err);
setStatusMessage('Error pause/resume');
setStatusMsg('Error: pause/resume');
}
}
// ----- Finish Up (2 min) -----
async function handleFinishUp() {
try {
const res=await fetch(`${SERVER_URL}/finish-up`,{method:'POST'});
const data=await res.json();
if(data.error) setStatusMessage(`Error: ${data.error}`);
else setStatusMessage('2-min finish-up started');
} catch(err){
const res = await fetch('/finish-up', { method: 'POST' });
const data = await res.json();
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg('2-minute finish-up started');
}
} catch (err) {
console.error(err);
setStatusMessage('Error finish-up');
}
}
async function handleCancelFinishUp(){
try {
const res=await fetch(`${SERVER_URL}/cancel-finish-up`,{ method:'POST'});
const data=await res.json();
if(data.error) setStatusMessage(`Error: ${data.error}`);
else setStatusMessage('Finish-up canceled');
} catch(err){
console.error(err);
setStatusMessage('Error cancel finish-up');
}
}
async function handleIgnoreRing(){
try {
const res=await fetch(`${SERVER_URL}/ignore-ring`,{ method:'POST'});
const data=await res.json();
if(data.error) setStatusMessage(`Error: ${data.error}`);
else setStatusMessage('Ignored ring (no 2-min started)');
} catch(err){
console.error(err);
setStatusMessage('Error ignoring ring');
setStatusMsg('Error finishing up');
}
}
// MOVIE / EPISODE
function startChooseMovie(){
// ----- Cancel Finish-Up -----
async function handleCancelFinishUp() {
try {
const res = await fetch('/cancel-finish-up', { method: 'POST' });
const data = await res.json();
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg('Finish-up canceled');
}
} catch (err) {
console.error(err);
setStatusMsg('Error canceling finish-up');
}
}
// ----- Ignore ring -----
async function handleIgnoreRing() {
try {
const res = await fetch('/ignore-ring', { method: 'POST' });
const data = await res.json();
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg('Ring ignored, no 2-min started');
}
} catch (err) {
console.error(err);
setStatusMsg('Error ignoring ring');
}
}
// ----- Movie/Episode usage -----
function startChooseMovie() {
setChooseMovieMode(true);
setChooseEpisodeMode(false);
setChosen([]);
setStatusMessage('Select exactly 4 points for a movie');
setChosenPoints([]);
setStatusMsg('Select exactly 4 points for a Movie');
}
function startChooseEpisode(){
function startChooseEpisode() {
setChooseEpisodeMode(true);
setChooseMovieMode(false);
setChosen([]);
setStatusMessage('Select exactly 2 points for an episode');
setChosenPoints([]);
setStatusMsg('Select exactly 2 points for an Episode');
}
function toggleChosen(category, index){
const found=chosen.find(c=>c.category===category && c.index===index);
if(found){
setChosen(chosen.filter(c=> c!==found));
function toggleChosen(category, index) {
const found = chosenPoints.find(
(c) => c.category === category && c.index === index
);
if (found) {
setChosenPoints(chosenPoints.filter((c) => c !== found));
} else {
setChosen([...chosen, { category,index }]);
setChosenPoints([...chosenPoints, { category, index }]);
}
}
async function submitMovie(){
if(chosen.length!==4){
setStatusMessage('Must pick exactly 4 points for a movie');
async function submitMovie() {
if (chosenPoints.length !== 4) {
setStatusMsg('Must pick exactly 4 points for a movie');
return;
}
try {
const res=await fetch(`${SERVER_URL}/use-movie`,{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ chosenPoints: chosen }),
const res = await fetch('/use-movie', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chosenPoints }),
});
const data=await res.json();
if(data.error) setStatusMessage(`Error: ${data.error}`);
else setStatusMessage('Movie => used chosen 4 points');
} catch(err){
const data = await res.json();
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg('Movie => used chosen 4 points');
}
} catch (err) {
console.error(err);
setStatusMessage('Error submitting movie');
setStatusMsg('Error submitting movie');
}
setChooseMovieMode(false);
setChosen([]);
setChosenPoints([]);
}
async function submitEpisode(){
if(chosen.length!==2){
setStatusMessage('Must pick exactly 2 points for an episode');
async function submitEpisode() {
if (chosenPoints.length !== 2) {
setStatusMsg('Must pick exactly 2 points for an Episode');
return;
}
try {
const res=await fetch(`${SERVER_URL}/use-episode`,{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ chosenPoints: chosen }),
const res = await fetch('/use-episode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chosenPoints }),
});
const data=await res.json();
if(data.error) setStatusMessage(`Error: ${data.error}`);
else setStatusMessage('Episode => used chosen 2 points');
} catch(err){
const data = await res.json();
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg('Episode => used chosen 2 points');
}
} catch (err) {
console.error(err);
setStatusMessage('Error submitting episode');
setStatusMsg('Error submitting episode');
}
setChooseEpisodeMode(false);
setChosen([]);
setChosenPoints([]);
}
// ADD POINTS => 3 step flow
const [passwordChecked, setPasswordChecked] = useState(false); // if true => step2
function openAddPointsFlow(){
// ----- Add Points (3-step flow) -----
function openAddPointsFlow() {
setShowAddPointsOverlay(true);
setAddPointsStep(1);
setPointsPassword('');
setPointsCategory('');
setPointsAmount(1);
setPasswordChecked(false);
}
function closeAddPointsFlow(){
function closeAddPointsFlow() {
setShowAddPointsOverlay(false);
}
// Step 1: user enters password => we check immediately
// Step 1: check password immediately
async function checkPointsPassword() {
if(!pointsPassword){
setStatusMessage('Please enter a password');
if (!pointsPassword) {
setStatusMsg('Please enter a password');
return;
}
try {
const res=await fetch(`${SERVER_URL}/check-password`,{
method:'POST',
headers:{'Content-Type':'application/json'},
const res = await fetch('/check-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pointsPassword }),
});
const data=await res.json();
if(data.error){
setStatusMessage(`Error: ${data.error}`);
const data = await res.json();
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
// password correct => go to step2
setPasswordChecked(true);
setStatusMsg('Password ok, pick category next');
setAddPointsStep(2);
}
} catch(err){
} catch (err) {
console.error(err);
setStatusMessage('Error checking password');
setStatusMsg('Error checking password');
}
}
// Step 2 => pick category
function pickAddPointsCategory(cat){
function pickAddPointsCategory(cat) {
setPointsCategory(cat);
setAddPointsStep(3);
}
// Step 3 => pick amount => do /add-points
async function pickAddPointsAmount(amt){
setPointsAmount(amt);
// Now call POST /add-points
async function pickAddPointsAmount(amount) {
setPointsAmount(amount);
try {
const res=await fetch(`${SERVER_URL}/add-points`,{
method:'POST',
headers:{'Content-Type':'application/json'},
const res = await fetch('/add-points', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
password: pointsPassword,
category: pointsCategory,
amount: amt
})
amount,
}),
});
const data=await res.json();
if(data.error) setStatusMessage(`Error: ${data.error}`);
else setStatusMessage(`+${amt} points added to ${pointsCategory}`);
} catch(err){
const data = await res.json();
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg(`+${amount} points added to ${pointsCategory}`);
}
} catch (err) {
console.error(err);
setStatusMessage('Error adding points');
setStatusMsg('Error adding points');
}
setShowAddPointsOverlay(false);
}
// ADMIN
async function handleAdminLogin(){
const pw=prompt('Enter admin password:');
if(!pw) return;
// ----- ADMIN -----
async function handleAdminLogin() {
const pw = prompt('Enter admin password:');
if (!pw) return;
try {
const res=await fetch(`${SERVER_URL}/admin/login`,{
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ password:pw }),
const res = await fetch('/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pw }),
});
const data=await res.json();
if(data.error) setStatusMessage(`Error: ${data.error}`);
else {
const data = await res.json();
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setAdminMode(true);
setStatusMessage('Admin mode enabled');
setStatusMsg('Admin mode enabled');
}
} catch(err){
} catch (err) {
console.error(err);
setStatusMessage('Error admin login');
setStatusMsg('Error admin login');
}
}
function handleAdminLogout(){
function handleAdminLogout() {
setAdminMode(false);
setStatusMessage('Admin mode disabled');
setStatusMsg('Admin mode disabled');
}
async function adminRequest(path){
async function adminRequest(path) {
try {
const res=await fetch(`${SERVER_URL}/admin/${path}`,{ method:'POST'});
const data=await res.json();
if(data.error) setStatusMessage(`Error: ${data.error}`);
else setStatusMessage(`Admin => ${path} success`);
} catch(err){
const res = await fetch(`/admin/${path}`, { method: 'POST' });
const data = await res.json();
if (data.error) {
setStatusMsg(`Error: ${data.error}`);
} else {
setStatusMsg(`Admin => ${path} success`);
}
} catch (err) {
console.error(err);
setStatusMessage(`Error admin ${path}`);
setStatusMsg(`Error admin ${path}`);
}
}
// RENDER BUBBLES
function renderBubbles(usedArr, category){
return usedArr.map((val, idx)=>{
// If chosen => highlight
const isChosen = chosen.find(c=> c.category===category && c.index===idx);
const circleColor = val ? '#8B0000' : '#4caf50'; // used => dark red, unused => green
// -----------------------------
// PUSH NOTIFICATIONS SETUP
// -----------------------------
async function registerSWAndSubscribeToPush() {
if (!('serviceWorker' in navigator)) {
console.log('Service workers not supported in this browser.');
return;
}
try {
// Register custom service worker from /public folder
const reg = await navigator.serviceWorker.register('/service-worker.js');
console.log('Service Worker registered:', reg);
// Request Notification permission
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.log('Notification permission not granted.');
return;
}
// Subscribe
const subscription = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
console.log('Push subscription:', subscription);
// Send subscription to server
await fetch('/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subscription }),
});
console.log('Subscribed to push notifications!');
} catch (err) {
console.error('SW registration or push subscription failed:', err);
}
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i=0; i<rawData.length; i++){
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// -----------------------------
// RENDER BUBBLES (solo/together)
// -----------------------------
function renderBubbles(usedArr, category) {
return usedArr.map((val, idx) => {
const isSelected = chosenPoints.find(
(c) => c.category === category && c.index === idx
);
const bubbleColor = val ? '#8B0000' : '#4caf50'; // used => dark red, unused => green
// Outline color for base vs appended
let outline = (idx<5) ? '2px solid #5f7b99' : '2px solid #7f5f99';
if(isChosen) outline='3px solid yellow';
let outline = (idx < 5) ? '2px solid #5f7b99' : '2px solid #7f5f99';
if (isSelected) outline = '3px solid yellow';
return (
<div
key={idx}
style={{
width:30, height:30,
borderRadius:'50%',
backgroundColor: circleColor,
margin:5,
cursor:'pointer',
width: 30,
height: 30,
borderRadius: '50%',
backgroundColor: bubbleColor,
margin: 5,
cursor: 'pointer',
border: outline,
boxSizing:'border-box'
boxSizing: 'border-box',
}}
onClick={()=>{
if(val){
setStatusMessage('That bubble is already used.');
onClick={() => {
if (val) {
setStatusMsg('That bubble is already used.');
return;
}
if(chooseMovieMode||chooseEpisodeMode){
toggleChosen(category,idx);
if (chooseMovieMode || chooseEpisodeMode) {
toggleChosen(category, idx);
} else {
handleBubbleClick(category,idx);
handleBubbleClick(category, idx);
}
}}
/>
@@ -368,10 +457,10 @@ export default function App() {
});
}
// Are we in 2-min finish-up?
// Are we in the 2-min finish-up?
const inFinishUp = timerRunning && !ringing && timeRemaining>0 && timeRemaining<=120000;
// Time formatting
// Format time
function formatTime(ms){
if(ms<=0) return '00:00';
const totalSec=Math.floor(ms/1000);
@@ -379,13 +468,14 @@ export default function App() {
const ss=totalSec%60;
return `${String(mm).padStart(2,'0')}:${String(ss).padStart(2,'0')}`;
}
// Format logs
function formatLogDate(iso){
const d=new Date(iso);
const w=d.toLocaleDateString('en-US',{ weekday:'long'});
const w=d.toLocaleDateString('en-US',{ weekday:'long' });
const day=d.getDate();
const ord=getOrdinal(day);
const time=d.toLocaleTimeString('en-US',{ hour12:true,hour:'numeric',minute:'2-digit'});
return `${w} ${day}${ord}, ${time}`;
const timeStr=d.toLocaleTimeString('en-US',{ hour12:true,hour:'numeric',minute:'2-digit' });
return `${w} ${day}${ord}, ${timeStr}`;
}
function getOrdinal(n){
const s=['th','st','nd','rd'];
@@ -393,13 +483,16 @@ export default function App() {
return s[(v-20)%10]||s[v]||s[0];
}
// RENDER
// -----------------------------
// RENDER
// -----------------------------
return (
<div style={styles.container}>
<h1>Mylin's Points</h1>
<h1>Screen-Time App (Push Notifications)</h1>
{statusMessage && <div style={styles.statusBox}>{statusMessage}</div>}
{statusMsg && <div style={styles.statusBox}>{statusMsg}</div>}
{/* Audio for local ring (foreground only) */}
<audio ref={alarmRef} src="/alarm.mp3" />
{/* SOLO */}
@@ -418,7 +511,7 @@ export default function App() {
</div>
</section>
{/* Add Points button => 3-step overlay */}
{/* Add Points (3-step) */}
<button style={styles.button} onClick={openAddPointsFlow}>
Add Points
</button>
@@ -427,32 +520,37 @@ export default function App() {
<section style={{marginTop:20}}>
{chooseMovieMode?(
<div>
<p>Select exactly 4 points => Movie</p>
<p>Select 4 points for a Movie</p>
<button style={styles.button} onClick={submitMovie}>Submit Movie</button>
<button style={styles.button} onClick={()=>{
<button style={styles.button} onClick={()=> {
setChooseMovieMode(false);
setChosen([]);
setChosenPoints([]);
}}>Cancel</button>
</div>
):(
<button style={styles.button} onClick={startChooseMovie}>Movie (4 points)</button>
<button style={styles.button} onClick={startChooseMovie}>
Movie (4 points)
</button>
)}
{chooseEpisodeMode?(
<div>
<p>Select exactly 2 points => Episode</p>
<p>Select 2 points for an Episode</p>
<button style={styles.button} onClick={submitEpisode}>Submit Episode</button>
<button style={styles.button} onClick={()=>{
<button style={styles.button} onClick={()=> {
setChooseEpisodeMode(false);
setChosen([]);
setChosenPoints([]);
}}>Cancel</button>
</div>
):(
<button style={styles.button} onClick={startChooseEpisode}>Episode (2 points)</button>
<button style={styles.button} onClick={startChooseEpisode}>
Episode (2 points)
</button>
)}
</section>
{/* Timer */}
<section style={{ marginTop:30 }}>
{/* Timer display */}
<section style={{marginTop:30}}>
{(timerRunning || timeRemaining>0) && (
<div>
<h3>Timer: {formatTime(timeRemaining)} {isPaused && '(Paused)'}</h3>
@@ -462,18 +560,14 @@ export default function App() {
</div>
)}
{ringing && (
<div style={{ marginTop:10 }}>
<p style={{ color:'yellow' }}>Timer ended! Alarm is ringing!</p>
<button style={styles.button} onClick={handleFinishUp}>
Finish Up (2 min)
</button>
<button style={styles.button} onClick={handleIgnoreRing}>
Ignore
</button>
<div style={{marginTop:10}}>
<p style={{color:'yellow'}}>Timer ended! Alarm is ringing!</p>
<button style={styles.button} onClick={handleFinishUp}>Finish Up (2 min)</button>
<button style={styles.button} onClick={handleIgnoreRing}>Ignore</button>
</div>
)}
{inFinishUp && (
<div style={{ marginTop:10 }}>
<div style={{marginTop:10}}>
<button style={styles.button} onClick={handleCancelFinishUp}>
Cancel Finish Up
</button>
@@ -482,9 +576,9 @@ export default function App() {
</section>
{/* Logs */}
<section style={{ marginTop:20 }}>
<section style={{marginTop:20}}>
<h3>Recent Logs (last 5)</h3>
{logs.slice(-5).reverse().map((entry,i)=>(
{logs.slice(-5).reverse().map((entry, i)=>(
<p key={i}>
[{entry.type.toUpperCase()}]
{entry.index!=null && ` #${entry.index+1}`}
@@ -493,11 +587,11 @@ export default function App() {
))}
</section>
{/* ADD POINTS OVERLAY */}
{/* Add Points Overlay */}
{showAddPointsOverlay && (
<div style={styles.overlay}>
<div style={styles.addPointsContainer}>
{addPointsStep === 1 && (
<div style={styles.addPointsDialog}>
{addPointsStep===1 && (
<>
<h3>Step 1: Enter Password</h3>
<input
@@ -509,44 +603,51 @@ export default function App() {
<button style={styles.button} onClick={checkPointsPassword}>
Check Password
</button>
<button style={{...styles.button, backgroundColor:'#999'}} onClick={closeAddPointsFlow}>
<button
style={{...styles.button, backgroundColor:'#999'}}
onClick={closeAddPointsFlow}
>
Cancel
</button>
</>
)}
{addPointsStep === 2 && (
{addPointsStep===2 && (
<>
<h3>Step 2: Choose Category</h3>
<div style={styles.buttonRow}>
<button style={styles.button} onClick={()=> pickAddPointsCategory('solo')}>
<button style={styles.button} onClick={()=>pickAddPointsCategory('solo')}>
Solo
</button>
<button style={styles.button} onClick={()=> pickAddPointsCategory('together')}>
<button style={styles.button} onClick={()=>pickAddPointsCategory('together')}>
Together
</button>
</div>
<button style={{...styles.button, backgroundColor:'#999'}} onClick={closeAddPointsFlow}>
<button
style={{...styles.button, backgroundColor:'#999'}}
onClick={closeAddPointsFlow}
>
Cancel
</button>
</>
)}
{addPointsStep === 3 && (
{addPointsStep===3 && (
<>
<h3>Step 3: How many points?</h3>
<div style={styles.buttonRow}>
{[1,2,3,4].map(amt=>(
{[1,2,3,4].map(amt => (
<button
key={amt}
style={styles.button}
onClick={()=> pickAddPointsAmount(amt)}
onClick={()=>pickAddPointsAmount(amt)}
>
+{amt}
</button>
))}
</div>
<button style={{...styles.button, backgroundColor:'#999'}} onClick={closeAddPointsFlow}>
<button
style={{...styles.button, backgroundColor:'#999'}}
onClick={closeAddPointsFlow}
>
Cancel
</button>
</>
@@ -561,7 +662,6 @@ export default function App() {
<h2>Admin Panel</h2>
<button style={styles.adminButton} onClick={()=>adminRequest('clear-logs')}>Clear Logs</button>
<button style={styles.adminButton} onClick={()=>adminRequest('reset-usage')}>Reset Usage</button>
{/* The new remove-all-earned points button */}
<button style={styles.adminButton} onClick={()=>adminRequest('remove-all-earned')}>
Remove All Earned
</button>
@@ -569,7 +669,9 @@ export default function App() {
<button style={styles.adminButton} onClick={()=>adminRequest('remove-earned')}>-1 (SoloUsed)</button>
<button style={styles.adminButton} onClick={()=>adminRequest('clear-timer')}>Clear Timer</button>
<button style={styles.adminButton} onClick={()=>adminRequest('expire-timer')}>Expire Timer (1s)</button>
<button style={styles.adminButton} onClick={handleAdminLogout}>Logout Admin</button>
<button style={styles.adminButton} onClick={handleAdminLogout}>
Logout Admin
</button>
</div>
):(
<div style={{marginTop:30}}>
@@ -583,7 +685,7 @@ export default function App() {
}
// -----------------------------
// STYLES
// STYLES
// -----------------------------
const styles = {
container: {
@@ -626,7 +728,7 @@ const styles = {
alignItems:'center',
zIndex:9999
},
addPointsContainer: {
addPointsDialog: {
backgroundColor:'#333',
padding:20,
borderRadius:8,
@@ -665,3 +767,5 @@ const styles = {
cursor:'pointer'
}
};
export default App;