mirror of
https://github.com/HugeFrog24/shakethefrog.git
synced 2026-03-02 08:24:33 +00:00
bugfix
This commit is contained in:
@@ -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<Heart[]>([]);
|
||||
|
||||
// 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 (
|
||||
<div className="absolute inset-0 pointer-events-none -z-10">
|
||||
<div className="absolute inset-0 pointer-events-none -z-10 overflow-hidden">
|
||||
{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));
|
||||
}}
|
||||
>
|
||||
<HeartIcon
|
||||
className="w-16 h-16 text-pink-500 opacity-80 animate-fade-out"
|
||||
|
||||
25
app/components/ParentComponent.tsx
Normal file
25
app/components/ParentComponent.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { SpeechBubble } from './SpeechBubble';
|
||||
import FrogImage from '../public/images/frog-ai.ai'; // Assuming this is the frog image
|
||||
|
||||
export function ParentComponent() {
|
||||
const [isShaken, setIsShaken] = useState(false);
|
||||
const [triggerCount, setTriggerCount] = useState(0);
|
||||
|
||||
const handleShake = () => {
|
||||
setTriggerCount(prev => prev + 1);
|
||||
setIsShaken(true);
|
||||
// Shake the frog for 1 second
|
||||
setTimeout(() => {
|
||||
setIsShaken(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FrogImage className={isShaken ? 'shake-animation' : ''} />
|
||||
<SpeechBubble isShaken={isShaken} triggerCount={triggerCount} />
|
||||
<button onClick={handleShake}>Shake the Frog</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
app/components/SpeechBubble.tsx
Normal file
83
app/components/SpeechBubble.tsx
Normal file
@@ -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<number>(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 (
|
||||
<div className="absolute -top-24 left-1/2 -translate-x-1/2 bg-white dark:bg-slate-800 px-4 py-2 rounded-xl shadow-lg animate-float z-20">
|
||||
<div className="relative">
|
||||
{message}
|
||||
{/* Triangle pointer */}
|
||||
<div className="absolute -bottom-6 left-1/2 -translate-x-1/2 w-0 h-0
|
||||
border-l-[8px] border-l-transparent
|
||||
border-r-[8px] border-r-transparent
|
||||
border-t-[8px] border-t-white
|
||||
dark:border-t-slate-800" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export function ThemeToggle() {
|
||||
return (
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className="fixed top-4 right-4 p-2 rounded-full bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
|
||||
className="fixed top-4 right-4 p-2 rounded-full bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors z-50"
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
{darkMode ? (
|
||||
|
||||
Reference in New Issue
Block a user