push notifs
This commit is contained in:
parent
ab91a53b15
commit
49b71ae1c7
125
package-lock.json
generated
125
package-lock.json
generated
@ -8,10 +8,13 @@
|
||||
"name": "untitled2",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"cra-template": "1.2.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-scripts": "5.0.1"
|
||||
"express": "^4.21.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-push": "^3.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@ -4672,6 +4675,17 @@
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1.js": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-types-flow": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
||||
@ -5058,6 +5072,11 @@
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
|
||||
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
@ -5194,6 +5213,11 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@ -5794,6 +5818,18 @@
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
@ -6650,6 +6686,14 @@
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@ -7687,7 +7731,6 @@
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@ -8828,6 +8871,14 @@
|
||||
"entities": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http_ece": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
|
||||
"integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/http-deceiver": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
|
||||
@ -10829,6 +10880,25 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
|
||||
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@ -16116,17 +16186,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"license": "Apache-2.0",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
@ -16421,6 +16490,44 @@
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/web-push": {
|
||||
"version": "3.6.7",
|
||||
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
|
||||
"integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
|
||||
"dependencies": {
|
||||
"asn1.js": "^5.3.0",
|
||||
"http_ece": "1.2.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"jws": "^4.0.0",
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"bin": {
|
||||
"web-push": "src/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/web-push/node_modules/agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/web-push/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
|
||||
|
@ -3,10 +3,13 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"cra-template": "1.2.0",
|
||||
"express": "^4.21.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-scripts": "5.0.1"
|
||||
"react-scripts": "5.0.1",
|
||||
"web-push": "^3.6.7"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@ -14,6 +17,7 @@
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"proxy": "http://localhost:4000",
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
|
45
public/service-worker.js
Normal file
45
public/service-worker.js
Normal file
@ -0,0 +1,45 @@
|
||||
/* public/service-worker.js
|
||||
|
||||
Listens to 'push' events and shows a notification.
|
||||
This file MUST be served via HTTPS or on localhost.
|
||||
*/
|
||||
|
||||
self.addEventListener('push', function(event) {
|
||||
if (!event.data) {
|
||||
return;
|
||||
}
|
||||
const payload = event.data.json(); // {title, body}
|
||||
const title = payload.title || 'Timer Notification';
|
||||
const body = payload.body || 'Time is up!';
|
||||
|
||||
const options = {
|
||||
body: body,
|
||||
icon: '/icon.png', // optional
|
||||
badge: '/badge.png' // optional
|
||||
// any other notification options
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title, options)
|
||||
);
|
||||
});
|
||||
|
||||
// Handle notification click
|
||||
self.addEventListener('notificationclick', function(event) {
|
||||
event.notification.close();
|
||||
// Focus or open the site
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type:'window' }).then( windowClients => {
|
||||
// if we already have a window open, focus it
|
||||
for (let client of windowClients) {
|
||||
if (client.url.includes('/PointTracker') && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
// otherwise, open a new one
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow('/');
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
333
server.js
333
server.js
@ -1,102 +1,124 @@
|
||||
/*
|
||||
server.js
|
||||
A single Node/Express server that:
|
||||
1) Manages timers, logs, admin routes (same logic as before).
|
||||
2) Serves the React build from "client/build".
|
||||
|
||||
Node/Express + web-push sample.
|
||||
- Timer logic: 15 min usage => ring => optional 2 min => ignore or finish up
|
||||
- Weekly reset for base arrays (soloUsed, togetherUsed)
|
||||
- Push Subscriptions in memory
|
||||
- Serves React build from "client/build"
|
||||
|
||||
Setup Steps:
|
||||
1) npm install express cors path web-push
|
||||
2) Generate VAPID keys: "npx web-push generate-vapid-keys"
|
||||
3) Replace placeholders below
|
||||
4) "npm run build" in your client folder, so "client/build" exists
|
||||
5) node server.js => listens on port 4000
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path'); // for serving the React build
|
||||
const path = require('path');
|
||||
const webPush = require('web-push');
|
||||
|
||||
// -- REPLACE WITH YOUR ACTUAL VAPID KEYS! --
|
||||
const VAPID_PUBLIC_KEY = 'BCn73fh1YZV3rFbK9H234he4nNWhhEKnYiQ_UZ1U0nxR6Q6cKvTG6v05Uyq7KXh0FxwjPOV3jHR3DPLqF5Lyfm4';
|
||||
const VAPID_PRIVATE_KEY = 'ysmxNfkY_V0CVBwL0UJb1BeYl0dgrF4vw09cNWfFW-M';
|
||||
|
||||
// Configure web-push
|
||||
webPush.setVapidDetails(
|
||||
'mailto:youremail@example.com',
|
||||
VAPID_PUBLIC_KEY,
|
||||
VAPID_PRIVATE_KEY
|
||||
);
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// ----- CONFIG -----
|
||||
const PORT = 80;
|
||||
// In-memory push subscription store
|
||||
// For a real app, store in DB
|
||||
let pushSubscriptions = [];
|
||||
|
||||
// Default base counts
|
||||
/* ------------------------------------------------------------------
|
||||
TIMER / WEEKLY RESET LOGIC
|
||||
------------------------------------------------------------------ */
|
||||
const SOLO_BASE_COUNT = 5;
|
||||
const TOGETHER_BASE_COUNT = 5;
|
||||
|
||||
// Passwords
|
||||
const POINTS_PASSWORD = 'ParentsOnly25'; // for adding points
|
||||
const ADMIN_PASSWORD = 'Bubb75i-i'; // for admin panel
|
||||
|
||||
// Timer durations (ms)
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
const TWO_MINUTES = 2 * 60 * 1000;
|
||||
|
||||
// ----- IN-MEMORY STATE -----
|
||||
// Start with 5 false for each
|
||||
let soloUsed = Array(SOLO_BASE_COUNT).fill(false);
|
||||
let togetherUsed = Array(TOGETHER_BASE_COUNT).fill(false);
|
||||
|
||||
// Logs: e.g. { type:'solo'|'together'|'movie'|'episode', index, usedAt:'ISOString' }
|
||||
let logs = [];
|
||||
|
||||
let lastReset = null;
|
||||
|
||||
// Timer state
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
const TWO_MINUTES = 2 * 60 * 1000;
|
||||
|
||||
let timerRunning = false;
|
||||
let timeRemaining = 0;
|
||||
let isPaused = false;
|
||||
let activeUsage = null; // e.g. { category:'solo'|'together', index }
|
||||
let activeUsage = null; // e.g. { category:'solo'|'together', index:number }
|
||||
let ringing = false;
|
||||
|
||||
// -----------------------------
|
||||
// Weekly Reset Logic
|
||||
// -----------------------------
|
||||
// Weekly reset to set all usage to false if new Monday
|
||||
function getMonday(d = new 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];
|
||||
}
|
||||
|
||||
function resetIfNewWeek() {
|
||||
const currentMonday = getMonday();
|
||||
if (!lastReset || lastReset !== currentMonday) {
|
||||
// Keep array lengths, reset all to false
|
||||
soloUsed = soloUsed.map(() => false);
|
||||
togetherUsed = togetherUsed.map(() => false);
|
||||
|
||||
lastReset = currentMonday;
|
||||
console.log('[Server] Weekly reset triggered:', currentMonday);
|
||||
console.log('[Server] Weekly reset triggered on Monday =>', currentMonday);
|
||||
}
|
||||
}
|
||||
|
||||
// Decrement the timer
|
||||
// Timer loop => decrement
|
||||
setInterval(() => {
|
||||
if (timerRunning && !isPaused && timeRemaining > 0) {
|
||||
timeRemaining -= 1000;
|
||||
if (timeRemaining <= 0) {
|
||||
// If finishing a 15-min => ring; if finishing 2-min => just end
|
||||
// Timer ended
|
||||
timerRunning = false;
|
||||
timeRemaining = 0;
|
||||
|
||||
// If we aren't already ringing, that means we ended the 15-min block => ring
|
||||
// If we ended a 15-min block => ring
|
||||
if (!ringing) {
|
||||
ringing = true;
|
||||
console.log('[Server] 15-minute timer ended => ringing=true');
|
||||
console.log('[Server] 15-minute ended => ringing=true');
|
||||
// Send push notification to all
|
||||
sendPushToAll('Timer Ended!', 'Time is up! Tap for details.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// -----------------------------
|
||||
// EXPRESS / MIDDLEWARE
|
||||
// -----------------------------
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
// Send push to all subscribed
|
||||
function sendPushToAll(title, body) {
|
||||
console.log('[Server] Sending push to', pushSubscriptions.length, 'subscribers');
|
||||
const payload = JSON.stringify({ title, body });
|
||||
|
||||
// Weekly reset check on each request
|
||||
pushSubscriptions.forEach((sub) => {
|
||||
webPush.sendNotification(sub, payload).catch((err) => {
|
||||
console.error('[Push Error]', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Middleware => check weekly reset on each request
|
||||
app.use((req, res, next) => {
|
||||
resetIfNewWeek();
|
||||
next();
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// API ROUTES
|
||||
// -----------------------------
|
||||
/* ------------------------------------------------------------------
|
||||
API ROUTES
|
||||
------------------------------------------------------------------ */
|
||||
|
||||
// GET /state => return current usage/timer
|
||||
app.get('/state', (req, res) => {
|
||||
res.json({
|
||||
soloUsed,
|
||||
@ -111,63 +133,50 @@ app.get('/state', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Single bubble => 15-min timer
|
||||
// POST /use-bubble => single bubble => 15 min
|
||||
app.post('/use-bubble', (req, res) => {
|
||||
const { category, index } = req.body;
|
||||
|
||||
if (timerRunning && !ringing && timeRemaining > 0) {
|
||||
return res.status(400).json({ error: 'A timer is already active.' });
|
||||
}
|
||||
// If ringing => auto-ignore
|
||||
// If currently ringing => ignore
|
||||
if (ringing) {
|
||||
timerRunning = false;
|
||||
timeRemaining = 0;
|
||||
isPaused = false;
|
||||
activeUsage = null;
|
||||
ringing = false;
|
||||
console.log('[Server] Timer was ringing => auto-ignored, new usage starts.');
|
||||
console.log('[Server] Timer was ringing => auto-ignored => new usage starts.');
|
||||
}
|
||||
const { category, index } = req.body;
|
||||
let arr = category === 'solo' ? soloUsed : category === 'together' ? togetherUsed : null;
|
||||
if (!arr) return res.status(400).json({ error: 'Invalid category' });
|
||||
if (index < 0 || index >= arr.length) return res.status(400).json({ error: 'Index out of range' });
|
||||
if (arr[index]) return res.status(400).json({ error: 'That bubble is already used.' });
|
||||
|
||||
let arr;
|
||||
if (category === 'solo') arr = soloUsed;
|
||||
else if (category === 'together') arr = togetherUsed;
|
||||
else return res.status(400).json({ error: 'Invalid category' });
|
||||
|
||||
if (index < 0 || index >= arr.length) {
|
||||
return res.status(400).json({ error: 'Index out of range' });
|
||||
}
|
||||
if (arr[index] === true) {
|
||||
return res.status(400).json({ error: 'That bubble is already used.' });
|
||||
}
|
||||
|
||||
// Mark used
|
||||
arr[index] = true;
|
||||
const usedAt = new Date().toISOString();
|
||||
logs.push({ type: category, index, usedAt });
|
||||
logs.push({ type: category, index, usedAt: new Date().toISOString() });
|
||||
|
||||
// Start 15-min
|
||||
timerRunning = true;
|
||||
timeRemaining = FIFTEEN_MINUTES;
|
||||
isPaused = false;
|
||||
activeUsage = { category, index };
|
||||
console.log(`[Server] ${category} #${index + 1} => 15-min started`);
|
||||
return res.json({ success: true });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Check password quickly
|
||||
// check password
|
||||
app.post('/check-password', (req, res) => {
|
||||
const { password } = req.body;
|
||||
if (password !== POINTS_PASSWORD) {
|
||||
if (password !== 'mySecretPassword') {
|
||||
return res.status(403).json({ error: 'Incorrect password.' });
|
||||
}
|
||||
return res.json({ success: true });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Add points => appends "amount" new false items to solo/together
|
||||
// add-points => append new false items
|
||||
app.post('/add-points', (req, res) => {
|
||||
const { password, category, amount } = req.body;
|
||||
if (password !== POINTS_PASSWORD) {
|
||||
return res.status(403).json({ error: 'Incorrect password.' });
|
||||
if (password !== 'mySecretPassword') {
|
||||
return res.status(403).json({ error: 'Wrong password' });
|
||||
}
|
||||
if (!['solo', 'together'].includes(category)) {
|
||||
return res.status(400).json({ error: 'Invalid category' });
|
||||
@ -175,19 +184,18 @@ app.post('/add-points', (req, res) => {
|
||||
if (typeof amount !== 'number' || amount < 1 || amount > 4) {
|
||||
return res.status(400).json({ error: 'Amount must be 1..4' });
|
||||
}
|
||||
|
||||
if (category === 'solo') {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
soloUsed.push(false);
|
||||
}
|
||||
console.log(`[Server] +${amount} appended to soloUsed => length now ${soloUsed.length}`);
|
||||
console.log(`[Server] +${amount} appended to soloUsed => length now`, soloUsed.length);
|
||||
} else {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
togetherUsed.push(false);
|
||||
}
|
||||
console.log(`[Server] +${amount} appended to togetherUsed => length now ${togetherUsed.length}`);
|
||||
console.log(`[Server] +${amount} appended to togetherUsed => length now`, togetherUsed.length);
|
||||
}
|
||||
return res.json({ success: true });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Pause/Resume
|
||||
@ -200,10 +208,10 @@ app.post('/pause-resume', (req, res) => {
|
||||
}
|
||||
isPaused = !isPaused;
|
||||
console.log('[Server] Timer pause =>', isPaused);
|
||||
return res.json({ success: true, isPaused });
|
||||
res.json({ success: true, isPaused });
|
||||
});
|
||||
|
||||
// Finish-up => only if ringing
|
||||
// finish-up => only if ringing
|
||||
app.post('/finish-up', (req, res) => {
|
||||
if (!ringing) {
|
||||
return res.status(400).json({ error: 'Not currently ringing. 2-min only after 15-min ends.' });
|
||||
@ -213,10 +221,10 @@ app.post('/finish-up', (req, res) => {
|
||||
isPaused = false;
|
||||
ringing = false;
|
||||
console.log('[Server] 2-min finish-up started');
|
||||
return res.json({ success: true });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Cancel finish-up
|
||||
// cancel-finish-up
|
||||
app.post('/cancel-finish-up', (req, res) => {
|
||||
if (!timerRunning || ringing || timeRemaining > TWO_MINUTES) {
|
||||
return res.status(400).json({ error: 'Not currently in finish-up timer.' });
|
||||
@ -226,10 +234,10 @@ app.post('/cancel-finish-up', (req, res) => {
|
||||
isPaused = false;
|
||||
activeUsage = null;
|
||||
console.log('[Server] 2-min finish-up canceled');
|
||||
return res.json({ success: true });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Ignore ring => if ringing
|
||||
// ignore-ring
|
||||
app.post('/ignore-ring', (req, res) => {
|
||||
if (!ringing) {
|
||||
return res.status(400).json({ error: 'Not currently ringing.' });
|
||||
@ -240,122 +248,145 @@ app.post('/ignore-ring', (req, res) => {
|
||||
activeUsage = null;
|
||||
ringing = false;
|
||||
console.log('[Server] Ring ignored => no 2-min started');
|
||||
return res.json({ success: true });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Movie => user picks 4
|
||||
// use-movie
|
||||
app.post('/use-movie', (req, res) => {
|
||||
const { chosenPoints } = req.body;
|
||||
if (!Array.isArray(chosenPoints) || chosenPoints.length !== 4) {
|
||||
return res.status(400).json({ error: 'Must pick exactly 4 points' });
|
||||
}
|
||||
for (const p of chosenPoints) {
|
||||
let arr = p.category === 'solo' ? soloUsed : p.category === 'together' ? togetherUsed : null;
|
||||
if (!arr) return res.status(400).json({ error: 'Invalid category in chosen points.' });
|
||||
if (arr[p.index] === true) {
|
||||
return res.status(400).json({ error: 'A chosen point is already used.' });
|
||||
}
|
||||
let arr = p.category==='solo'?soloUsed: p.category==='together'?togetherUsed:null;
|
||||
if (!arr) return res.status(400).json({ error:'Invalid category in chosen points.' });
|
||||
if (arr[p.index] === true) return res.status(400).json({ error:'A chosen point is already used.' });
|
||||
}
|
||||
for (const p of chosenPoints) {
|
||||
let arr = p.category === 'solo' ? soloUsed : togetherUsed;
|
||||
let arr = p.category==='solo'?soloUsed: togetherUsed;
|
||||
arr[p.index] = true;
|
||||
}
|
||||
const usedAt = new Date().toISOString();
|
||||
logs.push({ type: 'movie', index: null, usedAt });
|
||||
console.log('[Server] Movie => used 4 chosen points at', usedAt);
|
||||
return res.json({ success: true });
|
||||
logs.push({ type:'movie', index:null, usedAt:new Date().toISOString() });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Episode => user picks 2
|
||||
// use-episode
|
||||
app.post('/use-episode', (req, res) => {
|
||||
const { chosenPoints } = req.body;
|
||||
if (!Array.isArray(chosenPoints) || chosenPoints.length !== 2) {
|
||||
return res.status(400).json({ error: 'Must pick exactly 2 points' });
|
||||
}
|
||||
for (const p of chosenPoints) {
|
||||
let arr = p.category === 'solo' ? soloUsed : p.category === 'together' ? togetherUsed : null;
|
||||
if (!arr) return res.status(400).json({ error: 'Invalid category in chosen points.' });
|
||||
if (arr[p.index] === true) {
|
||||
return res.status(400).json({ error: 'A chosen point is already used.' });
|
||||
}
|
||||
let arr = p.category==='solo'?soloUsed: p.category==='together'?togetherUsed:null;
|
||||
if (!arr) return res.status(400).json({ error:'Invalid category in chosen points.' });
|
||||
if (arr[p.index]) return res.status(400).json({ error:'A chosen point is already used.' });
|
||||
}
|
||||
for (const p of chosenPoints) {
|
||||
let arr = p.category === 'solo' ? soloUsed : togetherUsed;
|
||||
arr[p.index] = true;
|
||||
let arr = p.category==='solo'?soloUsed: togetherUsed;
|
||||
arr[p.index]=true;
|
||||
}
|
||||
const usedAt = new Date().toISOString();
|
||||
logs.push({ type: 'episode', index: null, usedAt });
|
||||
console.log('[Server] Episode => used 2 points at', usedAt);
|
||||
return res.json({ success: true });
|
||||
logs.push({ type:'episode', index:null, usedAt:new Date().toISOString()});
|
||||
res.json({ success:true });
|
||||
});
|
||||
|
||||
// ----- ADMIN -----
|
||||
app.post('/admin/login', (req, res) => {
|
||||
const { password } = req.body;
|
||||
if (password !== ADMIN_PASSWORD) {
|
||||
return res.status(403).json({ error: 'Wrong admin password' });
|
||||
/* ------------------------------------------------------------------
|
||||
PUSH NOTIFICATION ROUTES
|
||||
------------------------------------------------------------------ */
|
||||
// POST /subscribe => client sends { subscription }
|
||||
app.post('/subscribe', (req, res)=>{
|
||||
const sub = req.body.subscription;
|
||||
// Check if already in pushSubscriptions?
|
||||
pushSubscriptions.push(sub);
|
||||
console.log('[Server] New subscription => total', pushSubscriptions.length);
|
||||
res.json({ success:true });
|
||||
});
|
||||
|
||||
// POST /unsubscribe => remove from array
|
||||
app.post('/unsubscribe', (req, res)=>{
|
||||
const sub = req.body.subscription;
|
||||
pushSubscriptions = pushSubscriptions.filter(s => s.endpoint!==sub.endpoint);
|
||||
console.log('[Server] Unsubscribe => total', pushSubscriptions.length);
|
||||
res.json({ success:true });
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
ADMIN
|
||||
------------------------------------------------------------------ */
|
||||
app.post('/admin/login',(req,res)=>{
|
||||
const { password }=req.body;
|
||||
if(password!=='adminSecret'){
|
||||
return res.status(403).json({ error:'Wrong admin password' });
|
||||
}
|
||||
return res.json({ success: true });
|
||||
res.json({ success:true });
|
||||
});
|
||||
app.post('/admin/clear-logs', (req, res) => {
|
||||
logs = [];
|
||||
|
||||
app.post('/admin/clear-logs',(req,res)=>{
|
||||
logs=[];
|
||||
console.log('[Server] Logs cleared by admin');
|
||||
res.json({ success: true });
|
||||
res.json({ success:true });
|
||||
});
|
||||
app.post('/admin/reset-usage', (req, res) => {
|
||||
soloUsed = soloUsed.map(() => false);
|
||||
togetherUsed = togetherUsed.map(() => false);
|
||||
console.log('[Server] Usage reset by admin => all set false, arrays kept');
|
||||
res.json({ success: true });
|
||||
|
||||
app.post('/admin/reset-usage',(req,res)=>{
|
||||
soloUsed=soloUsed.map(()=>false);
|
||||
togetherUsed=togetherUsed.map(()=>false);
|
||||
console.log('[Server] Usage reset by admin => all set false');
|
||||
res.json({ success:true });
|
||||
});
|
||||
app.post('/admin/remove-all-earned', (req, res) => {
|
||||
// keep only first 5 in each array
|
||||
|
||||
app.post('/admin/remove-all-earned',(req,res)=>{
|
||||
// revert arrays to length 5
|
||||
soloUsed.splice(5);
|
||||
togetherUsed.splice(5);
|
||||
console.log('[Server] All earned points removed => each array back to length 5');
|
||||
res.json({ success: true });
|
||||
console.log('[Server] All earned points removed => each array length=5');
|
||||
res.json({ success:true });
|
||||
});
|
||||
app.post('/admin/add-earned', (req, res) => {
|
||||
|
||||
// For demonstration, add/remove 1 from soloUsed
|
||||
app.post('/admin/add-earned',(req,res)=>{
|
||||
soloUsed.push(false);
|
||||
console.log('[Server] Admin +1 to soloUsed => length now', soloUsed.length);
|
||||
res.json({ success: true });
|
||||
console.log('[Server] Admin +1 to soloUsed => length', soloUsed.length);
|
||||
res.json({ success:true });
|
||||
});
|
||||
app.post('/admin/remove-earned', (req, res) => {
|
||||
if (soloUsed.length <= 0) {
|
||||
return res.status(400).json({ error: 'No bubble to remove in soloUsed' });
|
||||
app.post('/admin/remove-earned',(req,res)=>{
|
||||
if(soloUsed.length<=0){
|
||||
return res.status(400).json({ error:'No bubble to remove in soloUsed' });
|
||||
}
|
||||
soloUsed.pop();
|
||||
console.log('[Server] Admin removed last from soloUsed => length now', soloUsed.length);
|
||||
res.json({ success: true });
|
||||
console.log('[Server] Admin removed last => length', soloUsed.length);
|
||||
res.json({ success:true });
|
||||
});
|
||||
app.post('/admin/clear-timer', (req, res) => {
|
||||
timerRunning = false;
|
||||
timeRemaining = 0;
|
||||
isPaused = false;
|
||||
activeUsage = null;
|
||||
ringing = false;
|
||||
|
||||
app.post('/admin/clear-timer',(req,res)=>{
|
||||
timerRunning=false;
|
||||
timeRemaining=0;
|
||||
isPaused=false;
|
||||
activeUsage=null;
|
||||
ringing=false;
|
||||
console.log('[Server] Timer cleared by admin');
|
||||
res.json({ success: true });
|
||||
res.json({ success:true });
|
||||
});
|
||||
app.post('/admin/expire-timer', (req, res) => {
|
||||
if (timeRemaining > 1) {
|
||||
timeRemaining = 1;
|
||||
|
||||
app.post('/admin/expire-timer',(req,res)=>{
|
||||
if(timeRemaining>1){
|
||||
timeRemaining=1;
|
||||
console.log('[Server] Timer => set to 1s => about to end');
|
||||
}
|
||||
res.json({ success: true });
|
||||
res.json({ success:true });
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// SERVE REACT BUILD
|
||||
// -----------------------------
|
||||
app.use(express.static(path.join(__dirname, 'build')));
|
||||
/* ------------------------------------------------------------------
|
||||
SERVE REACT BUILD
|
||||
------------------------------------------------------------------ */
|
||||
app.use(express.static(path.join(__dirname,'build')));
|
||||
|
||||
// Catch-all to serve index.html for any unknown route
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'build', 'index.html'));
|
||||
app.get('*',(req,res)=>{
|
||||
res.sendFile(path.join(__dirname,'build','index.html'));
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
/* ------------------------------------------------------------------
|
||||
START SERVER
|
||||
------------------------------------------------------------------ */
|
||||
const PORT = 4000;
|
||||
app.listen(PORT, ()=>{
|
||||
console.log(`[Server] Listening on port ${PORT}`);
|
||||
});
|
||||
|
600
src/App.js
600
src/App.js
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user