mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
event popup!!
This commit is contained in:
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
@@ -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.
@@ -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.
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user