Files
react-editor-shopify/components/navigation/navigation.tsx
2026-05-05 14:04:51 -04:00

243 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Menu as MenuIcon, ShoppingBag, Search } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { useShopifyCart } from "@/hooks/use-shopify-cart";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
export type NavigationProps = {
brand: string;
logo?: string;
links: Array<{ label: string; href: string }>;
showSearch: "yes" | "no";
showCart: "yes" | "no";
sticky: "yes" | "no";
tone: "default" | "muted" | "inverse";
bannerText: string;
bannerTone: "default" | "accent" | "inverse";
};
export function Navigation({
brand,
logo,
links,
showSearch,
showCart,
sticky,
tone,
bannerText,
bannerTone,
}: NavigationProps) {
const [mobileOpen, setMobileOpen] = useState(false);
const [cartOpen, setCartOpen] = useState(false);
const cart = useShopifyCart();
const itemCount = cart?.itemCount ?? 0;
const toneClass: Record<NavigationProps["tone"], string> = {
default: "bg-background text-foreground border-b border-border",
muted: "bg-muted/40 text-foreground border-b border-border",
inverse: "bg-foreground text-background",
};
const bannerToneClass: Record<NavigationProps["bannerTone"], string> = {
default: "bg-muted text-foreground",
accent: "bg-primary text-primary-foreground",
inverse: "bg-foreground text-background",
};
const hasBanner = typeof bannerText === "string" && bannerText.trim().length > 0;
return (
<>
<div
className={cn(
"w-full",
sticky === "yes" && "sticky top-0 z-40",
)}
>
{hasBanner && (
<div className={cn("w-full", bannerToneClass[bannerTone])}>
<div className="container mx-auto max-w-7xl px-6 py-2 text-center text-xs tracking-wide md:text-sm">
{bannerText}
</div>
</div>
)}
<header
className={cn(
"w-full",
sticky === "yes" && "backdrop-blur",
toneClass[tone],
)}
>
<div className="container mx-auto flex h-16 max-w-7xl items-center justify-between px-6 md:h-20">
<Link
to="/"
className="inline-flex items-center font-semibold tracking-tight"
style={{ fontSize: "1.125rem", letterSpacing: "0.02em" }}
>
{logo ? (
<img
src={logo}
alt={brand || "Brand Logo"}
className="h-8 w-auto object-contain"
/>
) : (
brand || "Brand Logo"
)}
</Link>
<nav className="hidden items-center gap-8 md:flex">
{links.map((l) => (
<Link
key={l.href + l.label}
to={l.href}
className="text-sm tracking-wide opacity-80 transition-opacity hover:opacity-100"
>
{l.label}
</Link>
))}
</nav>
<div className="flex items-center gap-1">
{showSearch === "yes" && (
<button
aria-label="Search"
className="hidden h-10 w-10 items-center justify-center rounded-full transition-colors hover:bg-foreground/5 md:inline-flex"
>
<Search size={18} strokeWidth={1.5} />
</button>
)}
{showCart === "yes" && (
<button
onClick={() => setCartOpen(true)}
aria-label="Cart"
className="relative inline-flex h-10 w-10 items-center justify-center rounded-full transition-colors hover:bg-foreground/5"
>
<ShoppingBag size={18} strokeWidth={1.5} />
{itemCount > 0 && (
<span className="absolute -right-0.5 -top-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-foreground px-1 text-[10px] font-medium text-background">
{itemCount}
</span>
)}
</button>
)}
<button
onClick={() => setMobileOpen(true)}
aria-label="Menu"
className="inline-flex h-10 w-10 items-center justify-center rounded-full transition-colors hover:bg-foreground/5 md:hidden"
>
<MenuIcon size={20} strokeWidth={1.5} />
</button>
</div>
</div>
</header>
</div>
{/* Mobile menu */}
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetContent side="right" className="w-[88vw] max-w-sm">
<SheetHeader>
<SheetTitle className="text-left">{brand}</SheetTitle>
</SheetHeader>
<nav className="mt-6 flex flex-col gap-1">
{links.map((l) => (
<Link
key={l.href + l.label}
to={l.href}
className="rounded-md px-3 py-3 text-base hover:bg-muted"
>
{l.label}
</Link>
))}
</nav>
</SheetContent>
</Sheet>
{/* Cart drawer */}
<Sheet open={cartOpen} onOpenChange={setCartOpen}>
<SheetContent side="right" className="flex w-[92vw] max-w-md flex-col">
<SheetHeader>
<SheetTitle className="text-left">Cart</SheetTitle>
</SheetHeader>
<div className="-mx-6 mt-4 flex-1 overflow-y-auto px-6">
{cart?.items?.length ? (
<ul className="divide-y divide-border">
{cart.items.map((line: any) => (
<li key={line.id} className="flex gap-4 py-4">
<div className="aspect-square h-20 flex-shrink-0 overflow-hidden rounded-md bg-muted">
{line.merchandise?.product?.images?.edges?.[0]?.node?.url ? (
<img
src={line.merchandise.product.images.edges[0].node.url}
alt={line.merchandise.product.title}
className="h-full w-full object-cover"
/>
) : null}
</div>
<div className="flex flex-1 flex-col">
<p className="text-sm font-medium">{line.merchandise?.product?.title}</p>
{line.merchandise?.title && line.merchandise.title !== "Default Title" ? (
<p className="text-xs text-muted-foreground">{line.merchandise.title}</p>
) : null}
<div className="mt-auto flex items-center justify-between">
<div className="flex items-center gap-2 text-xs">
<button
onClick={() => cart.updateItemQuantity(line.id, line.quantity - 1)}
className="h-6 w-6 rounded-full border border-border hover:bg-muted"
>
</button>
<span>{line.quantity}</span>
<button
onClick={() => cart.updateItemQuantity(line.id, line.quantity + 1)}
className="h-6 w-6 rounded-full border border-border hover:bg-muted"
>
+
</button>
</div>
<span className="text-sm">
{line.merchandise?.price
? new Intl.NumberFormat("en-US", {
style: "currency",
currency: line.merchandise.price.currencyCode,
}).format(parseFloat(line.merchandise.price.amount) * line.quantity)
: null}
</span>
</div>
</div>
</li>
))}
</ul>
) : (
<div className="flex h-full flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground">
<ShoppingBag size={28} strokeWidth={1.25} />
<p>Your cart is empty.</p>
</div>
)}
</div>
{cart?.items?.length ? (
<div className="border-t border-border pt-4">
<div className="mb-4 flex items-center justify-between text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="font-medium">
{new Intl.NumberFormat("en-US", {
style: "currency",
currency: cart.cart?.cost?.totalAmount?.currencyCode ?? "USD",
}).format(cart.totalAmount)}
</span>
</div>
<a
href={cart.checkoutUrl ?? "#"}
className="inline-flex w-full items-center justify-center rounded-full bg-foreground px-4 py-3 text-sm font-medium text-background transition-opacity hover:opacity-90"
>
Checkout
</a>
</div>
) : null}
</SheetContent>
</Sheet>
</>
);
}