Next.js (App Router) clone of negindentalclinic.ir, wired to the Meshkee Website API (blog, contact form, favicon), with on-page SEO and a scroll-reveal animation sequence on the homepage. See context.md for full project context. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
194 lines
5.6 KiB
JavaScript
194 lines
5.6 KiB
JavaScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { CUSTOMER_PORTAL_URL, ADMIN_DASHBOARD_URL } from "@/lib/meshkee";
|
|
import { useMeshkeeSession, isAdminUser } from "@/lib/auth-client";
|
|
import styles from "./Header.module.css";
|
|
|
|
const NAV = [
|
|
{ href: "/services", label: "خدمات کلینیک" },
|
|
{ href: "/blog", label: "مقالات" },
|
|
{ href: "/virtual-tour", label: "تور مجازی" },
|
|
{ href: "/about-us", label: "درباره ما" },
|
|
{ href: "/contact-us", label: "تماس با ما" },
|
|
];
|
|
|
|
const CLINIC_PHONE_LABEL = "۰۲۵ - ۳۲۹۴۵۰۷۷";
|
|
const CLINIC_PHONE_TEL = "tel:+982532945077";
|
|
|
|
function PhoneIcon() {
|
|
return (
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M4 5c0-.6.4-1 1-1h3l2 5-2.5 1.5a12 12 0 0 0 5.5 5.5L16 15l5 2v3c0 .6-.4 1-1 1A17 17 0 0 1 4 5z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function UserIcon() {
|
|
return (
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
|
|
<circle cx="12" cy="8" r="4" />
|
|
<path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function ChevronIcon() {
|
|
return (
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="m6 9 6 6 6-6" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function userDisplayName(user) {
|
|
return (
|
|
user?.firstName ||
|
|
user?.name ||
|
|
user?.fullName ||
|
|
(user?.cellNumber ? user.cellNumber : "کاربر")
|
|
);
|
|
}
|
|
|
|
function AccountMenu() {
|
|
const { status, user, logout } = useMeshkeeSession();
|
|
const [open, setOpen] = useState(false);
|
|
const rootRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onDocClick = (e) => {
|
|
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
|
};
|
|
document.addEventListener("mousedown", onDocClick);
|
|
return () => document.removeEventListener("mousedown", onDocClick);
|
|
}, [open]);
|
|
|
|
if (status !== "in") {
|
|
// Signed out (or still loading): the icon just sends the shopper to
|
|
// the customer account portal to sign in / register.
|
|
return (
|
|
<a
|
|
className={styles.userBtn}
|
|
href={CUSTOMER_PORTAL_URL}
|
|
aria-label="ورود کاربران"
|
|
title="ورود کاربران"
|
|
>
|
|
<UserIcon />
|
|
</a>
|
|
);
|
|
}
|
|
|
|
const admin = isAdminUser(user);
|
|
|
|
return (
|
|
<div className={styles.account} ref={rootRef}>
|
|
<button
|
|
type="button"
|
|
className={styles.accountBtn}
|
|
aria-expanded={open}
|
|
onClick={() => setOpen((v) => !v)}
|
|
>
|
|
<span className={styles.accountAvatar}>
|
|
<UserIcon />
|
|
</span>
|
|
<span className={styles.accountName}>{userDisplayName(user)} عزیز</span>
|
|
<ChevronIcon />
|
|
</button>
|
|
|
|
{open ? (
|
|
<div className={styles.accountMenu} role="menu">
|
|
<a href={`${CUSTOMER_PORTAL_URL}/profile`} role="menuitem">
|
|
پروفایل من
|
|
</a>
|
|
{admin ? (
|
|
<a href={ADMIN_DASHBOARD_URL} role="menuitem">
|
|
مدیریت وبسایت
|
|
</a>
|
|
) : null}
|
|
<button type="button" role="menuitem" onClick={logout}>
|
|
خروج
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Header() {
|
|
const pathname = usePathname();
|
|
const [open, setOpen] = useState(false);
|
|
const [scrolled, setScrolled] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const onScroll = () => setScrolled(window.scrollY > 20);
|
|
onScroll();
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
return () => window.removeEventListener("scroll", onScroll);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setOpen(false);
|
|
}, [pathname]);
|
|
|
|
return (
|
|
<header className={`${styles.header} ${scrolled ? styles.headerScrolled : ""}`}>
|
|
<div className={`container ${styles.inner}`}>
|
|
<Link href="/" className={styles.brand} aria-label="کلینیک دندانپزشکی نگین">
|
|
<Image
|
|
src="/assets/img/Header/header.png"
|
|
alt="کلینیک دندانپزشکی نگین"
|
|
width={190}
|
|
height={57}
|
|
priority
|
|
/>
|
|
</Link>
|
|
|
|
<nav className={`${styles.nav} ${open ? styles.navOpen : ""}`}>
|
|
{NAV.map((item) => {
|
|
const active = pathname.startsWith(item.href);
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={`${styles.link} ${active ? styles.linkActive : ""}`}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
<div className={styles.actions}>
|
|
<a
|
|
className={styles.phone}
|
|
href={CLINIC_PHONE_TEL}
|
|
dir="ltr"
|
|
aria-label={`تماس با کلینیک ${CLINIC_PHONE_LABEL}`}
|
|
>
|
|
<PhoneIcon />
|
|
<span className={styles.phoneNum}>{CLINIC_PHONE_LABEL}</span>
|
|
</a>
|
|
|
|
<AccountMenu />
|
|
|
|
<button
|
|
type="button"
|
|
className={styles.burger}
|
|
aria-expanded={open}
|
|
aria-label="باز و بسته کردن منو"
|
|
onClick={() => setOpen((v) => !v)}
|
|
>
|
|
<span />
|
|
<span />
|
|
<span />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|