feat: add Beletage logo + interactive 3-logo mini-game with secret combo confetti
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user