Initial commit: Negin Dental Clinic website
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>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user