This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Globe, Moon, Sun, Sparkles, Menu, X, ChevronDown, ShoppingBag, UserCircle, Lock } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { Link, usePathname, useRouter } from "@/i18n/routing";
|
||||
import { useUIStore } from "@/lib/store/uiStore";
|
||||
|
||||
const NAV_KEYS = [
|
||||
{ key: "applications", href: "/#applications-deep" },
|
||||
{ key: "globalMap", href: "/#global" },
|
||||
{ key: "ourStory", href: "/#our-story" },
|
||||
{ key: "insideFlux", href: "/news" },
|
||||
{ key: "parts", href: "/parts" },
|
||||
];
|
||||
|
||||
const LOCALES = [
|
||||
{ code: "en", label: "EN" },
|
||||
{ code: "it", label: "IT" },
|
||||
{ code: "vec", label: "VEC" },
|
||||
{ code: "es", label: "ES" },
|
||||
{ code: "de", label: "DE" }
|
||||
];
|
||||
|
||||
export default function NavBar() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [isPastHero, setIsPastHero] = useState(false);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
const [isTranslating, setIsTranslating] = useState(false);
|
||||
const [isLangMenuOpen, setIsLangMenuOpen] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
const [logoLightError, setLogoLightError] = useState(false);
|
||||
const [logoDarkError, setLogoDarkError] = useState(false);
|
||||
|
||||
// 🔥 NUEVO ESTADO PARA SABER SI HAY SESIÓN B2B
|
||||
const [hasSession, setHasSession] = useState(false);
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const t = useTranslations("Navigation");
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 20);
|
||||
setIsPastHero(window.scrollY > window.innerHeight * 0.7);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
handleScroll();
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setIsLangMenuOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
// Verificar si existe la cookie "flux_b2b_session"
|
||||
const checkSession = () => {
|
||||
const cookies = document.cookie.split("; ");
|
||||
const sessionExists = cookies.some(c => c.startsWith("flux_b2b_session="));
|
||||
setHasSession(sessionExists);
|
||||
};
|
||||
checkSession();
|
||||
// Re-chequear cuando el modal dispare un refresh
|
||||
const interval = setInterval(checkSession, 2000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem("flux-theme");
|
||||
|
||||
if (pathname.includes("/heritage")) {
|
||||
setIsDark(true);
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
if (savedTheme === "dark") {
|
||||
setIsDark(true);
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
setIsDark(false);
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
if (pathname.startsWith("/hq-command")) return null;
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = isDark ? "light" : "dark";
|
||||
setIsDark(!isDark);
|
||||
if (newTheme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
localStorage.setItem("flux-theme", newTheme);
|
||||
};
|
||||
|
||||
const switchLanguage = (newLocale: string) => {
|
||||
setIsLangMenuOpen(false);
|
||||
if (newLocale === locale) return;
|
||||
|
||||
setIsTranslating(true);
|
||||
setTimeout(() => {
|
||||
router.replace(pathname, { locale: newLocale });
|
||||
setIsTranslating(false);
|
||||
setIsMobileMenuOpen(false);
|
||||
}, 600);
|
||||
};
|
||||
|
||||
const isDarkEsthetic = isDark || isMobileMenuOpen || !isPastHero;
|
||||
|
||||
// 🔥 ESTILO DINÁMICO PARA EL BOTÓN B2B
|
||||
const b2bButtonClass = scrolled
|
||||
? (isDarkEsthetic
|
||||
? "bg-white/10 text-white hover:bg-white/20 border border-white/10"
|
||||
: "bg-black/5 text-[#1D1D1F] hover:bg-black/10 border border-black/5")
|
||||
: "bg-black/60 text-white backdrop-blur-md hover:bg-black/80 border border-white/10 shadow-lg";
|
||||
|
||||
return (
|
||||
<header className={`fixed top-0 w-full z-50 transition-all duration-700 flex justify-center ${scrolled ? "pt-3" : "pt-6 md:pt-8"}`}>
|
||||
|
||||
<nav className={`relative flex items-center justify-between px-6 md:px-8 py-3 mx-4 w-full max-w-6xl rounded-full transition-all duration-700 ${
|
||||
scrolled ? (
|
||||
isDarkEsthetic
|
||||
? "bg-[#0A0A0C]/80 backdrop-blur-3xl border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.5)]"
|
||||
: "bg-white/70 backdrop-blur-2xl border border-white/60 shadow-[0_8px_32px_rgba(0,0,0,0.06)]"
|
||||
) : (
|
||||
isDarkEsthetic
|
||||
? "bg-[#0A0A0C]/40 backdrop-blur-xl border border-white/10 shadow-sm"
|
||||
: "bg-white/40 backdrop-blur-xl border border-white/40 shadow-sm"
|
||||
)
|
||||
}`}>
|
||||
|
||||
{/* LOGO LINK */}
|
||||
<Link href="/#technology" className="flex items-center gap-2 group z-50" onClick={() => setIsMobileMenuOpen(false)}>
|
||||
{!logoLightError ? (
|
||||
<img
|
||||
src="/flux-logo.svg"
|
||||
alt="FLUX Logo"
|
||||
className={`h-7 md:h-8 w-auto transition-all duration-500 group-hover:scale-105 ${isDarkEsthetic ? 'hidden' : 'block'}`}
|
||||
onError={() => setLogoLightError(true)}
|
||||
/>
|
||||
) : (
|
||||
<span className={`font-bold tracking-widest text-lg md:text-xl text-[#1D1D1F] ${isDarkEsthetic ? 'hidden' : 'block'}`}>FLUX</span>
|
||||
)}
|
||||
{!logoDarkError ? (
|
||||
<img
|
||||
src="/flux-logo.svg"
|
||||
alt="FLUX Logo Dark"
|
||||
className={`h-7 md:h-8 w-auto transition-all duration-500 group-hover:scale-105 ${isDarkEsthetic ? 'block' : 'hidden'}`}
|
||||
onError={() => setLogoDarkError(true)}
|
||||
/>
|
||||
) : (
|
||||
<span className={`font-bold tracking-widest text-lg md:text-xl text-white ${isDarkEsthetic ? 'block' : 'hidden'}`}>FLUX</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* ENLACES CENTRALES */}
|
||||
<ul className="hidden md:flex items-center relative z-10" onMouseLeave={() => setHoveredIndex(null)}>
|
||||
{NAV_KEYS.map((item, index) => {
|
||||
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href));
|
||||
const textClass = isDarkEsthetic
|
||||
? isActive ? "text-white" : "text-white/80 hover:text-white"
|
||||
: isActive ? "text-[#1D1D1F]" : "text-[#86868B hover:text-[#1D1D1F]";
|
||||
|
||||
return (
|
||||
<li key={item.key} className="relative" onMouseEnter={() => setHoveredIndex(index)}>
|
||||
<Link
|
||||
href={item.href as any}
|
||||
className={`relative px-5 py-2 text-sm font-medium transition-colors z-20 block ${textClass}`}
|
||||
>
|
||||
{t(item.key)}
|
||||
</Link>
|
||||
{hoveredIndex === index && (
|
||||
<motion.div
|
||||
layoutId="nav-hover"
|
||||
className={`absolute inset-0 rounded-full z-10 ${isDarkEsthetic ? "bg-white/[0.08]" : "bg-black/[0.04]"}`}
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{/* CONTROLES DERECHOS */}
|
||||
<div className={`hidden md:flex items-center gap-4 transition-colors z-10 ${isDarkEsthetic ? "text-white/80" : "text-[#86868B]"}`}>
|
||||
|
||||
{/* 🔥 BOTÓN DEL PORTAL B2B 🔥 */}
|
||||
<button
|
||||
onClick={() => window.dispatchEvent(new CustomEvent('flux:open-auth'))}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold uppercase tracking-widest transition-all ${b2bButtonClass}`}
|
||||
>
|
||||
{hasSession ? <UserCircle size={14} /> : <Lock size={14} />}
|
||||
{hasSession ? "Profile" : "B2B"}
|
||||
</button>
|
||||
|
||||
<div className={`w-px h-4 transition-colors ${isDarkEsthetic ? "bg-white/15" : "bg-black/10"}`}></div>
|
||||
|
||||
{/* BOTÓN DEL CARRITO */}
|
||||
<button
|
||||
onClick={() => useUIStore.getState().toggleCart()}
|
||||
className={`transition-colors relative w-6 h-6 flex items-center justify-center group ${isDarkEsthetic ? "text-white/80 hover:text-white" : "text-[#86868B] hover:text-[#1D1D1F]"}`}
|
||||
>
|
||||
<ShoppingBag size={18} strokeWidth={1.5} className="group-hover:scale-105 transition-transform" />
|
||||
<DynamicCartBadge />
|
||||
</button>
|
||||
|
||||
<div className={`w-px h-4 transition-colors ${isDarkEsthetic ? "bg-white/15" : "bg-black/10"}`}></div>
|
||||
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsLangMenuOpen(!isLangMenuOpen)} disabled={isTranslating}
|
||||
className={`flex items-center gap-1.5 transition-colors relative px-3 py-1.5 rounded-full ${isDarkEsthetic ? "bg-white/10 text-white hover:bg-white/20" : "bg-black/5 hover:bg-black/10 hover:text-[#1D1D1F]"}`}
|
||||
>
|
||||
{isTranslating ? (
|
||||
<motion.div animate={{ rotate: 360 }} transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}>
|
||||
<Sparkles size={14} className="text-[#00F0FF]" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<Globe size={14} strokeWidth={1.5} />
|
||||
)}
|
||||
<span className={`text-xs font-semibold tracking-widest uppercase ${isTranslating ? 'text-[#00F0FF]' : ''}`}>{locale}</span>
|
||||
<ChevronDown size={12} className={`transition-transform duration-300 ${isLangMenuOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isLangMenuOpen && (
|
||||
<motion.div initial={{ opacity: 0, y: 10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 10, scale: 0.95 }} transition={{ duration: 0.15 }}
|
||||
className={`absolute top-full right-0 mt-2 w-24 border shadow-xl overflow-hidden flex flex-col p-1 rounded-2xl ${isDarkEsthetic ? "bg-[#111] border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.4)]" : "bg-white border-black/5"}`}
|
||||
>
|
||||
{LOCALES.map(l => {
|
||||
const isActiveLocale = locale === l.code;
|
||||
const itemClass = isActiveLocale
|
||||
? isDarkEsthetic ? "bg-[#00F0FF]/15 text-[#00F0FF]" : "bg-[#0066CC]/10 text-[#0066CC]"
|
||||
: isDarkEsthetic ? "text-white/70 hover:bg-white/5 hover:text-white" : "text-[#86868B] hover:bg-black/5 hover:text-[#1D1D1F]";
|
||||
|
||||
return (
|
||||
<button key={l.code} onClick={() => switchLanguage(l.code)} className={`text-xs font-medium px-4 py-2 rounded-xl text-left transition-colors ${itemClass}`}>
|
||||
{l.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className={`w-px h-4 transition-colors ${isDarkEsthetic ? "bg-white/15" : "bg-black/10"}`}></div>
|
||||
|
||||
<button onClick={toggleTheme} className={`transition-colors relative w-5 h-5 flex items-center justify-center group ${isDarkEsthetic ? "text-white/80 hover:text-white" : "text-[#86868B] hover:text-[#1D1D1F]"}`}>
|
||||
<AnimatePresence mode="wait">
|
||||
{isDarkEsthetic ? (
|
||||
<motion.div key="sun" initial={{ opacity: 0, rotate: -90 }} animate={{ opacity: 1, rotate: 0 }} exit={{ opacity: 0, rotate: 90 }} className="absolute">
|
||||
<Sun size={18} strokeWidth={1.5} className="group-hover:text-[#00F0FF] transition-colors" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div key="moon" initial={{ opacity: 0, rotate: -90 }} animate={{ opacity: 1, rotate: 0 }} exit={{ opacity: 0, rotate: 90 }} className="absolute">
|
||||
<Moon size={18} strokeWidth={1.5} className="group-hover:text-[#0066CC] transition-colors" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* BOTÓN DE MENÚ MÓVIL */}
|
||||
<button className={`md:hidden z-50 p-2 transition-colors ${isDarkEsthetic ? "text-white" : "text-[#1D1D1F]"}`} onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}>
|
||||
{isMobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* MENÚ MÓVIL */}
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<motion.div initial={{ opacity: 0, y: -20, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -20, scale: 0.95 }} transition={{ duration: 0.2 }}
|
||||
className="absolute top-20 left-4 right-4 bg-[#0A0A0C]/95 backdrop-blur-3xl border border-white/10 rounded-3xl shadow-2xl p-6 flex flex-col gap-6 md:hidden z-40"
|
||||
>
|
||||
<ul className="flex flex-col gap-4">
|
||||
{NAV_KEYS.map((item) => (
|
||||
<li key={item.key}>
|
||||
<Link href={item.href as any} onClick={() => setIsMobileMenuOpen(false)} className="text-lg font-medium text-white block">
|
||||
{t(item.key)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="h-px w-full bg-white/10" />
|
||||
|
||||
{/* 🔥 BOTÓN B2B MÓVIL */}
|
||||
<button
|
||||
onClick={() => { setIsMobileMenuOpen(false); window.dispatchEvent(new CustomEvent('flux:open-auth')); }}
|
||||
className="flex items-center justify-center gap-2 bg-white/10 hover:bg-white/20 text-white px-4 py-3 rounded-xl text-sm font-semibold transition-colors"
|
||||
>
|
||||
{hasSession ? <UserCircle size={18} /> : <Lock size={18} />}
|
||||
{hasSession ? "B2B Profile" : "Access B2B Portal"}
|
||||
</button>
|
||||
|
||||
<div className="h-px w-full bg-white/10" />
|
||||
|
||||
<div>
|
||||
<span className="text-[10px] uppercase tracking-widest text-white/60 font-semibold mb-3 block">Language</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{LOCALES.map(l => (
|
||||
<button key={l.code} onClick={() => switchLanguage(l.code)} className={`px-4 py-2 rounded-xl text-xs font-semibold transition-all ${locale === l.code ? 'bg-[#00F0FF] text-black' : 'bg-white/5 text-white/70'}`}>
|
||||
{l.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-px w-full bg-white/10" />
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] uppercase tracking-widest text-white/60 font-semibold">Theme</span>
|
||||
<button onClick={toggleTheme} className="flex items-center gap-2 text-sm font-medium text-white/70 bg-white/5 px-4 py-2 rounded-xl">
|
||||
{isDark ? <><Sun size={16} /> Light Mode</> : <><Moon size={16} /> Dark Mode</>}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper para renderizar el contador del carrito
|
||||
function DynamicCartBadge() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const cartItems = useUIStore((state) => state.cartItems);
|
||||
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
|
||||
if (!mounted || cartItems.length === 0) return null;
|
||||
|
||||
const totalItems = cartItems.reduce((acc, item) => acc + item.quantity, 0);
|
||||
|
||||
return (
|
||||
<span className="absolute -top-1 -right-1 w-3.5 h-3.5 bg-rose-500 text-white text-[8px] font-bold flex items-center justify-center rounded-full border-2 border-transparent">
|
||||
{totalItems}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user