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,132 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import styles from "./Footer.module.css";
|
||||
|
||||
const PHONES = ["۰۲۵ - ۳۲۹۴۵۰۸۸", "۰۲۵ - ۳۲۹۴۵۳۵۰", "۰۲۵ - ۳۲۹۴۵۰۷۷"];
|
||||
|
||||
const faDigitsToLatin = (s) =>
|
||||
s.replace(/[۰-۹]/g, (d) => "۰۱۲۳۴۵۶۷۸۹".indexOf(d)).replace(/\D/g, "");
|
||||
|
||||
const INSURANCES = [
|
||||
"بیمه بانک تجارت",
|
||||
"بیمه بانک ملت",
|
||||
"بیمه سامان",
|
||||
"بیمه معلم",
|
||||
"بیمه نوین",
|
||||
"بیمه ایران",
|
||||
"بیمه کوثر",
|
||||
"بیمه دی",
|
||||
"بیمه دانا",
|
||||
];
|
||||
|
||||
const SOCIAL = [
|
||||
{ label: "اینستاگرام", href: "https://www.instagram.com/neginclinicc", icon: "instagram" },
|
||||
{ label: "آپارات", href: "https://www.aparat.com/v/3zqwp", icon: "aparat" },
|
||||
{ label: "تلگرام", href: "https://telegram.me/negin_clinic", icon: "telegram" },
|
||||
];
|
||||
|
||||
function Icon({ name }) {
|
||||
if (name === "instagram") {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<rect x="3" y="3" width="18" height="18" rx="5" />
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<circle cx="17.5" cy="6.5" r="1" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (name === "telegram") {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M21.9 4.3 18.7 19c-.2 1-.9 1.3-1.8.8l-4.9-3.6-2.4 2.3c-.3.3-.5.5-1 .5l.4-5 9-8.1c.4-.3-.1-.5-.6-.2L6.7 13 1.9 11.5c-1-.3-1-1 .2-1.5L20.6 3c.9-.3 1.6.2 1.3 1.3z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm-1.8 13.6V8.4L16 12l-5.8 3.6z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<div className={`container ${styles.grid}`}>
|
||||
<div className={styles.col}>
|
||||
<Image
|
||||
src="/assets/files/sJcexIa9R4.png"
|
||||
alt="کلینیک دندانپزشکی نگین"
|
||||
width={200}
|
||||
height={60}
|
||||
className={styles.logo}
|
||||
/>
|
||||
<p className={styles.tag}>
|
||||
کلینیک دندانپزشکی نگین، ارائهدهنده کلیه خدمات دندانپزشکی از جمله:
|
||||
ارتودنسی نامرئی، ایمپلنت و زیبایی.
|
||||
</p>
|
||||
<div className={styles.social}>
|
||||
{SOCIAL.map((s) => (
|
||||
<a
|
||||
key={s.icon}
|
||||
href={s.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={s.label}
|
||||
title={s.label}
|
||||
>
|
||||
<Icon name={s.icon} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.col}>
|
||||
<h6 className={styles.head}>تلفن</h6>
|
||||
<ul className={styles.list}>
|
||||
{PHONES.map((p) => (
|
||||
<li key={p}>
|
||||
<a dir="ltr" href={`tel:+98${faDigitsToLatin(p).replace(/^0/, "")}`}>
|
||||
{p}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<h6 className={styles.head}>آدرس</h6>
|
||||
<p className={styles.addr}>قم، بلوار امین، کوچه ۲۱، شماره ۵ (روبروی صدا و سیما)</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.col}>
|
||||
<h6 className={styles.head}>بیمههای طرف قرارداد</h6>
|
||||
<ul className={`${styles.list} ${styles.insList}`}>
|
||||
{INSURANCES.map((i) => (
|
||||
<li key={i}>{i}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={styles.col}>
|
||||
<h6 className={styles.head}>دسترسی سریع</h6>
|
||||
<ul className={styles.list}>
|
||||
<li><Link href="/services">خدمات کلینیک</Link></li>
|
||||
<li><Link href="/about-us">درباره ما</Link></li>
|
||||
<li><Link href="/contact-us">تماس با ما</Link></li>
|
||||
<li><Link href="/blog">مقالات</Link></li>
|
||||
<li><Link href="/virtual-tour">تور مجازی</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* bottom row — mirrors the Meshkee footer structure */}
|
||||
<div className={styles.copyRow}>
|
||||
<div className={`container ${styles.copyInner}`}>
|
||||
<span dir="ltr">© {new Date().getFullYear()} Meshkee — مشکی</span>
|
||||
<a href="https://www.meshkee.com" target="_blank" rel="noopener noreferrer">
|
||||
طراحی و توسعه توسط تیم تجارت الکترونیک مشکی
|
||||
<span className={styles.copyEn}>Designed & Developed By Meshkee E-Commerce Team</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
.footer {
|
||||
background: #f4f8fb;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 40px;
|
||||
padding: 70px 24px 50px;
|
||||
}
|
||||
|
||||
@media (min-width: 620px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 980px) {
|
||||
.grid {
|
||||
grid-template-columns: 1.4fr 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: auto;
|
||||
height: 56px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 2.1;
|
||||
margin: 0 0 18px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.social {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.social a {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--brand);
|
||||
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.social a:hover {
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.head {
|
||||
font-size: 16px;
|
||||
color: var(--heading);
|
||||
margin: 0 0 16px;
|
||||
position: relative;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.head::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
bottom: 0;
|
||||
width: 36px;
|
||||
height: 2px;
|
||||
background: var(--cyan);
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.list a:hover {
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.insList {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px 16px;
|
||||
}
|
||||
|
||||
.addr {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.copyRow {
|
||||
background: #fff;
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.copyInner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px 24px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.copyInner a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--heading);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.copyInner a:hover {
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.copyEn {
|
||||
font-family: var(--font-en);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.copyInner {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
.copyInner a {
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: box-shadow 0.25s ease, border-color 0.25s ease, background 0.25s ease;
|
||||
}
|
||||
|
||||
.headerScrolled {
|
||||
box-shadow: 0 8px 30px rgba(43, 67, 128, 0.08);
|
||||
border-bottom-color: var(--border);
|
||||
}
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: var(--header-h);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.brand img {
|
||||
width: auto;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.link {
|
||||
position: relative;
|
||||
padding: 10px 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--heading);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.linkActive {
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
.linkActive::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline: 14px;
|
||||
bottom: 2px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background: var(--brand);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.phone {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 16px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-tint);
|
||||
color: var(--brand);
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.phone:hover {
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.phone svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.userBtn {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--heading);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.userBtn:hover {
|
||||
border-color: var(--brand);
|
||||
color: var(--brand);
|
||||
background: var(--bg-tint);
|
||||
}
|
||||
|
||||
/* ---------- signed-in account menu ---------- */
|
||||
.account {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.accountBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 14px 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
color: var(--heading);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accountBtn:hover {
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.accountAvatar {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-tint);
|
||||
color: var(--brand);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.accountName {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.accountMenu {
|
||||
position: absolute;
|
||||
inset-inline-end: 0;
|
||||
top: calc(100% + 10px);
|
||||
min-width: 190px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
.accountMenu a,
|
||||
.accountMenu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
color: var(--heading);
|
||||
background: none;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accountMenu a:hover,
|
||||
.accountMenu button:hover {
|
||||
background: var(--bg-tint);
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.accountName {
|
||||
display: none;
|
||||
}
|
||||
.accountBtn {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.userBtn:hover {
|
||||
border-color: var(--brand);
|
||||
color: var(--brand);
|
||||
background: var(--bg-tint);
|
||||
}
|
||||
|
||||
.burger {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.burger span {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background: var(--heading);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.burger {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
top: 100%;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 2px;
|
||||
padding: 14px 24px 22px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-12px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.navOpen {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.link {
|
||||
padding: 13px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.linkActive::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.phoneNum {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.phone {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import styles from "./PageHero.module.css";
|
||||
|
||||
export default function PageHero({ fa, en, lead }) {
|
||||
return (
|
||||
<section className={styles.hero}>
|
||||
<div className="container">
|
||||
<span className={styles.en}>{en}</span>
|
||||
<h1 className={styles.fa}>{fa}</h1>
|
||||
{lead ? <p className={styles.lead}>{lead}</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
.hero {
|
||||
position: relative;
|
||||
padding: 46px 0 38px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
background-color: #f6fafd;
|
||||
background-repeat: no-repeat, repeat;
|
||||
background-position: center, center;
|
||||
background-size: cover, 26px 26px;
|
||||
background-image: radial-gradient(
|
||||
120% 160% at 15% -20%,
|
||||
rgba(59, 85, 154, 0.05),
|
||||
transparent 55%
|
||||
),
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Ccircle cx='2' cy='2' r='1.5' fill='%2322356b' fill-opacity='0.22'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.en {
|
||||
display: block;
|
||||
font-family: var(--font-en);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
font-size: 14px;
|
||||
color: var(--en-accent);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.fa {
|
||||
color: var(--heading);
|
||||
font-size: 27px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: 10px auto 0;
|
||||
max-width: 560px;
|
||||
font-size: 15px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
text-align-last: center;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.fa {
|
||||
font-size: 22px;
|
||||
}
|
||||
.hero {
|
||||
padding: 34px 0 28px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Fades/slides a block into place once it scrolls into view (or immediately
|
||||
* if it's already on screen at load — e.g. the hero). Pass `delay` (ms) to
|
||||
* stage a sequence, and `from` to pick the entry direction.
|
||||
*
|
||||
* Pure CSS transition under the hood (`.reveal` / `.reveal--in` in
|
||||
* globals.css) — this component only toggles the class at the right time,
|
||||
* so it stays cheap and respects `prefers-reduced-motion` for free.
|
||||
*/
|
||||
export default function Reveal({
|
||||
as: Tag = "div",
|
||||
from = "up", // "up" | "down" | "left" | "right" | "scale"
|
||||
delay = 0,
|
||||
once = true,
|
||||
className = "",
|
||||
style,
|
||||
children,
|
||||
...rest
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
if (typeof IntersectionObserver === "undefined") {
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true);
|
||||
if (once) io.unobserve(entry.target);
|
||||
} else if (!once) {
|
||||
setVisible(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold: 0.18, rootMargin: "0px 0px -10% 0px" }
|
||||
);
|
||||
io.observe(el);
|
||||
return () => io.disconnect();
|
||||
}, [once]);
|
||||
|
||||
return (
|
||||
<Tag
|
||||
ref={ref}
|
||||
className={`reveal reveal--${from} ${visible ? "reveal--in" : ""} ${className}`}
|
||||
style={{ "--reveal-delay": `${delay}ms`, ...style }}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user