- Replace hardcoded hex colors with gold/gold-light/gold-dark Tailwind tokens - Extract Schedule into DayCard, ScheduleFilters, MobileSchedule sub-components - Extract Team into TeamCarousel, TeamMemberInfo sub-components - Add UI_CONFIG for centralized magic numbers (timings, thresholds) - Add reusable IconBadge component, simplify Contact section - Convert Pricing clickable divs to semantic buttons for a11y - Remove unused SocialLinks, btn-outline, btn-ghost, nav-link CSS classes - Fix React setState-during-render error in TeamCarousel (deferred update pattern) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
52 lines
1.2 KiB
TypeScript
52 lines
1.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { UI_CONFIG } from "@/lib/config";
|
|
|
|
interface Heart {
|
|
id: number;
|
|
left: number;
|
|
size: number;
|
|
delay: number;
|
|
duration: number;
|
|
opacity: number;
|
|
}
|
|
|
|
export function FloatingHearts() {
|
|
const [hearts, setHearts] = useState<Heart[]>([]);
|
|
|
|
useEffect(() => {
|
|
const generated: Heart[] = Array.from({ length: UI_CONFIG.team.floatingHeartsCount }, (_, i) => ({
|
|
id: i,
|
|
left: Math.random() * 100,
|
|
size: 8 + Math.random() * 16,
|
|
delay: Math.random() * 10,
|
|
duration: 10 + Math.random() * 15,
|
|
opacity: 0.03 + Math.random() * 0.08,
|
|
}));
|
|
setHearts(generated);
|
|
}, []);
|
|
|
|
if (hearts.length === 0) return null;
|
|
|
|
return (
|
|
<div className="pointer-events-none absolute inset-0 overflow-hidden">
|
|
{hearts.map((heart) => (
|
|
<div
|
|
key={heart.id}
|
|
className="absolute text-gold"
|
|
style={{
|
|
left: `${heart.left}%`,
|
|
bottom: "-20px",
|
|
fontSize: `${heart.size}px`,
|
|
opacity: heart.opacity,
|
|
animation: `heart-float ${heart.duration}s ease-in ${heart.delay}s infinite`,
|
|
}}
|
|
>
|
|
♥
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|