production: docker + nginx config for rf-flux.com
Deploy to VPS / deploy (push) Has been cancelled

This commit is contained in:
2026-03-20 13:46:05 -05:00
parent b275b19f08
commit fc24313f15
187 changed files with 20977 additions and 767 deletions
+356
View File
@@ -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>
);
}