event popup!!

This commit is contained in:
legop3
2026-04-19 02:56:13 -04:00
parent 52352bd692
commit 5438873846
15 changed files with 139 additions and 13 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-CCaG2_WI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C3BucANX.css">
<script type="module" crossorigin src="/assets/index-Cxe8USDb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BquPV0pG.css">
</head>
<body>
<div id="root"></div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.
+10
View File
@@ -242,12 +242,22 @@ async function runRewardForButton(button) {
assignNewReward(button);
return;
}
const completedCount = Number.isFinite(button.count) ? button.count : 0;
const completedGoal = Number.isFinite(button.goal) ? button.goal : 0;
const ctx = buildRewardContext();
try {
await reward.run(ctx);
} catch (err) {
logger.warn('Reward execution failed', { rewardId: reward.id, error: err.message });
}
io.emit('buttonBox:rewardRun', {
buttonId: button.id,
rewardId: reward.id,
rewardName: reward.name || reward.id,
count: completedCount,
goal: completedGoal,
ts: Date.now(),
});
button.lastRewardAt = Date.now();
button.count = 0;
assignNewReward(button);
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.
+2
View File
@@ -36,6 +36,7 @@ import RoverQueuesPanel from './components/RoverQueuesPanel.jsx';
import VipPanel from './components/VipPanel.jsx';
import { useSession } from './context/SessionContext.jsx';
import ButtonBoxPanel from './components/ButtonBoxPanel.jsx';
import RewardRunOverlay from './components/RewardRunOverlay.jsx';
function useLayoutMode() {
const [mode, setMode] = useState(() => {
@@ -302,6 +303,7 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
{renderedLayout}
</main>
<AlertFeed />
<RewardRunOverlay />
<TurnAlertListener />
<ModeGateOverlay />
<HelpOverlay
+19
View File
@@ -167,6 +167,18 @@ export default function AdminPanel() {
}
};
const handleTestRewardOverlay = async () => {
window.dispatchEvent(
new CustomEvent('buttonBox:rewardRunLocalTest', {
detail: {
rewardName: 'Admin Mode',
count: 1000,
goal: 1000,
},
}),
);
};
useEffect(() => {
setGoalDraft(currentGoal);
}, [currentGoal]);
@@ -267,6 +279,13 @@ export default function AdminPanel() {
>
{serverRebooting ? 'Server rebooting...' : 'Reboot Server'}
</button>
<button
type="button"
onClick={handleTestRewardOverlay}
className="button-dark"
>
Test Reward Popup
</button>
</div>
<div className="space-y-0.5">
<div className="flex items-center justify-between text-xs text-slate-400">
+93
View File
@@ -0,0 +1,93 @@
import { useEffect, useRef, useState } from 'react';
import { useSocket } from '../context/SocketContext.jsx';
import { useSettingsNamespace } from '../settings/index.js';
import { AUDIO_SETTINGS_DEFAULTS } from '../settings/namespaces.js';
const SHOW_MS = 3200;
function normalizePayload(payload = {}) {
const rewardName = typeof payload.rewardName === 'string' && payload.rewardName.trim()
? payload.rewardName.trim()
: 'Unknown Event';
const goal = Number.isFinite(Number(payload.goal)) ? Math.max(1, Math.floor(Number(payload.goal))) : 0;
const count = Number.isFinite(Number(payload.count)) ? Math.max(0, Math.floor(Number(payload.count))) : goal;
return {
rewardName,
goal,
count,
ts: Date.now(),
};
}
export default function RewardRunOverlay() {
const socket = useSocket();
const { value: audioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
const masterVolume = Number.isFinite(audioSettings?.masterVolume)
? audioSettings.masterVolume
: AUDIO_SETTINGS_DEFAULTS.masterVolume;
const alertVolume = Number.isFinite(audioSettings?.alertVolume)
? audioSettings.alertVolume
: AUDIO_SETTINGS_DEFAULTS.alertVolume;
const effectiveAlertVolume = Math.max(0, Math.min(1, masterVolume * alertVolume));
const [overlay, setOverlay] = useState(null);
const audioRef = useRef(null);
useEffect(() => {
audioRef.current = new Audio('/tada.mp3');
audioRef.current.volume = effectiveAlertVolume;
audioRef.current.load();
}, [effectiveAlertVolume]);
useEffect(() => {
let timer = null;
function playSound() {
const audio = audioRef.current;
if (!audio) return;
audio.volume = effectiveAlertVolume;
audio.currentTime = 0;
audio.play().catch(() => {});
}
function onRewardRun(payload = {}) {
const next = normalizePayload(payload);
setOverlay(next);
playSound();
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
setOverlay(null);
}, SHOW_MS);
}
function onLocalRewardRun(event) {
onRewardRun(event?.detail || {});
}
socket.on('buttonBox:rewardRun', onRewardRun);
window.addEventListener('buttonBox:rewardRunLocalTest', onLocalRewardRun);
return () => {
if (timer) clearTimeout(timer);
socket.off('buttonBox:rewardRun', onRewardRun);
window.removeEventListener('buttonBox:rewardRunLocalTest', onLocalRewardRun);
};
}, [effectiveAlertVolume, socket]);
if (!overlay) return null;
return (
<div className="pointer-events-none fixed inset-0 z-[120] flex items-center justify-center bg-black/75 px-1 py-1">
<div className="pointer-events-auto w-full max-w-2xl">
<div className="surface rounded-none border-2 border-cyan-300 bg-amber-100/95 px-1 py-1 text-center text-black shadow-2xl">
<div className="flex items-center justify-center gap-1 border-2 border-blue-600 bg-yellow-100 px-0.5 py-1">
<img src="/party1.gif" alt="" className="hidden shrink-0 sm:block" />
<div>
<p className="font-mono text-2xl font-bold text-red-700 sm:text-3xl">
Running event: {overlay.rewardName}!
</p>
<p className="font-mono text-xl font-bold text-blue-800 sm:text-2xl">
{overlay.count}/{overlay.goal}
</p>
</div>
<img src="/party2.gif" alt="" className="hidden shrink-0 sm:block" />
</div>
</div>
</div>
</div>
);
}
+2
View File
@@ -14,6 +14,7 @@ import CommunityGoalBanner from '../components/CommunityGoalBanner.jsx';
import RoverQueuesPanel from '../components/RoverQueuesPanel.jsx';
import RawUserPilePanel from '../components/RawUserPilePanel.jsx';
import ButtonBoxPanel from '../components/ButtonBoxPanel.jsx';
import RewardRunOverlay from '../components/RewardRunOverlay.jsx';
function usePortraitLayout() {
const [isPortrait, setIsPortrait] = useState(() => {
@@ -220,6 +221,7 @@ function SpectatorContent() {
</section>
</main>
<AlertFeed />
<RewardRunOverlay />
</div>
);
}