diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index 6ca60b7..5f377de 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -65,7 +65,15 @@ export default function BlindboxRoomPage() { const boxControls = useAnimation(); const inputRef = useRef(null); - const confettiCanvasRef = useRef(null); + const timersRef = useRef[]>([]); + const confettiAliveRef = useRef(false); + + useEffect(() => { + return () => { + timersRef.current.forEach(clearTimeout); + confettiAliveRef.current = false; + }; + }, []); useEffect(() => { if (!isRegistered()) { @@ -121,7 +129,8 @@ export default function BlindboxRoomPage() { useEffect(() => { if (isMember && inputRef.current) { - setTimeout(() => inputRef.current?.focus(), 300); + const t = setTimeout(() => inputRef.current?.focus(), 300); + timersRef.current.push(t); } }, [isMember]); @@ -162,7 +171,7 @@ export default function BlindboxRoomPage() { setPoolCount((c) => c + 1); setMyIdeas((prev) => [{ id, content: text, createdAt: new Date().toISOString() }, ...prev]); setSubmitFlash(true); - setTimeout(() => setSubmitFlash(false), 600); + timersRef.current.push(setTimeout(() => setSubmitFlash(false), 600)); boxControls.start({ scale: [1, 1.08, 1], rotate: [0, -3, 3, 0], @@ -256,14 +265,15 @@ export default function BlindboxRoomPage() { const fireConfetti = () => { const colors = ["#a855f7", "#6366f1", "#ec4899", "#f59e0b", "#10b981"]; confetti({ particleCount: 100, spread: 120, origin: { y: 0.4 }, colors, startVelocity: 45, ticks: 250 }); + confettiAliveRef.current = true; const end = Date.now() + 3000; const frame = () => { - if (Date.now() > end) return; + if (Date.now() > end || !confettiAliveRef.current) return; confetti({ particleCount: 3, angle: 60, spread: 55, origin: { x: 0, y: 0.6 }, colors, startVelocity: 35, ticks: 150 }); confetti({ particleCount: 3, angle: 120, spread: 55, origin: { x: 1, y: 0.6 }, colors, startVelocity: 35, ticks: 150 }); requestAnimationFrame(frame); }; - setTimeout(frame, 200); + timersRef.current.push(setTimeout(frame, 200)); }; const { share, copyToClipboard } = useShare(); @@ -287,7 +297,7 @@ export default function BlindboxRoomPage() { const handleLeaveOrDelete = async () => { if (!confirmLeave) { setConfirmLeave(true); - setTimeout(() => setConfirmLeave(false), 3000); + timersRef.current.push(setTimeout(() => setConfirmLeave(false), 3000)); return; } if (leaving || !profile || !room) return; @@ -319,7 +329,6 @@ export default function BlindboxRoomPage() { return (
- {/* Header */}
diff --git a/src/components/ShareCardModal.tsx b/src/components/ShareCardModal.tsx index 1b379a8..93ad04b 100644 --- a/src/components/ShareCardModal.tsx +++ b/src/components/ShareCardModal.tsx @@ -38,21 +38,20 @@ export default function ShareCardModal({ const [imageDataUrl, setImageDataUrl] = useState(null); const [imageLoading, setImageLoading] = useState(false); + const imageSrc = data.type === "restaurant" ? data.restaurant.images?.[0] : undefined; + useEffect(() => { if (!open) { setImageDataUrl(null); return; } - if (data.type !== "restaurant") return; - - const src = data.restaurant.images?.[0]; - if (!src) return; + if (!imageSrc) return; setImageLoading(true); - loadImageAsDataUrl(src) + loadImageAsDataUrl(imageSrc) .then(setImageDataUrl) .finally(() => setImageLoading(false)); - }, [open, data]); + }, [open, imageSrc]); const handleGenerate = useCallback(async (): Promise => { if (!cardRef.current) return null;