140 lines
6.7 KiB
TypeScript
140 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { AnimatePresence, motion, useMotionValue, useSpring, useTransform } from "framer-motion";
|
|
import { Menu, X } from "lucide-react";
|
|
import Image from "next/image";
|
|
import { type MouseEvent, type PointerEvent, useState } from "react";
|
|
import { business, type Locale } from "@/config/business";
|
|
import type { Messages } from "@/messages";
|
|
import LanguageSwitcher from "./LanguageSwitcher";
|
|
import PhysicsButton from "./PhysicsButton";
|
|
|
|
const navCtaClassName =
|
|
"rounded-full border border-white/45 bg-[linear-gradient(180deg,#565b63_0%,#20242a_48%,#111317_100%)] font-semibold text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.38),inset_0_-10px_18px_rgba(0,0,0,0.18),0_14px_32px_rgba(17,19,23,0.18)] transition-colors hover:bg-[linear-gradient(180deg,#476a95_0%,#264b73_54%,#173655_100%)]";
|
|
|
|
export default function Navbar({ locale, onLocaleChange, t, whatsappUrl }: { locale: Locale; onLocaleChange: (locale: Locale) => void; t: Messages; whatsappUrl: string }) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
function scrollToSection(event: MouseEvent<HTMLAnchorElement>, href: string) {
|
|
if (!href.startsWith("#")) return;
|
|
event.preventDefault();
|
|
const target = document.querySelector(href);
|
|
if (!target) return;
|
|
setOpen(false);
|
|
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
window.history.replaceState(null, "", href);
|
|
}
|
|
|
|
return (
|
|
<header className="fixed inset-x-0 top-0 z-50 px-4 pt-4 max-[374px]:px-2.5 sm:px-6">
|
|
<nav className="glass premium-glass nav-glass relative mx-auto flex max-w-7xl items-center justify-start gap-2 overflow-hidden rounded-full px-3 py-3 max-[374px]:px-2 max-[374px]:py-2.5 md:justify-between sm:px-5" aria-label="Main navigation">
|
|
<a href="#home" onClick={(event) => scrollToSection(event, "#home")} className="relative z-[3] flex shrink-0 items-center gap-3" aria-label="New Optic home">
|
|
<span className="relative grid size-10 place-items-center overflow-hidden rounded-full bg-white shadow-sm max-[374px]:size-9">
|
|
<Image src={business.assets.logo} alt="New Optic logo" fill sizes="40px" className="object-contain p-1" priority />
|
|
</span>
|
|
<span className="hidden text-sm font-semibold tracking-[-0.02em] xs:inline sm:inline">{business.name}</span>
|
|
</a>
|
|
|
|
<div className="relative z-[3] hidden items-center gap-7 lg:flex">
|
|
{t.nav.links.map((link) => (
|
|
<StretchNavLink key={link.href} href={link.href} label={link.label} onClick={(event) => scrollToSection(event, link.href)} />
|
|
))}
|
|
</div>
|
|
|
|
<div className="relative z-[3] hidden items-center gap-3 md:flex">
|
|
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} />
|
|
<PhysicsButton href={whatsappUrl} external className={`${navCtaClassName} px-5 py-2.5`}>
|
|
{t.nav.cta}
|
|
</PhysicsButton>
|
|
</div>
|
|
|
|
<div className="relative z-[3] flex min-w-0 flex-1 items-center gap-1.5 max-[374px]:gap-0.5 sm:gap-2 md:hidden">
|
|
<LanguageSwitcher locale={locale} onLocaleChange={onLocaleChange} className="w-[132px] shrink-0 bg-white/72 backdrop-blur-xl max-[374px]:w-[106px] max-[374px]:p-0.5" buttonClassName="flex-1 px-0.5 max-[374px]:px-0 max-[374px]:py-1.5 sm:px-3" />
|
|
<PhysicsButton href={whatsappUrl} external className={`${navCtaClassName} min-w-0 flex-1 px-2.5 py-2 text-[11px] max-[374px]:px-1.5 max-[374px]:py-1.5 max-[374px]:text-[9px] sm:px-5 sm:py-2.5 sm:text-sm`}>
|
|
{t.nav.cta}
|
|
</PhysicsButton>
|
|
<motion.button
|
|
type="button"
|
|
onClick={() => setOpen((value) => !value)}
|
|
whileTap={{ scale: 0.94 }}
|
|
transition={{ duration: 0.08 }}
|
|
className="grid size-11 shrink-0 place-items-center rounded-full bg-white/70 text-ink outline-none transition max-[374px]:size-9 focus-visible:ring-2 focus-visible:ring-optical/45 focus-visible:ring-offset-2"
|
|
aria-label={t.nav.menu}
|
|
aria-expanded={open}
|
|
>
|
|
{open ? <X size={18} /> : <Menu size={18} />}
|
|
</motion.button>
|
|
</div>
|
|
</nav>
|
|
|
|
<AnimatePresence>
|
|
{open ? (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -8, scale: 0.985 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: -6, scale: 0.99 }}
|
|
transition={{ duration: 0.13, ease: "easeOut" }}
|
|
className="relative mx-auto mt-3 max-w-7xl overflow-hidden rounded-[2rem] border border-ink/8 bg-white p-4 shadow-glass md:hidden"
|
|
>
|
|
<div className="grid gap-2">
|
|
{t.nav.links.map((link) => (
|
|
<motion.a
|
|
key={link.href}
|
|
href={link.href}
|
|
onClick={(event) => scrollToSection(event, link.href)}
|
|
initial={{ opacity: 0, x: locale === "ar" ? -8 : 8 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.1, ease: "easeOut" }}
|
|
whileTap={{ scale: 0.98 }}
|
|
className="rounded-2xl px-4 py-3 text-sm font-semibold text-ink/72 hover:bg-smoke"
|
|
>
|
|
{link.label}
|
|
</motion.a>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
) : null}
|
|
</AnimatePresence>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
function StretchNavLink({ href, label, onClick }: { href: string; label: string; onClick: (event: MouseEvent<HTMLAnchorElement>) => void }) {
|
|
const pullX = useMotionValue(0);
|
|
const pullY = useMotionValue(0);
|
|
const x = useSpring(pullX, { stiffness: 260, damping: 16, mass: 0.3 });
|
|
const y = useSpring(pullY, { stiffness: 260, damping: 16, mass: 0.3 });
|
|
const scaleX = useTransform(x, [-18, 0, 18], [1.12, 1, 1.12]);
|
|
const scaleY = useTransform(y, [-12, 0, 12], [0.94, 1, 0.94]);
|
|
|
|
function handlePointerMove(event: PointerEvent<HTMLAnchorElement>) {
|
|
if (event.pointerType === "touch") return;
|
|
|
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
const localX = event.clientX - bounds.left - bounds.width / 2;
|
|
const localY = event.clientY - bounds.top - bounds.height / 2;
|
|
|
|
pullX.set(Math.max(-18, Math.min(18, localX * 0.42)));
|
|
pullY.set(Math.max(-12, Math.min(12, localY * 0.38)));
|
|
}
|
|
|
|
function release() {
|
|
pullX.set(0);
|
|
pullY.set(0);
|
|
}
|
|
|
|
return (
|
|
<motion.a
|
|
href={href}
|
|
onClick={onClick}
|
|
onPointerMove={handlePointerMove}
|
|
onPointerLeave={release}
|
|
onPointerCancel={release}
|
|
style={{ x, y, scaleX, scaleY }}
|
|
className="inline-flex min-h-9 items-center rounded-full px-1 text-sm font-medium text-ink/58 will-change-transform transition-colors hover:text-ink"
|
|
>
|
|
{label}
|
|
</motion.a>
|
|
);
|
|
}
|