- Achievements system with 6 badges and confetti celebration animation - Health recovery timeline showing 9 milestones from 20min to 1 year - Money savings tracker with cost configuration and goal progress - Daily reminder notifications with browser permission handling - New Prisma models: Achievement, ReminderSettings, SavingsConfig - API routes for all new features - Full dashboard integration with staggered animations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
119 lines
3.3 KiB
TypeScript
119 lines
3.3 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { BadgeDefinition } from '@/lib/storage';
|
|
import {
|
|
Trophy,
|
|
Footprints,
|
|
Flame,
|
|
Shield,
|
|
Swords,
|
|
Crown,
|
|
Sparkles,
|
|
} from 'lucide-react';
|
|
|
|
interface CelebrationAnimationProps {
|
|
badge: BadgeDefinition;
|
|
onComplete: () => void;
|
|
}
|
|
|
|
const iconMap: Record<string, React.ElementType> = {
|
|
Footprints,
|
|
Flame,
|
|
Shield,
|
|
Swords,
|
|
Crown,
|
|
Trophy,
|
|
};
|
|
|
|
export function CelebrationAnimation({
|
|
badge,
|
|
onComplete,
|
|
}: CelebrationAnimationProps) {
|
|
const [particles, setParticles] = useState<
|
|
Array<{ id: number; x: number; y: number; color: string; delay: number }>
|
|
>([]);
|
|
|
|
useEffect(() => {
|
|
// Generate confetti particles
|
|
const newParticles = Array.from({ length: 50 }, (_, i) => ({
|
|
id: i,
|
|
x: Math.random() * 100,
|
|
y: Math.random() * 100,
|
|
color: ['#fbbf24', '#a855f7', '#22c55e', '#3b82f6', '#ef4444'][
|
|
Math.floor(Math.random() * 5)
|
|
],
|
|
delay: Math.random() * 0.5,
|
|
}));
|
|
setParticles(newParticles);
|
|
|
|
// Auto dismiss after 3 seconds
|
|
const timer = setTimeout(() => {
|
|
onComplete();
|
|
}, 3000);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [onComplete]);
|
|
|
|
const Icon = iconMap[badge.icon] || Trophy;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
onClick={onComplete}
|
|
>
|
|
{/* Confetti particles */}
|
|
{particles.map((particle) => (
|
|
<div
|
|
key={particle.id}
|
|
className="absolute w-2 h-2 rounded-full animate-confetti"
|
|
style={{
|
|
left: `${particle.x}%`,
|
|
top: '-10px',
|
|
backgroundColor: particle.color,
|
|
animationDelay: `${particle.delay}s`,
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{/* Badge reveal */}
|
|
<div className="relative animate-scale-in">
|
|
{/* Glow effect */}
|
|
<div className="absolute inset-0 bg-yellow-500/30 rounded-full blur-3xl scale-150 animate-pulse-subtle" />
|
|
|
|
{/* Main content */}
|
|
<div className="relative bg-gradient-to-br from-purple-600 to-indigo-700 p-8 rounded-2xl border border-yellow-500/50 shadow-2xl">
|
|
<div className="flex flex-col items-center gap-4">
|
|
{/* Sparkles */}
|
|
<div className="absolute -top-4 -right-4">
|
|
<Sparkles className="h-8 w-8 text-yellow-400 animate-float" />
|
|
</div>
|
|
<div className="absolute -bottom-4 -left-4">
|
|
<Sparkles className="h-6 w-6 text-yellow-400 animate-float delay-300" />
|
|
</div>
|
|
|
|
{/* Badge icon */}
|
|
<div className="p-4 bg-gradient-to-br from-yellow-400 to-amber-500 rounded-full shadow-lg">
|
|
<Icon className="h-12 w-12 text-white" />
|
|
</div>
|
|
|
|
{/* Text */}
|
|
<div className="text-center">
|
|
<p className="text-yellow-400 text-sm font-medium uppercase tracking-wider mb-1">
|
|
Achievement Unlocked!
|
|
</p>
|
|
<h2 className="text-2xl font-bold text-white mb-1">{badge.name}</h2>
|
|
<p className="text-white/70 text-sm">{badge.description}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tap to dismiss hint */}
|
|
<p className="absolute bottom-8 text-white/50 text-sm">
|
|
Tap anywhere to dismiss
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|