feat: add Beletage logo + interactive 3-logo mini-game with secret combo confetti

This commit is contained in:
AI Assistant
2026-02-27 12:12:49 +02:00
parent 042e4e1108
commit dafbc1c69a
4 changed files with 178 additions and 27 deletions
+4
View File
@@ -23,6 +23,10 @@ export const COMPANIES: Record<CompanyId, Company> = {
color: "#22B5AB",
address: "str. Unirii, nr. 3, ap. 26",
city: "Cluj-Napoca",
logo: {
light: "/logos/logo-btg-light.svg",
dark: "/logos/logo-btg-dark.svg",
},
},
"urban-switch": {
id: "urban-switch",
+91 -27
View File
@@ -64,60 +64,124 @@ function NavItem({
);
}
const LOGO_COMPANIES = ["studii-de-teren", "urban-switch"] as const;
const LOGO_COMPANIES = ["beletage", "urban-switch", "studii-de-teren"] as const;
const ANIMATIONS = [
"animate-[spin_0.5s_ease-in-out]",
"animate-[bounce_0.5s_ease-in-out]",
"animate-[ping_0.4s_ease-in-out]",
] as const;
const COMPANY_GLOW: Record<string, string> = {
beletage: "hover:drop-shadow-[0_0_6px_#22B5AB]",
"urban-switch": "hover:drop-shadow-[0_0_6px_#6366f1]",
"studii-de-teren": "hover:drop-shadow-[0_0_6px_#f59e0b]",
};
// Secret combo: click logos in order BTG → US → SDT to trigger confetti
const SECRET_COMBO = ["beletage", "urban-switch", "studii-de-teren"];
function SidebarLogos() {
const [clickCount, setClickCount] = useState(0);
const [shuffled, setShuffled] = useState(false);
const [animatingId, setAnimatingId] = useState<string | null>(null);
const [comboProgress, setComboProgress] = useState(0);
const [showConfetti, setShowConfetti] = useState(false);
const handleLogoClick = useCallback(() => {
setClickCount((prev) => {
const next = prev + 1;
if (next >= 3) {
setShuffled((s) => !s);
return 0;
}
return next;
});
}, []);
// Reset click count after 2 seconds of inactivity
// Reset combo after 3 seconds of inactivity
useEffect(() => {
if (clickCount === 0) return;
const timer = setTimeout(() => setClickCount(0), 2000);
if (comboProgress === 0) return;
const timer = setTimeout(() => setComboProgress(0), 3000);
return () => clearTimeout(timer);
}, [clickCount]);
}, [comboProgress]);
const logos = shuffled ? [...LOGO_COMPANIES].reverse() : [...LOGO_COMPANIES];
// Hide confetti after 2 seconds
useEffect(() => {
if (!showConfetti) return;
const timer = setTimeout(() => setShowConfetti(false), 2000);
return () => clearTimeout(timer);
}, [showConfetti]);
const handleLogoClick = useCallback(
(companyId: string, e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// Trigger individual animation
setAnimatingId(companyId);
setTimeout(() => setAnimatingId(null), 500);
// Check secret combo
if (SECRET_COMBO[comboProgress] === companyId) {
const next = comboProgress + 1;
if (next >= SECRET_COMBO.length) {
setShowConfetti(true);
setComboProgress(0);
} else {
setComboProgress(next);
}
} else if (SECRET_COMBO[0] === companyId) {
setComboProgress(1);
} else {
setComboProgress(0);
}
},
[comboProgress],
);
return (
<div className="flex items-center gap-1.5">
{logos.map((companyId) => {
<div className="relative flex items-center gap-1">
{LOGO_COMPANIES.map((companyId, index) => {
const company = COMPANIES[companyId];
const logoSrc = company?.logo?.light;
if (!logoSrc) return null;
const isAnimating = animatingId === companyId;
const animation = isAnimating
? ANIMATIONS[index % ANIMATIONS.length]
: "";
const glow = COMPANY_GLOW[companyId] ?? "";
return (
<button
key={companyId}
type="button"
onClick={handleLogoClick}
onClick={(e) => handleLogoClick(companyId, e)}
className={cn(
"relative shrink-0 rounded-md p-0.5 transition-all duration-300 hover:scale-110 focus:outline-none",
shuffled && "animate-pulse",
"relative shrink-0 rounded-md p-0.5 transition-all duration-200 hover:scale-110 focus:outline-none",
glow,
isAnimating && animation,
)}
title={company.shortName}
>
<Image
src={logoSrc}
alt={company.shortName}
width={36}
height={36}
className="h-9 w-9 object-contain"
width={32}
height={32}
className="h-8 w-8 object-contain"
suppressHydrationWarning
/>
</button>
);
})}
{/* Confetti burst on secret combo */}
{showConfetti && (
<div className="pointer-events-none absolute -top-2 left-1/2 -translate-x-1/2 z-50">
<div className="flex gap-0.5 animate-[fadeIn_0.2s_ease-in]">
{["🎉", "✨", "🏗️", "✨", "🎉"].map((emoji, i) => (
<span
key={i}
className="text-sm animate-[bounce_0.6s_ease-in-out]"
style={{ animationDelay: `${i * 80}ms` }}
>
{emoji}
</span>
))}
</div>
<p className="mt-0.5 whitespace-nowrap text-center text-[9px] font-bold text-primary animate-[pulse_0.5s_ease-in-out_infinite]">
Echipa completă!
</p>
</div>
)}
</div>
);
}