mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
event popup!!
This commit is contained in:
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