From 52ddcd3db9b21907561bd1530827ea8b72a0ce96 Mon Sep 17 00:00:00 2001 From: HugeFrog24 <62775760+HugeFrog24@users.noreply.github.com> Date: Wed, 15 Jan 2025 03:40:11 +0100 Subject: [PATCH] bugfix --- app/components/FloatingHearts.tsx | 71 ++++++++++-------- app/components/ParentComponent.tsx | 25 +++++++ app/components/SpeechBubble.tsx | 83 +++++++++++++++++++++ app/components/ThemeToggle.tsx | 2 +- app/config/messages.ts | 89 +++++++++++++++++++++++ app/config/shake.ts | 32 +++++++++ app/globals.css | 34 ++++++++- app/hooks/useIsMobile.ts | 20 ++++++ app/layout.tsx | 2 - app/page.tsx | 111 ++++++++++++++++++----------- 10 files changed, 394 insertions(+), 75 deletions(-) create mode 100644 app/components/ParentComponent.tsx create mode 100644 app/components/SpeechBubble.tsx create mode 100644 app/config/messages.ts create mode 100644 app/config/shake.ts create mode 100644 app/hooks/useIsMobile.ts diff --git a/app/components/FloatingHearts.tsx b/app/components/FloatingHearts.tsx index a9b33f5..91e1c91 100644 --- a/app/components/FloatingHearts.tsx +++ b/app/components/FloatingHearts.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { HeartIcon } from '@heroicons/react/24/solid'; +import { shakeConfig } from '../config/shake'; interface Heart { id: number; @@ -9,6 +10,7 @@ interface Heart { speed: number; startPosition: { x: number; y: number }; scale: number; + createdAt: number; } interface FloatingHeartsProps { @@ -18,56 +20,66 @@ interface FloatingHeartsProps { export function FloatingHearts({ intensity }: FloatingHeartsProps) { const [hearts, setHearts] = useState([]); + // Cleanup interval to remove old hearts + useEffect(() => { + const cleanupInterval = setInterval(() => { + const now = Date.now(); + setHearts(prev => prev.filter(heart => + now - heart.createdAt < shakeConfig.animations.heartFloat + )); + }, shakeConfig.hearts.cleanupInterval); + + return () => clearInterval(cleanupInterval); + }, []); + useEffect(() => { if (intensity <= 0) return; // Number of hearts based on intensity - const numHearts = Math.min(Math.floor(intensity * 2), 50); + const numHearts = Math.min(Math.floor(intensity * 2), shakeConfig.hearts.maxPerShake); // Create waves of hearts - const waves = 4; // Number of waves - const heartsPerWave = Math.ceil(numHearts / waves); - const waveDelay = 200; // Delay between waves in ms + const heartsPerWave = Math.ceil(numHearts / shakeConfig.hearts.waves); const timers: NodeJS.Timeout[] = []; // Generate hearts in waves - for (let wave = 0; wave < waves; wave++) { + for (let wave = 0; wave < shakeConfig.hearts.waves; wave++) { const timer = setTimeout(() => { - const newHearts = Array.from({ length: heartsPerWave }, (_, i) => { - const totalIndex = wave * heartsPerWave + i; - return { - id: Date.now() + totalIndex, - // Distribute angles evenly within each wave - angle: Math.random() * 360, // Random angle for full radial distribution - speed: 0.8 + Math.random() * 0.4, - startPosition: { - x: Math.random() * 40 - 20, - y: Math.random() * 40 - 20, - }, - scale: 0.8 + Math.random() * 0.4, - }; - }); + const now = Date.now(); + const newHearts = Array.from({ length: heartsPerWave }, (_, i) => ({ + id: now + i, + angle: Math.random() * 360, + speed: shakeConfig.hearts.minSpeed + + Math.random() * (shakeConfig.hearts.maxSpeed - shakeConfig.hearts.minSpeed), + startPosition: { + x: Math.random() * (shakeConfig.hearts.spreadX * 2) - shakeConfig.hearts.spreadX, + y: Math.random() * (shakeConfig.hearts.spreadY * 2) - shakeConfig.hearts.spreadY, + }, + scale: shakeConfig.hearts.minScale + + Math.random() * (shakeConfig.hearts.maxScale - shakeConfig.hearts.minScale), + createdAt: now, + })); setHearts(prev => [...prev, ...newHearts]); - }, wave * waveDelay); + + // Remove this wave's hearts after animation + const cleanupTimer = setTimeout(() => { + setHearts(prev => prev.filter(heart => heart.createdAt !== now)); + }, shakeConfig.animations.heartFloat); + + timers.push(cleanupTimer); + }, wave * shakeConfig.hearts.waveDelay); timers.push(timer); } - // Remove hearts after animation completes - const cleanupTimer = setTimeout(() => { - setHearts(prev => prev.filter(heart => heart.id > Date.now() - 3500)); - }, waves * waveDelay + 3500); - - timers.push(cleanupTimer); - return () => { timers.forEach(timer => clearTimeout(timer)); }; }, [intensity]); return ( -
+
{hearts.map((heart) => { const style = { '--angle': `${heart.angle}deg`, @@ -82,6 +94,9 @@ export function FloatingHearts({ intensity }: FloatingHeartsProps) { key={heart.id} className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 animate-float-heart" style={style} + onAnimationEnd={() => { + setHearts(prev => prev.filter(h => h.id !== heart.id)); + }} > { + setTriggerCount(prev => prev + 1); + setIsShaken(true); + // Shake the frog for 1 second + setTimeout(() => { + setIsShaken(false); + }, 1000); + }; + + return ( +
+ + + +
+ ); +} \ No newline at end of file diff --git a/app/components/SpeechBubble.tsx b/app/components/SpeechBubble.tsx new file mode 100644 index 0000000..85d84d2 --- /dev/null +++ b/app/components/SpeechBubble.tsx @@ -0,0 +1,83 @@ +import { useEffect, useState, useCallback, useRef } from 'react'; +import { frogMessages } from '../config/messages'; + +// Increase visibility duration for speech bubbles +const VISIBILITY_MS = 3000; // 3 seconds for message visibility +const COOLDOWN_MS = 2000; // 2 seconds between new messages + +interface SpeechBubbleProps { + isShaken: boolean; + triggerCount: number; +} + +export function SpeechBubble({ isShaken, triggerCount }: SpeechBubbleProps) { + const [message, setMessage] = useState(''); + const [isVisible, setIsVisible] = useState(false); + const lastTriggerTime = useRef(0); + const showTimeRef = useRef(0); + const getRandomMessage = useCallback(() => { + const randomIndex = Math.floor(Math.random() * frogMessages.length); + return frogMessages[randomIndex]; + }, []); + + // Handle showing new messages + useEffect(() => { + if (triggerCount === 0) return; // Skip initial mount + + const now = Date.now(); + const timeSinceLastMessage = now - lastTriggerTime.current; + + // Show new message if cooldown has expired + if (timeSinceLastMessage >= COOLDOWN_MS) { + lastTriggerTime.current = now; + showTimeRef.current = now; + const newMessage = getRandomMessage(); + setMessage(newMessage); + setIsVisible(true); + } + }, [triggerCount, getRandomMessage]); + + // Handle visibility duration + useEffect(() => { + if (!isVisible) return; + + const checkVisibility = setInterval(() => { + const now = Date.now(); + const timeVisible = now - showTimeRef.current; + + if (timeVisible >= VISIBILITY_MS) { + setIsVisible(false); + } + }, 100); // Check every 100ms + + return () => { + clearInterval(checkVisibility); + }; + }, [isVisible]); + + // Uncomment and modify the useEffect to control visibility based on isShaken prop + // This will make the speech bubble stay visible even after shaking stops + useEffect(() => { + if (!isShaken) { + // Don't hide the speech bubble when shaking stops + // The visibility duration timer will handle hiding it + return; + } + }, [isShaken]); + + if (!isVisible) return null; + + return ( +
+
+ {message} + {/* Triangle pointer */} +
+
+
+ ); +} diff --git a/app/components/ThemeToggle.tsx b/app/components/ThemeToggle.tsx index 6172f85..43a81c0 100644 --- a/app/components/ThemeToggle.tsx +++ b/app/components/ThemeToggle.tsx @@ -9,7 +9,7 @@ export function ThemeToggle() { return ( - ) : motionPermission === 'granted' ? ( - "Shake your device, press spacebar, or click/tap frog!" - ) : ( - "Press spacebar or click/tap frog!" - )} -

+
+ +
+
+ +
+
+ +
+ + Frog +
+
+
+

+ {motionPermission === 'prompt' ? ( + + ) : motionPermission === 'granted' ? ( + `Shake your device${!isMobile ? ', press spacebar,' : ''} or click/tap frog!` + ) : ( + `${!isMobile ? 'Press spacebar or ' : ''}Click/tap frog!` + )} +

+
+
); }