Files
viberoom-web/src/widgets/space-focus-hud/ui/SpaceFocusHudWidget.tsx

156 lines
4.4 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
import { cn } from '@/shared/lib/cn';
import { useToast } from '@/shared/ui';
import { SpaceTimerHudWidget } from '@/widgets/space-timer-hud';
import { GoalCompleteSheet } from './GoalCompleteSheet';
import { GoalFlashOverlay } from './GoalFlashOverlay';
interface SpaceFocusHudWidgetProps {
goal: string;
timerLabel: string;
visible: boolean;
onGoalUpdate: (nextGoal: string) => void;
}
export const SpaceFocusHudWidget = ({
goal,
timerLabel,
visible,
onGoalUpdate,
}: SpaceFocusHudWidgetProps) => {
const { pushToast } = useToast();
const reducedMotion = useReducedMotion();
const [flashVisible, setFlashVisible] = useState(false);
const [sheetOpen, setSheetOpen] = useState(false);
const [completePulseVisible, setCompletePulseVisible] = useState(false);
const playbackStateRef = useRef<'running' | 'paused'>('running');
const flashTimerRef = useRef<number | null>(null);
const completePulseTimerRef = useRef<number | null>(null);
const triggerFlash = useCallback((durationMs: number) => {
if (reducedMotion || !visible) {
return;
}
setFlashVisible(true);
if (flashTimerRef.current) {
window.clearTimeout(flashTimerRef.current);
}
flashTimerRef.current = window.setTimeout(() => {
setFlashVisible(false);
flashTimerRef.current = null;
}, durationMs);
}, [reducedMotion, visible]);
useEffect(() => {
return () => {
if (flashTimerRef.current) {
window.clearTimeout(flashTimerRef.current);
flashTimerRef.current = null;
}
if (completePulseTimerRef.current) {
window.clearTimeout(completePulseTimerRef.current);
completePulseTimerRef.current = null;
}
};
}, []);
useEffect(() => {
if (!visible || reducedMotion) {
setFlashVisible(false);
return;
}
triggerFlash(2000);
}, [visible, reducedMotion, triggerFlash]);
useEffect(() => {
const ENABLE_PERIODIC_FLASH = false;
if (!visible || reducedMotion || !ENABLE_PERIODIC_FLASH) {
return;
}
const intervalId = window.setInterval(() => {
triggerFlash(800);
}, 10 * 60 * 1000);
return () => {
window.clearInterval(intervalId);
};
}, [visible, reducedMotion, triggerFlash]);
if (!visible) {
return null;
}
const handleOpenCompleteSheet = () => {
setCompletePulseVisible(true);
if (completePulseTimerRef.current) {
window.clearTimeout(completePulseTimerRef.current);
}
completePulseTimerRef.current = window.setTimeout(() => {
setCompletePulseVisible(false);
setSheetOpen(true);
completePulseTimerRef.current = null;
}, 700);
};
return (
<>
<GoalFlashOverlay goal={goal} visible={flashVisible} />
<div
className={cn(
'pointer-events-none fixed inset-x-0 z-20 flex justify-center px-4 transition-opacity duration-[240ms] ease-out motion-reduce:duration-0',
completePulseVisible ? 'opacity-100' : 'opacity-0',
)}
style={{ bottom: 'calc(env(safe-area-inset-bottom, 0px) + 5.1rem)' }}
aria-hidden
>
<div className="rounded-full border border-white/12 bg-black/24 px-3 py-1 text-xs text-white/84 backdrop-blur-md">
!
</div>
</div>
<SpaceTimerHudWidget
timerLabel={timerLabel}
goal={goal}
isImmersionMode
className="pr-[4.2rem]"
onGoalCompleteRequest={handleOpenCompleteSheet}
onPlaybackStateChange={(state) => {
if (reducedMotion) {
playbackStateRef.current = state;
return;
}
if (playbackStateRef.current === 'paused' && state === 'running') {
triggerFlash(1000);
}
playbackStateRef.current = state;
}}
/>
<GoalCompleteSheet
open={sheetOpen}
currentGoal={goal}
onClose={() => setSheetOpen(false)}
onRest={() => {
setSheetOpen(false);
pushToast({ title: '좋아요, 잠깐 쉬고 다시 이어가요.' });
}}
onConfirm={(nextGoal) => {
onGoalUpdate(nextGoal);
setSheetOpen(false);
pushToast({ title: '다음 한 조각으로 이어갑니다.' });
triggerFlash(1200);
}}
/>
</>
);
};