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:
2026-09-04 18:28:54 +03:30
co-authored by Claude Sonnet 5
commit f16b47966b
70 changed files with 4810 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
.intro,
.care {
display: grid;
gap: 40px;
align-items: center;
}
@media (min-width: 860px) {
.intro,
.care {
grid-template-columns: 1fr 1fr;
}
}
.intro h2,
.care h2 {
font-size: 28px;
margin: 6px 0 16px;
}
.intro > div:not(.introArt),
.care > div:not(.careArt) {
display: flex;
flex-direction: column;
}
/* push the section CTA to the left edge of the text column */
.intro :global(.btn),
.care :global(.btn) {
align-self: flex-end;
}
.introArt img,
.careArt img {
width: 100%;
height: auto;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
}
.adv {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 28px;
border-inline-start: 4px solid var(--cyan);
}
.adv h3 {
font-size: 19px;
margin: 0 0 10px;
}
.adv p {
margin: 0;
color: var(--muted);
font-size: 15px;
}
.insurances {
display: flex;
flex-wrap: wrap;
gap: 14px;
justify-content: center;
}
.insurances li {
background: var(--bg-tint);
color: var(--brand);
font-weight: 700;
padding: 12px 26px;
border-radius: 999px;
font-size: 15px;
}
+151
View File
@@ -0,0 +1,151 @@
import Image from "next/image";
import Link from "next/link";
import PageHero from "@/components/PageHero";
import styles from "./about.module.css";
export const metadata = {
title: "درباره ما",
description:
"درباره کلینیک دندانپزشکی نگین در قم؛ کادر مجرب، مواد باکیفیت، دسترسی آسان و قرارداد با بیمه‌های معتبر و بانک‌ها.",
};
const INSURANCES = [
"دانا",
"نوین",
"ایران",
"بانک ملت",
"معلم",
"کوثر",
"بانک تجارت",
"دی",
"سامان",
];
const ADVANTAGES = [
{
title: "دسترسی آسان",
text: "قرار گرفتن کلینیک در یکی از اصلی‌ترین خیابان‌های قم و دسترسی آسان از تمامی نقاط شهر.",
},
{
title: "مواد با کیفیت",
text: "استفاده از مواد و تجهیزات دندانپزشکی با کیفیت و مطابق با استانداردهای روز دنیا.",
},
{
title: "کادر مجرب",
text: "کادر مجرب دندانپزشکی و متخصصان عضو هیئت علمی دانشکده دندانپزشکی.",
},
{
title: "ویژه بانوان و کودکان",
text: "محیطی آرام و مناسب برای بانوان و کودکان جهت ارائه خدمات تخصصی و عمومی.",
},
];
export default function AboutPage() {
return (
<>
<PageHero
en="About Us"
fa="درباره ما"
lead="درباره کلینیک نگین بیشتر بدانید…"
/>
<section className="section">
<div className={`container ${styles.intro}`}>
<div className={styles.introArt}>
<Image
src="/assets/img/info3-2.jpg"
alt="کلینیک دندانپزشکی نگین"
width={720}
height={540}
/>
</div>
<div>
<span className="sec-head__en">More About Our Clinic</span>
<h2>درباره کلینیک بیشتر بدانید</h2>
<p>
دسترسی آسان از تمامی نقاط قم به کلینیک و قرار گرفتن آن در یکی از
اصلی‌ترین خیابان‌های شهر، از مزیت‌های بارز این کلینیک است. استفاده
از مواد با کیفیت و کادر مجرب دندانپزشکی از دیگر مزیت‌های کلینیک
دندانپزشکی نگین است.
</p>
<p>
کلینیک نگین با بهره‌گیری از متخصصین مجرب و محیطی آرام و مناسب برای
بانوان و کودکان، آماده پاسخگویی و ارائه خدمت به مراجعین محترم می‌باشد.
</p>
<Link href="/contact-us" className="btn btn--cyan">
با ما در تماس باشید
</Link>
</div>
</div>
</section>
<section className="section section--tint">
<div className="container">
<div className="sec-head">
<span className="sec-head__en">Why Negin</span>
<h2 className="sec-head__fa">مزیت‌های کلینیک نگین</h2>
</div>
<div className="grid grid--2">
{ADVANTAGES.map((a) => (
<article key={a.title} className={styles.adv}>
<h3>{a.title}</h3>
<p>{a.text}</p>
</article>
))}
</div>
</div>
</section>
<section className="section">
<div className="container">
<div className="sec-head">
<span className="sec-head__en">Insurance &amp; Banks</span>
<h2 className="sec-head__fa">طرف قرارداد با بیمه‌های معتبر و بانک‌ها</h2>
<p className="sec-head__lead">از جمله بیمه‌ها و بانک‌های طرف قرارداد کلینیک نگین:</p>
</div>
<ul className={styles.insurances}>
{INSURANCES.map((i) => (
<li key={i}>{i}</li>
))}
</ul>
</div>
</section>
<section className="section section--grey">
<div className={`container ${styles.care}`}>
<div>
<span className="sec-head__en">Dental Care</span>
<h2>مراقبت از دندان</h2>
<p>
یکی از مهم‌ترین اعضای بدن انسان دندان‌ها هستند. بسیاری از افراد با
کوتاهی کردن، سلامت دندان‌هایشان را به خطر می‌اندازند یا پس از خراب
شدن یک دندان خیلی دیر به دندانپزشک مراجعه می‌کنند که باعث مشکلات و
بیماری‌های دیگر دندان و لثه می‌شود.
</p>
<p>
به مرور زمان ترک‌ (شکاف)‌های ریزی روی مینای دندان ایجاد می‌شود؛ این
شکاف‌ها فشار ایجادشده هنگام جویدن را خنثی کرده و از شکسته شدن دندان
جلوگیری می‌کنند.
</p>
<p>
خمیردندان‌های معمولی موجود در بازار معمولاً به حد کافی فلوراید دارند
که استفاده از آن‌ها در وعده‌های مسواک زدن، به سفیدی دندان و محافظت از
مینای دندان کمک قابل‌توجهی می‌کند. با این حال، دندان‌ها همیشه نمی‌توانند
در برابر باکتری‌های موجود در دهان پایداری کنند؛ این باکتری‌ها اسیدهایی
ترشح می‌کنند که کلسیم و فسفات مینای دندان را از بین برده و باعث پوسیدگی
دندان‌ها می‌شوند.
</p>
</div>
<div className={styles.careArt}>
<Image
src="/assets/img/info3-3.jpg"
alt="مراقبت از دندان"
width={720}
height={540}
/>
</div>
</div>
</section>
</>
);
}
@@ -0,0 +1,76 @@
.wrap {
max-width: 820px;
margin: 0 auto;
}
.back {
display: inline-block;
margin-bottom: 24px;
font-size: 14px;
font-weight: 600;
color: var(--brand);
}
.title {
font-size: 32px;
margin: 0 0 10px;
}
.date {
color: var(--muted);
font-size: 14px;
margin: 0 0 28px;
}
.cover {
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 34px;
box-shadow: var(--shadow-sm);
}
.cover img {
width: 100%;
height: auto;
}
.content {
color: var(--text);
font-size: 16px;
line-height: 2.1;
}
.content :global(p) {
text-align: justify;
text-align-last: right;
margin: 0 0 1.4em;
}
.content :global(h2),
.content :global(h3) {
color: var(--heading);
margin: 1.6em 0 0.7em;
}
.content :global(img) {
max-width: 100%;
border-radius: var(--radius);
margin: 1.2em 0;
}
.content :global(a) {
color: var(--brand);
text-decoration: underline;
}
.content :global(ul),
.content :global(ol) {
padding-inline-start: 1.4em;
margin: 0 0 1.4em;
}
@media (max-width: 720px) {
.title {
font-size: 25px;
}
}
+67
View File
@@ -0,0 +1,67 @@
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getBlogById,
blogTitle,
blogExcerpt,
blogCover,
blogDate,
blogContentHtml,
faDate,
} from "@/lib/meshkee";
import styles from "./blog-detail.module.css";
export const revalidate = 120;
export async function generateMetadata({ params }) {
const { id } = await params;
const blog = await getBlogById(id);
if (!blog) return { title: "مقاله" };
const title = blogTitle(blog);
const description = blogExcerpt(blog) || undefined;
return {
title,
description,
openGraph: {
title,
description,
images: blogCover(blog) ? [blogCover(blog)] : undefined,
},
};
}
export default async function BlogDetailPage({ params }) {
const { id } = await params;
const blog = await getBlogById(id);
if (!blog) notFound();
const title = blogTitle(blog);
const cover = blogCover(blog);
const html = blogContentHtml(blog);
return (
<article className="section">
<div className={`container ${styles.wrap}`}>
<Link href="/blog" className={styles.back}>
← بازگشت به مقالات
</Link>
<h1 className={styles.title}>{title}</h1>
{blogDate(blog) ? <p className={styles.date}>{faDate(blogDate(blog))}</p> : null}
{cover ? (
<div className={styles.cover}>
<Image src={cover} alt={title} width={1200} height={640} priority />
</div>
) : null}
{html ? (
<div className={styles.content} dangerouslySetInnerHTML={{ __html: html }} />
) : (
<p className={styles.content}>{blogExcerpt(blog)}</p>
)}
</div>
</article>
);
}
+119
View File
@@ -0,0 +1,119 @@
.empty {
max-width: 620px;
margin: 0 auto;
text-align: center;
}
.empty h2 {
font-size: 28px;
margin-bottom: 14px;
}
.empty p {
color: var(--muted);
margin-bottom: 28px;
text-align: center;
text-align-last: center;
}
.card {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
display: flex;
flex-direction: column;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-md);
}
.cardArt {
display: block;
aspect-ratio: 16 / 10;
background: var(--bg-tint);
}
.cardArt img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cardArtFallback {
display: block;
width: 100%;
height: 100%;
background: linear-gradient(135deg, var(--brand), var(--cyan));
opacity: 0.16;
}
.cardBody {
padding: 22px 24px 26px;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
}
.cardDate {
font-size: 12px;
color: var(--cyan-deep);
font-weight: 700;
}
.cardBody h3 {
font-size: 18px;
margin: 0;
}
.cardBody p {
color: var(--muted);
font-size: 14px;
margin: 0;
flex: 1;
}
.cardMore {
margin-top: 6px;
font-size: 13px;
font-weight: 700;
color: var(--brand);
align-self: flex-start;
}
.cardMore::after {
content: " ←";
}
.pager {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 50px;
}
.pagerLink {
min-width: 38px;
padding: 8px 12px;
text-align: center;
border-radius: 10px;
border: 1px solid var(--border);
color: var(--heading);
font-weight: 600;
font-size: 14px;
}
.pagerLink:hover {
border-color: var(--brand);
color: var(--brand);
}
.pagerLinkActive {
background: var(--brand);
border-color: var(--brand);
color: #fff;
}
+90
View File
@@ -0,0 +1,90 @@
import Image from "next/image";
import Link from "next/link";
import PageHero from "@/components/PageHero";
import { getBlogs, blogTitle, blogExcerpt, blogCover, blogDate, faDate, slugifyFa } from "@/lib/meshkee";
import styles from "./blog.module.css";
export const metadata = {
title: "مقالات",
description:
"مقالات و مطالب آموزشی کلینیک دندانپزشکی نگین درباره سلامت دهان و دندان، مراقبت روزانه از دندان‌ها، ایمپلنت، ارتودنسی و آخرین روش‌های درمانی.",
};
export const revalidate = 120;
export default async function BlogPage({ searchParams }) {
const page = Number((await searchParams)?.page) || 1;
const { items, total, pageSize } = await getBlogs({ page, pageSize: 12 });
const totalPages = Math.max(1, Math.ceil((total || 0) / (pageSize || 12)));
return (
<>
<PageHero en="Blog" fa="مقالات" lead="مطالب آموزشی درباره سلامت دهان و دندان." />
<section className="section">
<div className="container">
{items.length === 0 ? (
<div className={styles.empty}>
<h2>به‌زودی</h2>
<p>
مقالات کلینیک نگین در حال آماده‌سازی است. برای دریافت مشاوره و اطلاعات
بیشتر با ما در تماس باشید.
</p>
<Link href="/contact-us" className="btn">
تماس با ما
</Link>
</div>
) : (
<>
<h2 className="visually-hidden">لیست مقالات</h2>
<div className="grid grid--3">
{items.map((blog) => {
const title = blogTitle(blog);
const cover = blogCover(blog);
const href = `/blog/${blog.id}/${slugifyFa(title)}`;
return (
<article key={blog.id} className={styles.card}>
<Link href={href} className={styles.cardArt}>
{cover ? (
<Image src={cover} alt={title} width={420} height={260} />
) : (
<span className={styles.cardArtFallback} aria-hidden="true" />
)}
</Link>
<div className={styles.cardBody}>
{blogDate(blog) ? (
<span className={styles.cardDate}>{faDate(blogDate(blog))}</span>
) : null}
<h3>
<Link href={href}>{title}</Link>
</h3>
{blogExcerpt(blog) ? <p>{blogExcerpt(blog)}</p> : null}
<Link href={href} className={styles.cardMore}>
ادامه مطلب
</Link>
</div>
</article>
);
})}
</div>
{totalPages > 1 ? (
<nav className={styles.pager} aria-label="صفحه‌بندی مقالات">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (
<Link
key={p}
href={p === 1 ? "/blog" : `/blog?page=${p}`}
className={`${styles.pagerLink} ${p === page ? styles.pagerLinkActive : ""}`}
>
{p}
</Link>
))}
</nav>
) : null}
</>
)}
</div>
</section>
</>
);
}
+131
View File
@@ -0,0 +1,131 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { submitContact } from "@/lib/meshkee";
import styles from "./contact.module.css";
const EMPTY = { name: "", email: "", phone: "", subject: "", message: "" };
export default function ContactForm() {
const [values, setValues] = useState(EMPTY);
const [status, setStatus] = useState("idle"); // idle | sending | error
const [error, setError] = useState("");
const [toast, setToast] = useState(null); // { text, kind } | null
const toastTimer = useRef(null);
useEffect(() => () => clearTimeout(toastTimer.current), []);
const showToast = (text, kind = "success") => {
setToast({ text, kind });
clearTimeout(toastTimer.current);
toastTimer.current = setTimeout(() => setToast(null), 4500);
};
const onChange = (e) =>
setValues((v) => ({ ...v, [e.target.name]: e.target.value }));
const onSubmit = async (e) => {
e.preventDefault();
setStatus("sending");
setError("");
try {
await submitContact({
title: values.subject,
name: values.name,
text: values.message,
email: values.email || undefined,
cellNumber: values.phone || undefined,
});
setValues(EMPTY);
setStatus("idle");
showToast("پیام شما با موفقیت ارسال شد. همکاران ما در اسرع وقت با شما تماس خواهند گرفت.");
} catch (err) {
setStatus("error");
setError(err.message || "ارسال پیام با خطا مواجه شد. لطفاً دوباره تلاش کنید.");
showToast("ارسال پیام ناموفق بود. لطفاً دوباره تلاش کنید.", "error");
}
};
return (
<>
<form className={styles.form} onSubmit={onSubmit} noValidate>
<div className={styles.row}>
<label>
<span>نام و نام خانوادگی</span>
<input
name="name"
value={values.name}
onChange={onChange}
required
autoComplete="name"
/>
</label>
<label>
<span>آدرس الکترونیکی</span>
<input
type="email"
name="email"
value={values.email}
onChange={onChange}
autoComplete="email"
dir="ltr"
/>
</label>
</div>
<div className={styles.row}>
<label>
<span>شماره تماس</span>
<input
name="phone"
value={values.phone}
onChange={onChange}
autoComplete="tel"
inputMode="tel"
dir="ltr"
/>
</label>
<label>
<span>موضوع</span>
<input
name="subject"
value={values.subject}
onChange={onChange}
required
/>
</label>
</div>
<label>
<span>پیام شما</span>
<textarea
name="message"
rows={6}
value={values.message}
onChange={onChange}
required
/>
</label>
<button type="submit" className="btn" disabled={status === "sending"}>
{status === "sending" ? "در حال ارسال…" : "ارسال پیام"}
</button>
{status === "error" && error ? (
<p className={styles.formError} role="alert">
{error}
</p>
) : null}
</form>
{toast ? (
<div
className={`${styles.toast} ${toast.kind === "error" ? styles.toastError : ""}`}
role="status"
>
{toast.text}
</div>
) : null}
</>
);
}
+190
View File
@@ -0,0 +1,190 @@
.infoGrid {
display: grid;
gap: 24px;
margin-bottom: 60px;
}
@media (min-width: 760px) {
.infoGrid {
grid-template-columns: repeat(3, 1fr);
}
}
.infoCard {
text-align: center;
padding: 34px 24px;
border: none;
border-radius: var(--radius);
background: var(--bg-tint);
transition: background-color 0.2s ease;
}
.infoCard:hover {
background-color: #e2eff9;
}
.infoCard h3 {
font-size: 18px;
margin: 4px 0 10px;
}
.infoCard p,
.infoCard a {
margin: 0;
color: var(--muted);
font-size: 15px;
text-align: center;
text-align-last: center;
}
.infoCard a:hover {
color: var(--brand);
}
.infoIcon {
width: 60px;
height: 60px;
margin: 0 auto 16px;
display: grid;
place-items: center;
border-radius: 50%;
background: #fff;
color: var(--brand);
}
/* ---------- form ---------- */
.formWrap {
/* same width as the info-card row above, and the same 60px rhythm
on top (gap from the row) and bottom (gap to what follows) */
margin: 0 0 60px;
background: var(--bg-tint);
border: none;
border-radius: var(--radius-lg);
padding: 44px;
box-shadow: none;
}
.form {
display: flex;
flex-direction: column;
gap: 20px;
}
.row {
display: grid;
gap: 20px;
}
@media (min-width: 620px) {
.row {
grid-template-columns: 1fr 1fr;
}
}
.form label {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: var(--heading);
}
.form input,
.form textarea {
font: inherit;
color: var(--text);
padding: 13px 16px;
border: 1px solid transparent;
border-radius: 10px;
background: #fff;
transition: border-color 0.2s ease;
}
.form input:focus,
.form textarea:focus {
outline: none;
border-color: var(--brand);
}
.form textarea {
resize: vertical;
}
.form :global(.btn) {
align-self: flex-end;
justify-content: center;
padding: 11px 28px;
font-size: 14px;
}
.formError {
margin: 0;
padding: 14px 18px;
border-radius: 10px;
background: #fdecec;
color: #b3261e;
font-size: 14px;
text-align: center;
text-align-last: center;
}
/* ---------- success / error toast ---------- */
.toast {
position: fixed;
inset-inline: 0;
bottom: 26px;
margin-inline: auto;
width: fit-content;
max-width: calc(100% - 40px);
z-index: 200;
display: flex;
align-items: center;
gap: 10px;
padding: 16px 24px;
border-radius: 999px;
background: #1f7a4d;
color: #fff;
font-size: 14px;
font-weight: 600;
box-shadow: var(--shadow-md);
animation: toast-in 0.25s ease;
}
.toastError {
background: #b3261e;
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ---------- map ---------- */
.mapSection {
line-height: 0;
}
.mapSection iframe {
width: 100%;
height: 420px;
border: 0;
filter: grayscale(0.15);
}
@media (max-width: 620px) {
.infoGrid {
margin-bottom: 32px;
}
.formWrap {
margin-bottom: 32px;
padding: 26px;
}
}
+98
View File
@@ -0,0 +1,98 @@
import PageHero from "@/components/PageHero";
import ContactForm from "./ContactForm";
import styles from "./contact.module.css";
export const metadata = {
title: "تماس با ما",
description:
"راه‌های ارتباطی کلینیک دندانپزشکی نگین در قم: آدرس بلوار امین، تلفن ۰۲۵-۳۲۹۴۵۰۷۷ و فرم تماس آنلاین.",
};
const INFO = [
{
en: "Address",
fa: "آدرس",
value: "قم، بلوار امین، کوچه ۲۱، شماره ۵ (روبروی صدا و سیما)",
icon: (
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.8">
<path d="M12 21s7-5.6 7-11a7 7 0 1 0-14 0c0 5.4 7 11 7 11z" />
<circle cx="12" cy="10" r="2.6" />
</svg>
),
},
{
en: "Phone",
fa: "تلفن تماس",
value: "۰۲۵ - ۳۲۹۴۵۰۷۷",
href: "tel:+982532945077",
ltr: true,
icon: (
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.8">
<path d="M4 5c0-.6.4-1 1-1h3l2 5-2.5 1.5a12 12 0 0 0 5.5 5.5L18 18l5 2v3c0 .6-.4 1-1 1A17 17 0 0 1 4 5z" />
</svg>
),
},
{
en: "Email",
fa: "آدرس الکترونیکی",
value: "info@negindentalclinic.ir",
href: "mailto:info@negindentalclinic.ir",
icon: (
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.8">
<rect x="3" y="5" width="18" height="14" rx="2" />
<path d="m4 7 8 6 8-6" />
</svg>
),
},
];
export default function ContactPage() {
return (
<>
<PageHero
en="Contact Us"
fa="ارتباط با ما"
lead="نظرات خود را با ما در میان بگذارید…"
/>
<section className="section">
<div className="container">
<h2 className="visually-hidden">راه‌های ارتباطی</h2>
<div className={styles.infoGrid}>
{INFO.map((i) => (
<article key={i.en} className={styles.infoCard}>
<span className={styles.infoIcon}>{i.icon}</span>
<span className="card__en">{i.en}</span>
<h3>{i.fa}</h3>
{i.href ? (
<a href={i.href} dir={i.ltr ? "ltr" : undefined}>
{i.value}
</a>
) : (
<p dir={i.ltr ? "ltr" : undefined}>{i.value}</p>
)}
</article>
))}
</div>
<div className={styles.formWrap}>
<div className="sec-head" style={{ marginBottom: 32 }}>
<span className="sec-head__en">Share your comments with us</span>
<h2 className="sec-head__fa">ارسال پیام</h2>
</div>
<ContactForm />
</div>
</div>
</section>
<section className={styles.mapSection}>
<iframe
title="نقشه کلینیک دندانپزشکی نگین"
src="https://www.google.com/maps?q=%D8%A8%D9%84%D9%88%D8%A7%D8%B1%20%D8%A7%D9%85%DB%8C%D9%86%20%D9%82%D9%85&output=embed"
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
/>
</section>
</>
);
}
+360
View File
@@ -0,0 +1,360 @@
/* ==========================================================================
Negin Dental Clinic — global styles
Palette extracted from the original site (negindentalclinic.ir)
========================================================================== */
:root {
--brand: #3b559a; /* primary blue (site accent) */
--brand-dark: #2c4380;
--cyan: #29abe2; /* logo cyan (accents/borders) */
--cyan-soft: #6ec9ef;
--cyan-deep: #1c7ba6; /* darker blue — used for the light-blue buttons */
--en-accent: #22356b; /* darker blue for Latin/English captions & titles */
--text: #5c6873; /* body copy */
--muted: #818695; /* secondary copy */
--heading: #333232; /* dark headings */
--bg: #ffffff;
--bg-tint: #eff6fa; /* pale blue section background */
--bg-tint-2: #f7fcff;
--bg-grey: #f7f7f7;
--border: #e6ebf1;
--radius: 14px;
--radius-lg: 28px;
--shadow-sm: 0 6px 18px rgba(43, 67, 128, 0.08);
--shadow-md: 0 18px 45px rgba(43, 67, 128, 0.12);
--container: 1180px;
--header-h: 92px;
--font-fa: var(--font-vazirmatn), "Vazirmatn", Tahoma, sans-serif;
--font-en: var(--font-cinzel), "Cinzel", "Times New Roman", serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-fa);
font-size: 16px;
line-height: 2;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--heading);
line-height: 1.6;
margin: 0 0 0.6em;
font-weight: 700;
}
p {
margin: 0 0 1.2em;
text-align: justify;
text-align-last: right;
}
a {
color: inherit;
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
color: var(--brand);
}
img {
max-width: 100%;
height: auto;
display: block;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
/* form controls don't inherit the page font by default in most browsers */
button,
input,
textarea,
select {
font-family: inherit;
}
/* Latin type — the site brand language for English captions/titles */
.en,
.en-title {
font-family: var(--font-en);
letter-spacing: 0.06em;
text-transform: uppercase;
}
/* ---------- layout helpers ---------- */
.container {
width: 100%;
max-width: var(--container);
margin: 0 auto;
padding: 0 24px;
}
.section {
padding: 90px 0;
}
.section--tint {
background: var(--bg-tint);
}
.section--grey {
background: var(--bg-grey);
}
.grid {
display: grid;
gap: 30px;
}
@media (min-width: 720px) {
.grid--2 {
grid-template-columns: repeat(2, 1fr);
}
.grid--3 {
grid-template-columns: repeat(3, 1fr);
}
}
/* ---------- section heading (bilingual, like the original) ---------- */
.sec-head {
text-align: center;
max-width: 760px;
margin: 0 auto 54px;
}
.sec-head__en {
display: block;
font-family: var(--font-en);
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--en-accent);
font-size: 15px;
margin-bottom: 10px;
}
.sec-head__fa {
font-size: 30px;
color: var(--heading);
margin: 0 0 14px;
}
.sec-head__lead {
color: var(--muted);
margin: 0;
text-align: center;
text-align-last: center;
}
.sec-head::after {
content: "";
display: block;
width: 64px;
height: 3px;
border-radius: 3px;
background: var(--brand);
margin: 20px auto 0;
}
/* ---------- buttons ---------- */
.btn {
--_bg: var(--brand);
--_fg: #fff;
display: inline-flex;
align-items: center;
gap: 10px;
padding: 13px 30px;
border-radius: 999px;
background: var(--_bg);
color: var(--_fg);
font-weight: 700;
font-size: 15px;
border: 2px solid var(--_bg);
cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
.btn:hover {
color: var(--_fg);
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
}
.btn--cyan {
--_bg: var(--cyan-deep);
}
.btn--ghost {
--_bg: transparent;
--_fg: var(--brand);
border-color: var(--brand);
}
.btn--ghost:hover {
--_fg: #fff;
background: var(--brand);
}
/* ---------- cards ---------- */
.card {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 34px 28px;
text-align: center;
height: 100%;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.card:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-md);
border-color: transparent;
}
.card__icon {
width: 96px;
height: 96px;
margin: 0 auto 14px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--bg-tint);
}
.card__icon img {
width: 60px;
height: 60px;
}
.card__en {
display: block;
font-family: var(--font-en);
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--en-accent);
font-size: 13px;
line-height: 1.4;
}
.card__title {
font-size: 20px;
margin: 2px 0 12px;
}
.card__text {
color: var(--muted);
font-size: 15px;
margin: 0;
text-align: justify;
text-align-last: right;
}
/* ---------- utilities ---------- */
.text-center {
text-align: center;
}
/* keep numeric strings (phone numbers, etc.) in left-to-right order inside RTL text */
.ltr,
[dir="ltr"] {
unicode-bidi: isolate;
}
.mt-0 {
margin-top: 0;
}
.lead {
font-size: 18px;
color: var(--muted);
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 720px) {
.section {
padding: 60px 0;
}
.sec-head__fa {
font-size: 24px;
}
}
/* ---------- scroll-in sequence animation (see components/Reveal.js) ---------- */
.reveal {
opacity: 0;
transition: opacity 0.7s cubic-bezier(0.16, 0.8, 0.24, 1) var(--reveal-delay, 0ms),
transform 0.7s cubic-bezier(0.16, 0.8, 0.24, 1) var(--reveal-delay, 0ms);
}
.reveal--up {
transform: translateY(30px);
}
.reveal--down {
transform: translateY(-30px);
}
.reveal--left {
transform: translateX(-36px);
}
.reveal--right {
transform: translateX(36px);
}
.reveal--scale {
transform: scale(0.92);
}
.reveal--in {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.reveal {
transition: none;
opacity: 1;
transform: none;
}
}
+73
View File
@@ -0,0 +1,73 @@
import { Cinzel, Vazirmatn } from "next/font/google";
import "./globals.css";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { getFavicon } from "@/lib/meshkee";
const DEFAULT_ICON = "/assets/img/Header/header.png";
const vazirmatn = Vazirmatn({
subsets: ["arabic", "latin"],
weight: ["300", "400", "500", "700", "800"],
variable: "--font-vazirmatn",
display: "swap",
});
const cinzel = Cinzel({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-cinzel",
display: "swap",
});
export async function generateMetadata() {
// "read favicon from backend, if not exist use default"
const favicon = await getFavicon();
const icon = favicon?.faviconUrl || DEFAULT_ICON;
return {
metadataBase: new URL("https://www.negindentalclinic.ir"),
title: {
default: "کلینیک دندانپزشکی نگین | Negin Dental Clinic",
template: "%s | کلینیک دندانپزشکی نگین",
},
description:
"کلینیک دندانپزشکی نگین در قم، ارائه‌دهنده کلیه خدمات دندانپزشکی از جمله ایمپلنت، ارتودنسی نامرئی (الاینر) و خدمات زیبایی توسط دندانپزشکان مجرب و متخصص.",
keywords: [
"کلینیک دندانپزشکی نگین",
"دندانپزشکی قم",
"ایمپلنت",
"ارتودنسی نامرئی",
"الاینر",
"دندانپزشک متخصص",
],
icons: {
icon,
},
openGraph: {
type: "website",
locale: "fa_IR",
siteName: "کلینیک دندانپزشکی نگین",
title: "کلینیک دندانپزشکی نگین | Negin Dental Clinic",
description:
"ارائه کلیه خدمات دندانپزشکی توسط دندانپزشکان مجرب و متخصصان عضو هیئت علمی دانشکده دندانپزشکی.",
},
};
}
export default function RootLayout({ children }) {
return (
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} ${cinzel.variable}`}>
<body>
{/* If JS never runs (blocked/failed), the scroll-reveal sequence on
Home would otherwise leave content stuck at opacity:0 forever. */}
<noscript>
<style>{`.reveal { opacity: 1 !important; transform: none !important; }`}</style>
</noscript>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
+79
View File
@@ -0,0 +1,79 @@
import fs from "node:fs";
import path from "node:path";
import { NextResponse } from "next/server";
import { WEBSITE_DOMAIN } from "@/lib/meshkee";
// Published for the Meshkee business dashboard's "Refresh" sitemap button —
// see AI_PROMPT.md ("What the website must publish"). Meshkee's own nginx
// proxy serves the actual /sitemap.xml (and the CMS-driven child sitemaps
// like /sitemap-blogs.xml) from the API; this route only lists this app's
// *static* public pages so the dashboard can fold them into the sitemap
// index. Do not add app/sitemap.ts / app/robots.ts — those paths are owned
// by the Meshkee proxy once this site is live on a Meshkee-managed domain.
export const dynamic = "force-static";
// Route segments that never map to a crawlable public page: dynamic detail
// pages (CMS-managed — Meshkee generates their own sitemap entries), and
// this "meshkee/*" integration namespace itself.
const EXCLUDE_SEGMENT = (segment) =>
segment.startsWith("[") || segment === "meshkee" || segment.startsWith("_");
// Per-path SEO weight. Anything not listed falls back to a sane default.
const PAGE_META = {
"/": { changefreq: "daily", priority: 1.0 },
"/services": { changefreq: "weekly", priority: 0.8 },
"/about-us": { changefreq: "monthly", priority: 0.6 },
"/blog": { changefreq: "daily", priority: 0.7 },
"/contact-us": { changefreq: "monthly", priority: 0.5 },
"/virtual-tour": { changefreq: "yearly", priority: 0.4 },
};
/** Scan app/ for `page.js` files and turn each into a public URL path. */
function scanStaticPages(dir, baseSegments = []) {
const pages = [];
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return pages;
}
const hasPage = entries.some(
(e) => e.isFile() && /^page\.(js|jsx|ts|tsx)$/.test(e.name)
);
if (hasPage && !baseSegments.some(EXCLUDE_SEGMENT)) {
const urlPath = "/" + baseSegments.join("/");
pages.push(urlPath === "/" ? "/" : urlPath.replace(/\/$/, ""));
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
// route groups "(name)" don't appear in the URL
const isGroup = entry.name.startsWith("(") && entry.name.endsWith(")");
const nextSegments = isGroup
? baseSegments
: [...baseSegments, entry.name];
if (EXCLUDE_SEGMENT(entry.name)) continue;
pages.push(...scanStaticPages(path.join(dir, entry.name), nextSegments));
}
return pages;
}
export async function GET() {
const appDir = path.join(process.cwd(), "app");
const paths = scanStaticPages(appDir).sort(
(a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b)
);
const staticPages = paths.map((p) => ({
path: p,
...(PAGE_META[p] || { changefreq: "monthly", priority: 0.5 }),
}));
return NextResponse.json({
baseUrl: `https://${WEBSITE_DOMAIN}`,
staticPages,
});
}
+301
View File
@@ -0,0 +1,301 @@
import Image from "next/image";
import Link from "next/link";
import { getBlogs, blogTitle, blogExcerpt, blogCover, blogDate, faDate, slugifyFa } from "@/lib/meshkee";
import Reveal from "@/components/Reveal";
import styles from "./page.module.css";
export const revalidate = 120;
const FEATURES = [
{
en: "Appointment",
fa: "نوبت‌دهی",
text: "برای گرفتن نوبت از متخصصین کلینیک با ما تماس بگیرید.",
cta: { label: "درخواست نوبت", href: "/contact-us" },
icon: "/assets/img/icons/1.png",
},
{
en: "Emergency Case",
fa: "موارد اورژانسی",
text: "در صورت بروز موارد اورژانسی با کلینیک نگین تماس بگیرید.",
cta: { label: "۰۲۵ - ۳۲۹۴۵۰۷۷", href: "tel:+982532945077" },
icon: "/assets/img/icons/2.png",
},
{
en: "Our Doctors",
fa: "پزشکان ما",
text: "ارائه خدمات توسط دندانپزشکان مجرب و متخصصان عضو هیئت علمی دانشکده دندانپزشکی.",
cta: { label: "معرفی پزشکان", href: "/about-us" },
icon: "/assets/img/icons/3.png",
},
];
const SCHEDULE = [
{ day: "شنبه", doctors: ["دکتر شجری", "دکتر صانعی‌پور", "دکتر دیناروند"] },
{ day: "یکشنبه", doctors: ["دکتر صانعی‌پور", "دکتر دیناروند", "دکتر شجری"] },
{ day: "دوشنبه", doctors: ["دکتر کریمی", "دکتر حسینی مطلق", "دکتر صانعی‌پور"] },
{ day: "سه‌شنبه", doctors: ["دکتر شجری", "دکتر دیناروند", "دکتر رسولی"] },
{ day: "چهارشنبه", doctors: ["دکتر صبری", "دکتر صانعی‌پور", "دکتر دیناروند"] },
{ day: "پنجشنبه", doctors: ["دکتر صبری", "دکتر رسولی", "دکتر حسینی مطلق"] },
];
const SERVICES = [
{ en: "Prosthesis", fa: "پروتز", text: "روکش ثابت و متحرک، روکش ایمپلنت.", icon: "/assets/img/icons/4.png" },
{ en: "Specialized", fa: "خدمات تخصصی", text: "خدمات تخصصی کودکان: کشیدن، ترمیم، عصب‌کشی، روکش استیل و فضانگهدارنده.", icon: "/assets/img/icons/5.png" },
{ en: "Public", fa: "خدمات عمومی", text: "کشیدن، ترمیم (پر کردن)، جرم‌گیری، درمان ریشه و ساخت پروتز.", icon: "/assets/img/icons/6.png" },
{ en: "Root Canal", fa: "درمان ریشه", text: "عصب‌کشی تخصصی شامل ریشه‌های عفونی، ریشه‌های سخت و کانال‌های تنگ و باریک.", icon: "/assets/img/icons/7.png" },
{ en: "Orthodontic", fa: "ارتودنسی", text: "ردیف کردن دندان‌ها و ساخت پلاک مخصوص کودکان مبتلا به شکاف کام.", icon: "/assets/img/icons/8.png" },
{ en: "Prio", fa: "پریو", text: "جراحی‌های لثه، جراحی‌های نرم و سخت، جراحی ایمپلنت و درمان بیماری‌های لثه.", icon: "/assets/img/icons/9.png" },
];
export default async function HomePage() {
const { items: latestBlogs } = await getBlogs({ page: 1, pageSize: 3 });
return (
<>
{/* ---------- hero ---------- */}
<section className={styles.hero}>
<div className={`container ${styles.heroInner}`}>
<div className={styles.heroCopy}>
<Reveal as="span" from="up" delay={0} className={`en-title ${styles.heroEn}`}>
Negin Dental Clinic
</Reveal>
<Reveal as="h1" from="up" delay={110} className={styles.heroTitle}>
کلینیک دندانپزشکی نگین
</Reveal>
<Reveal as="p" from="up" delay={220} className={styles.heroLead}>
ارائه‌دهنده کلیات خدمات دندانپزشکی از جمله ایمپلنت، ارتودنسی شفاف
(الاینر) و خدمات زیبایی، در محیطی آرام و مناسب برای بانوان و کودکان.
</Reveal>
<Reveal as="div" from="up" delay={330} className={styles.heroActions}>
<Link href="/contact-us" className="btn">
لیست نوبت‌دهی
</Link>
<Link href="/virtual-tour" className="btn btn--ghost">
سفر به کلینیک
</Link>
</Reveal>
</div>
<Reveal as="div" from="scale" delay={200} className={styles.heroArt}>
<Image
src="/assets/img/slider-1.png"
alt="خدمات دندانپزشکی کلینیک نگین"
width={754}
height={810}
priority
/>
</Reveal>
</div>
</section>
{/* ---------- feature cards ---------- */}
<section className={styles.features}>
<h2 className="visually-hidden">دسترسی سریع</h2>
<div className={`container grid grid--3`}>
{FEATURES.map((f, i) => (
<Reveal as="article" from="up" delay={i * 110} key={f.en} className="card">
<div className="card__icon">
<Image src={f.icon} alt="" width={60} height={60} />
</div>
<span className="card__en">{f.en}</span>
<h3 className="card__title">{f.fa}</h3>
<p className="card__text">{f.text}</p>
<Link
href={f.cta.href}
className={styles.cardLink}
dir={f.cta.href.startsWith("tel:") ? "ltr" : undefined}
>
{f.cta.label}
</Link>
</Reveal>
))}
</div>
</section>
{/* ---------- weekly schedule ---------- */}
<section className="section section--tint">
<div className="container">
<Reveal as="div" from="up" className="sec-head">
<span className="sec-head__en">Weekly Schedule</span>
<h2 className="sec-head__fa">خدمات ما در کلینیک</h2>
<p className="sec-head__lead">
برای درمان‌های خاص، به جدول زمان‌بندی حضور متخصصین در کلینیک توجه کنید.
</p>
</Reveal>
<Reveal as="div" from="up" delay={120} className={styles.tableWrap}>
<table className={styles.schedule}>
<thead>
<tr>
{SCHEDULE.map((s) => (
<th key={s.day}>{s.day}</th>
))}
</tr>
</thead>
<tbody>
<tr>
{SCHEDULE.map((s) => (
<td key={s.day}>
{s.doctors.map((d) => (
<span key={d}>{d}</span>
))}
</td>
))}
</tr>
</tbody>
</table>
</Reveal>
</div>
</section>
{/* ---------- info: Negin Dental Clinic ---------- */}
<section className="section">
<div className={`container ${styles.split}`}>
<Reveal as="div" from="right" className={styles.splitArt}>
<Image src="/assets/img/info3-2.jpg" alt="کلینیک دندانپزشکی نگین" width={720} height={540} />
</Reveal>
<Reveal as="div" from="left" delay={120} className={styles.splitCopy}>
<span className="sec-head__en">Negin Dental Clinic</span>
<h2>دندانپزشکی نگین</h2>
<p>
این مرکز در تمام ایام هفته با همراهی پزشکان متعهد و مجرب، در زمینه
تشخیص و درمان مشکلات و دردهای ناگهانی دندان در خدمت مادران و فرزندان
می‌باشد. لذا برای درمان‌های خاص به جدول زمان‌بندی حضور متخصصین در
کلینیک توجه کنید.
</p>
<Link href="/services" className="btn btn--cyan">
مشاهده خدمات
</Link>
</Reveal>
</div>
</section>
{/* ---------- info: About Negin Dental ---------- */}
<section className="section section--grey">
<div className={`container ${styles.split} ${styles.splitReverse}`}>
<Reveal as="div" from="left" className={styles.splitArt}>
<Image src="/assets/img/info3-3.jpg" alt="درباره کلینیک نگین" width={720} height={540} />
</Reveal>
<Reveal as="div" from="right" delay={120} className={styles.splitCopy}>
<span className="sec-head__en">About Negin Dental</span>
<h2>درباره کلینیک نگین</h2>
<p>
با شماره‌های کلینیک نگین تماس بگیرید و بدون حضور در کلینیک، نوبت خود
را با دکتر مورد نظر هماهنگ کنید. کادر مجرب دندانپزشکی و استفاده از
مواد با کیفیت، از مزیت‌های بارز کلینیک نگین است.
</p>
<Link href="/about-us" className="btn btn--ghost">
درباره ما
</Link>
</Reveal>
</div>
</section>
{/* ---------- services ---------- */}
<section className="section">
<div className="container">
<Reveal as="div" from="up" className="sec-head">
<span className="sec-head__en">Services</span>
<h2 className="sec-head__fa">خدمات ما در کلینیک</h2>
<p className="sec-head__lead">
ارائه کلیه خدمات دندانپزشکی توسط دندانپزشکان مجرب و متخصصان عضو هیئت
علمی دانشکده دندانپزشکی، با بهره‌گیری از تجهیزات پیشرفته و به‌روز دنیا.
</p>
</Reveal>
<div className="grid grid--3">
{SERVICES.map((s, i) => (
<Reveal as="article" from="up" delay={(i % 3) * 110} key={s.en} className="card">
<div className="card__icon">
<Image src={s.icon} alt="" width={60} height={60} />
</div>
<span className="card__en">{s.en}</span>
<h3 className="card__title">{s.fa}</h3>
<p className="card__text">{s.text}</p>
</Reveal>
))}
</div>
<div className="text-center" style={{ marginTop: 44 }}>
<Link href="/services" className="btn">
لیست کامل خدمات
</Link>
</div>
</div>
</section>
{/* ---------- latest blog posts ---------- */}
{latestBlogs.length > 0 ? (
<section className="section section--tint">
<div className="container">
<Reveal as="div" from="up" className="sec-head">
<span className="sec-head__en">Blog</span>
<h2 className="sec-head__fa">آخرین مقالات</h2>
</Reveal>
<div className="grid grid--3">
{latestBlogs.map((blog, i) => {
const title = blogTitle(blog);
const cover = blogCover(blog);
const href = `/blog/${blog.id}/${slugifyFa(title)}`;
return (
<Reveal as="article" from="up" delay={i * 110} key={blog.id} className={styles.blogCard}>
<Link href={href} className={styles.blogCardArt}>
{cover ? (
<Image src={cover} alt={title} width={420} height={260} />
) : (
<span className={styles.blogCardArtFallback} aria-hidden="true" />
)}
</Link>
<div className={styles.blogCardBody}>
{blogDate(blog) ? (
<span className={styles.blogCardDate}>{faDate(blogDate(blog))}</span>
) : null}
<h3>
<Link href={href}>{title}</Link>
</h3>
{blogExcerpt(blog) ? <p>{blogExcerpt(blog)}</p> : null}
</div>
</Reveal>
);
})}
</div>
<div className="text-center" style={{ marginTop: 44 }}>
<Link href="/blog" className="btn btn--ghost">
همه مقالات
</Link>
</div>
</div>
</section>
) : null}
{/* ---------- call CTA ---------- */}
<section className={styles.cta}>
<div className={`container ${styles.ctaInner}`}>
<Reveal as="div" from="right">
<span className="en-title" style={{ color: "var(--cyan-soft)" }}>
Call Us
</span>
<h2 className={styles.ctaTitle}>با ما در ارتباط باشید</h2>
<p className={styles.ctaText}>
در صورت موارد اورژانسی و نوبت‌دهی با ما تماس بگیرید. همکاران مستقر در
کلینیک، پرونده‌ای شامل اطلاعات شناسایی اولیه برای شما تشکیل می‌دهند و
سپس شما را به بخش تشخیص بیماری‌های دهان و دندان هدایت می‌کنند.
</p>
</Reveal>
<Reveal
as="a"
from="left"
delay={150}
dir="ltr"
href="tel:+982532945077"
className={styles.ctaPhone}
>
۰۲۵ - ۳۲۹۴۵۰۷۷
</Reveal>
</div>
</section>
</>
);
}
+281
View File
@@ -0,0 +1,281 @@
/* ---------- hero ---------- */
.hero {
position: relative;
overflow: hidden;
background:
radial-gradient(60% 80% at 85% 20%, rgba(41, 171, 226, 0.12), transparent 70%),
linear-gradient(180deg, #f7fcff 0%, #eef6fb 100%);
}
.heroInner {
display: grid;
gap: 40px;
align-items: center;
padding: 70px 24px 80px;
}
@media (min-width: 900px) {
.heroInner {
grid-template-columns: 1.05fr 0.95fr;
padding: 90px 24px 100px;
}
}
.heroEn {
color: var(--en-accent);
font-size: 18px;
letter-spacing: 0.16em;
}
.heroTitle {
font-size: 44px;
line-height: 1.35;
margin: 12px 0 18px;
color: var(--heading);
}
.heroLead {
font-size: 18px;
color: var(--muted);
max-width: 480px;
margin-bottom: 30px;
}
.heroActions {
display: flex;
flex-wrap: wrap;
gap: 14px;
}
.heroArt {
position: relative;
justify-self: center;
}
.heroArt img {
width: 100%;
max-width: 460px;
height: auto;
filter: drop-shadow(0 30px 50px rgba(43, 67, 128, 0.18));
}
@media (max-width: 900px) {
.heroTitle {
font-size: 32px;
}
}
/* ---------- feature cards ---------- */
.features {
margin-top: -50px;
padding-bottom: 20px;
position: relative;
z-index: 2;
}
.cardLink {
display: inline-block;
margin-top: 16px;
font-weight: 700;
font-size: 14px;
color: var(--brand);
}
.cardLink::after {
content: " ←";
}
/* ---------- schedule table ---------- */
.tableWrap {
overflow-x: auto;
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
background: #fff;
}
.schedule {
width: 100%;
min-width: 720px;
border-collapse: collapse;
text-align: center;
}
.schedule th {
background: var(--brand);
color: #fff;
font-size: 15px;
padding: 16px 10px;
font-weight: 700;
}
.schedule td {
padding: 22px 10px;
border: 1px solid var(--border);
vertical-align: top;
}
.schedule td span {
display: block;
font-size: 14px;
color: var(--text);
padding: 6px 0;
}
.schedule td span + span {
border-top: 1px dashed var(--border);
}
/* ---------- split info sections ---------- */
.split {
display: grid;
gap: 40px;
align-items: center;
}
@media (min-width: 860px) {
.split {
grid-template-columns: 1fr 1fr;
}
.splitReverse .splitArt {
order: 2;
}
}
.splitArt img {
width: 100%;
height: auto;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
}
.splitCopy {
display: flex;
flex-direction: column;
}
.splitCopy h2 {
font-size: 28px;
margin: 6px 0 16px;
}
/* push the section CTA to the left edge of the text column */
.splitCopy :global(.btn) {
align-self: flex-end;
}
/* ---------- call CTA ---------- */
.cta {
background:
linear-gradient(180deg, rgba(44, 67, 128, 0.9), rgba(44, 67, 128, 0.9)),
url("/assets/img/info_items_2/bg_img.jpg") center / cover no-repeat fixed;
color: #fff;
padding: 80px 0;
}
.ctaInner {
display: grid;
gap: 30px;
align-items: center;
}
@media (min-width: 860px) {
.ctaInner {
grid-template-columns: 1.6fr 1fr;
}
}
.ctaTitle {
color: #fff;
font-size: 30px;
margin: 8px 0 14px;
}
.ctaText {
color: rgba(255, 255, 255, 0.86);
margin: 0;
max-width: 560px;
}
.ctaPhone {
justify-self: start;
font-family: var(--font-en);
font-size: 30px;
font-weight: 700;
letter-spacing: 0.04em;
color: #fff;
padding: 18px 34px;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 999px;
transition: background 0.2s ease, color 0.2s ease;
}
@media (min-width: 860px) {
.ctaPhone {
justify-self: end;
}
}
.ctaPhone:hover {
background: #fff;
color: var(--brand);
}
/* ---------- latest blog cards ---------- */
.blogCard {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
display: flex;
flex-direction: column;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.blogCard:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-md);
}
.blogCardArt {
display: block;
aspect-ratio: 16 / 10;
background: var(--bg-tint);
}
.blogCardArt img {
width: 100%;
height: 100%;
object-fit: cover;
}
.blogCardArtFallback {
display: block;
width: 100%;
height: 100%;
background: linear-gradient(135deg, var(--brand), var(--cyan));
opacity: 0.16;
}
.blogCardBody {
padding: 22px 24px 26px;
display: flex;
flex-direction: column;
gap: 8px;
}
.blogCardDate {
font-size: 12px;
color: var(--cyan-deep);
font-weight: 700;
}
.blogCardBody h3 {
font-size: 18px;
margin: 0;
}
.blogCardBody p {
color: var(--muted);
font-size: 14px;
margin: 0;
}
+93
View File
@@ -0,0 +1,93 @@
import Image from "next/image";
import Link from "next/link";
import PageHero from "@/components/PageHero";
import styles from "./services.module.css";
export const metadata = {
title: "خدمات کلینیک",
description:
"کلیه خدمات دندانپزشکی کلینیک نگین: پروتز، ارتودنسی، درمان ریشه، جراحی ایمپلنت، ترمیم و زیبایی، خدمات کودکان و مشاوره آنلاین.",
};
const SERVICES = [
{ en: "Prosthesis", fa: "پروتز", text: "روکش ثابت و متحرک، روکش ایمپلنت.", icon: "/assets/img/icons/1.png" },
{ en: "Specialized", fa: "خدمات تخصصی", text: "خدمات تخصصی کودکان شامل کشیدن، ترمیم، عصب‌کشی، روکش استیل و فضانگهدارنده.", icon: "/assets/img/icons/2.png" },
{ en: "Public", fa: "خدمات عمومی", text: "کشیدن، ترمیم (پر کردن)، جرم‌گیری، درمان ریشه (عصب‌کشی) و پروتز (ساخت روکش و دندان مصنوعی).", icon: "/assets/img/icons/3.png" },
{ en: "Root Canal", fa: "درمان ریشه", text: "عصب‌کشی تخصصی شامل ریشه‌های عفونی، ریشه‌های سخت و کانال‌های تنگ و باریک.", icon: "/assets/img/icons/4.png" },
{ en: "Orthodontic", fa: "ارتودنسی", text: "ردیف کردن دندان‌ها و ساخت پلاک مخصوص کودکان مبتلا به شکاف کام.", icon: "/assets/img/icons/5.png" },
{ en: "Prio", fa: "پریو", text: "جراحی‌های لثه، جراحی‌های نرم و سخت، جراحی ایمپلنت و درمان بیماری‌های لثه.", icon: "/assets/img/icons/6.png" },
{ en: "Composite Veneer", fa: "ترمیم تخصصی", text: "ترمیمی و زیبایی شامل ونیر، لمینیت، ترمیم دندان‌های آسیب‌دیده و سفید کردن دندان‌ها (بلیچینگ).", icon: "/assets/img/icons/7.png" },
{ en: "Kids Teeth", fa: "کودکان", text: "ارائه کلیه خدمات دندانپزشکی توسط متخصصین باتجربه کودکان.", icon: "/assets/img/icons/8.png" },
{ en: "Implant Surgery", fa: "جراحی ایمپلنت", text: "جراحی ایمپلنت دندان دیجیتال، جراحی با حداقل برش.", icon: "/assets/img/icons/9.png" },
];
export default function ServicesPage() {
return (
<>
<PageHero
en="Services"
fa="خدمات"
lead="با خدمات ارائه‌شده توسط کلینیک بیشتر آشنا شوید."
/>
<section className="section">
<div className="container">
<div className="sec-head">
<span className="sec-head__en">Our Services</span>
<h2 className="sec-head__fa">خدمات ما در کلینیک</h2>
<p className="sec-head__lead">
ارائه کلیه خدمات دندانپزشکی توسط دندانپزشکان مجرب و متخصصان عضو هیئت
علمی دانشکده دندانپزشکی، با بهره‌گیری از تجهیزات پیشرفته و به‌روز دنیا.
پس با خیالی آسوده به ما مراجعه کنید…
</p>
</div>
<div className="grid grid--3">
{SERVICES.map((s) => (
<article key={s.en} className="card">
<div className="card__icon">
<Image src={s.icon} alt="" width={60} height={60} />
</div>
<span className="card__en">{s.en}</span>
<h3 className="card__title">{s.fa}</h3>
<p className="card__text">{s.text}</p>
</article>
))}
</div>
</div>
</section>
<section className="section section--tint">
<div className={`container ${styles.split}`}>
<div className={styles.art}>
<Image
src="/assets/img/info3-2.jpg"
alt="مشاوره دندانپزشکی از راه دور"
width={720}
height={540}
/>
</div>
<div>
<span className="sec-head__en">Consultation</span>
<h2>مشاوره</h2>
<p>
خدمت مشاوره از راه دور یا مشاوره آنلاین، از خدماتی است که سال‌های
متمادی است در کشورهای مختلف در حال تجربه است. بسیاری از کشورها
توانسته‌اند با این امکان، بخش زیادی از مراجعات غیرضروری به پزشک‌ها و
کلینیک‌ها را حذف کنند.
</p>
<h3 style={{ marginTop: 28 }}>خدمات حضوری</h3>
<p>
کلینیک نگین با بهره‌گیری از متخصصین مجرب و محیطی آرام و مناسب برای
بانوان و کودکان، آماده پاسخگویی و ارائه خدمت به مراجعین محترم می‌باشد.
خدمات این کلینیک اعم از خدمات تخصصی و عمومی، ویژه بانوان و کودکان است.
</p>
<Link href="/contact-us" className="btn btn--cyan">
درخواست نوبت
</Link>
</div>
</div>
</section>
</>
);
}
+40
View File
@@ -0,0 +1,40 @@
.split {
display: grid;
gap: 40px;
align-items: center;
}
@media (min-width: 860px) {
.split {
grid-template-columns: 1fr 1fr;
}
.art {
order: 2;
}
}
.split > div:not(.art) {
display: flex;
flex-direction: column;
}
.split h2 {
font-size: 28px;
margin: 6px 0 16px;
}
.split h3 {
font-size: 20px;
}
/* push the section CTA to the left edge of the text column */
.split :global(.btn) {
align-self: flex-end;
}
.art img {
width: 100%;
height: auto;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
}
+44
View File
@@ -0,0 +1,44 @@
import PageHero from "@/components/PageHero";
import styles from "./virtual-tour.module.css";
export const metadata = {
title: "تور مجازی",
description:
"پیش از مراجعه حضوری، فضای داخلی کلینیک دندانپزشکی نگین در قم را با تور مجازی ۳۶۰ درجه مشاهده کنید و از امکانات و محیط درمانی کلینیک آگاه شوید.",
};
export default function VirtualTourPage() {
return (
<>
<PageHero
en="Virtual Tour"
fa="تور مجازی"
lead="سفر مجازی به کلینیک دندانپزشکی نگین."
/>
<section className="section">
<div className={`container ${styles.wrap}`}>
<div className={styles.frame}>
<iframe
title="تور مجازی کلینیک نگین"
src="https://www.negindentalclinic.ir/assets/virtualtour.html"
loading="lazy"
allowFullScreen
/>
</div>
<p className={styles.note}>
در صورتی که تور مجازی بارگذاری نشد، می‌توانید آن را{" "}
<a
href="https://www.negindentalclinic.ir/assets/virtualtour.html"
target="_blank"
rel="noopener noreferrer"
>
در یک صفحه جدید
</a>{" "}
مشاهده کنید.
</p>
</div>
</section>
</>
);
}
+35
View File
@@ -0,0 +1,35 @@
.wrap {
max-width: 960px;
margin: 0 auto;
}
.frame {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-md);
background: var(--bg-tint);
}
.frame iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
.note {
margin-top: 20px;
text-align: center;
text-align-last: center;
color: var(--muted);
font-size: 14px;
}
.note a {
color: var(--brand);
font-weight: 700;
}