42d4cb896a
Public cafe discovery app:
- SEO-optimised pages: home, /cafe/[slug], /search, /city/[city]
- AI search bar with natural language queries
- Structured data (JSON-LD) for Google rich results
- City browsing, rating/filter sidebar, similar cafes
- Review listing, full menu preview, working-hours card
- Web App Manifest + offline fallback page (PWA)
- Next.js 16: params/searchParams typed as Promise<{}>
- Fix Lucide icon title→aria-label (type removed upstream)
- "use client" on offline page (onClick handler)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
106 lines
3.7 KiB
TypeScript
106 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useTranslations, useLocale } from "next-intl";
|
|
import { useRouter, usePathname } from "next/navigation";
|
|
import { Search, Menu, X, Globe, MapPin } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export function Navbar() {
|
|
const t = useTranslations("nav");
|
|
const locale = useLocale();
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const otherLocale = locale === "fa" ? "en" : "fa";
|
|
const otherPath = pathname.replace(`/${locale}`, `/${otherLocale}`);
|
|
|
|
const links = [
|
|
{ href: `/${locale}`, label: t("home") },
|
|
{ href: `/${locale}/search`, label: t("search") },
|
|
];
|
|
|
|
return (
|
|
<header className="sticky top-0 z-40 w-full border-b border-gray-100 bg-white/95 backdrop-blur-sm">
|
|
<div className="mx-auto flex h-14 max-w-7xl items-center gap-3 px-4 sm:px-6">
|
|
{/* Logo */}
|
|
<a href={`/${locale}`} className="flex items-center gap-2 shrink-0">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-brand-700">
|
|
<MapPin className="h-4 w-4 text-white" />
|
|
</div>
|
|
<span className="text-base font-bold text-gray-900">
|
|
{locale === "fa" ? "میزییاب" : "Meezi Finder"}
|
|
</span>
|
|
</a>
|
|
|
|
{/* Desktop nav */}
|
|
<nav className="hidden flex-1 items-center gap-1 sm:flex ms-4">
|
|
{links.map((l) => (
|
|
<a
|
|
key={l.href}
|
|
href={l.href}
|
|
className={cn(
|
|
"rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
|
|
pathname === l.href
|
|
? "bg-brand-50 text-brand-700"
|
|
: "text-gray-600 hover:bg-gray-100"
|
|
)}
|
|
>
|
|
{l.label}
|
|
</a>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="flex flex-1 items-center justify-end gap-2 sm:flex-none">
|
|
{/* Search shortcut */}
|
|
<a
|
|
href={`/${locale}/search`}
|
|
className="flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-400 transition hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700 sm:min-w-[180px]"
|
|
>
|
|
<Search className="h-3.5 w-3.5 shrink-0" />
|
|
<span className="hidden sm:inline">
|
|
{locale === "fa" ? "جستجوی کافه..." : "Search cafes..."}
|
|
</span>
|
|
</a>
|
|
|
|
{/* Language toggle */}
|
|
<a
|
|
href={otherPath}
|
|
className="flex items-center gap-1 rounded-lg px-2 py-1.5 text-xs font-medium text-gray-500 transition hover:bg-gray-100"
|
|
title={otherLocale === "fa" ? "فارسی" : "English"}
|
|
>
|
|
<Globe className="h-3.5 w-3.5" />
|
|
<span>{otherLocale === "fa" ? "FA" : "EN"}</span>
|
|
</a>
|
|
|
|
{/* Mobile menu toggle */}
|
|
<button
|
|
className="rounded-lg p-1.5 text-gray-500 hover:bg-gray-100 sm:hidden"
|
|
onClick={() => setOpen(!open)}
|
|
aria-label={open ? t("closeMenu") : t("openMenu")}
|
|
>
|
|
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile menu */}
|
|
{open && (
|
|
<div className="border-t border-gray-100 bg-white px-4 py-3 sm:hidden">
|
|
{links.map((l) => (
|
|
<a
|
|
key={l.href}
|
|
href={l.href}
|
|
onClick={() => setOpen(false)}
|
|
className="block rounded-lg px-3 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
|
>
|
|
{l.label}
|
|
</a>
|
|
))}
|
|
</div>
|
|
)}
|
|
</header>
|
|
);
|
|
}
|