Files
meshkee/src/components/ServicePageLayout.tsx
T
Alireza HassaniandCursor 47bee1ce66 Build Meshkee marketing site with service pages and shared layout.
Add home and service routes (website, app, CMS, SEO, graphic), brand assets, and components aligned with the live meshkee.com design.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 10:49:55 +03:30

321 lines
10 KiB
TypeScript

import Link from "next/link";
import { FramedImage } from "@/components/FramedImage";
import { Header } from "@/components/Header";
import { Footer } from "@/components/Footer";
import { RevealObserver } from "@/components/RevealObserver";
import { site } from "@/data/home";
interface Feature {
title: string;
titleEn?: string;
description: string;
icon?: string;
}
interface Section {
tag?: string;
title: string;
titleEn?: string;
body: string;
image?: string;
imageFrame?: boolean;
eyebrow?: string;
headline?: string;
features?: Feature[];
featureLayout?: "horizontal" | "team";
featureColumns?: 2 | 3;
}
interface ServicePageProps {
metaTitle: string;
tag: string;
title: string;
titleEn: string;
lead: string;
heroImage?: string;
sections: Section[];
injectAfter?: number;
inject?: React.ReactNode;
injections?: { after: number; node: React.ReactNode }[];
children?: React.ReactNode;
}
function SolutionFeatureCard({ feature }: { feature: Feature }) {
return (
<div className="group group-border-hover flex h-full gap-3 rounded-2xl border border-line/70 bg-white/90 p-6 transition hover:shadow-[0_16px_40px_rgba(14,17,22,0.06)] md:gap-4 md:p-7">
{feature.icon ? (
<img
src={feature.icon}
alt=""
width={48}
height={48}
className="h-12 w-12 shrink-0 object-contain"
/>
) : null}
<div className="min-w-0 flex-1">
<h3 className="whitespace-nowrap text-[0.95rem] font-normal text-ink md:text-base">
{feature.title}
</h3>
{feature.titleEn && (
<p className="latin mt-0.5 text-[10px] uppercase tracking-wider text-muted">
{feature.titleEn}
</p>
)}
<p className="text-body mt-3 w-full text-sm leading-8 text-ink-soft">
{feature.description}
</p>
</div>
</div>
);
}
function TeamFeatureCard({ feature }: { feature: Feature }) {
return (
<div className="group group-border-hover flex h-full flex-col overflow-hidden rounded-2xl bg-white transition hover:shadow-[0_16px_40px_rgba(14,17,22,0.06)]">
<div className="flex items-center gap-4 bg-[#F8F9FA] p-3">
{feature.icon ? (
<img
src={feature.icon}
alt=""
width={43}
height={43}
className="h-[43px] w-[43px] shrink-0 object-contain"
/>
) : null}
<div className="min-w-0">
<h3 className="mb-1 text-base font-normal text-ink">
{feature.title}
</h3>
{feature.titleEn && (
<p className="latin text-[10px] uppercase tracking-[0.35em] text-muted">
{feature.titleEn}
</p>
)}
</div>
</div>
<p className="text-body p-3 text-justify text-[17px] leading-8 text-ink-soft">
{feature.description}
</p>
</div>
);
}
function FeatureCard({
feature,
layout = "horizontal",
}: {
feature: Feature;
layout?: "horizontal" | "team";
}) {
if (layout === "team") {
return <TeamFeatureCard feature={feature} />;
}
return <SolutionFeatureCard feature={feature} />;
}
export function ServicePageLayout({
tag,
title,
titleEn,
lead,
heroImage,
sections,
injectAfter,
inject,
injections,
children,
}: ServicePageProps) {
const sectionInjections =
injections ??
(injectAfter !== undefined && inject
? [{ after: injectAfter, node: inject }]
: []);
return (
<>
<RevealObserver />
<Header />
<section className="relative overflow-hidden bg-white pt-40 pb-16 lg:pt-48 lg:pb-20">
<div className="mx-auto max-w-7xl px-5 lg:px-8">
<div className="grid gap-10 lg:grid-cols-2 lg:items-center lg:gap-16">
<div className="min-w-0 max-w-xl">
<h1
className="mb-3 text-[clamp(1.15rem,3.6vw,2.75rem)] font-normal tracking-tight text-ink"
style={{ animation: "rise-in 0.9s ease both 0.06s" }}
>
{title}
</h1>
<p
className="latin mb-8 text-xs uppercase tracking-widest text-muted"
style={{ animation: "rise-in 0.9s ease both 0.12s" }}
>
{titleEn}
</p>
<p
className="text-body mb-8 text-sm leading-8 text-ink-soft md:text-base"
style={{ animation: "rise-in 1s ease both 0.18s" }}
>
{lead}
</p>
<a
href={site.phoneHref}
className="latin link-hover inline-flex text-sm text-ink-soft"
dir="ltr"
style={{ animation: "rise-in 1s ease both 0.28s" }}
>
{site.phone}
</a>
</div>
{heroImage && (
<div
className="relative hidden lg:block"
style={{ animation: "fade-in 1.2s ease both 0.2s" }}
>
<div className="pointer-events-none absolute inset-8 rounded-full bg-[radial-gradient(circle,var(--glow),transparent_70%)] blur-2xl" />
<div className="animate-float relative mx-auto aspect-square w-full max-w-[480px]">
<img
src={heroImage}
alt={title}
className="h-full w-full object-contain drop-shadow-[0_24px_48px_rgba(14,17,22,0.12)]"
/>
</div>
</div>
)}
</div>
</div>
</section>
{sections.map((sec, i) => (
<div key={i}>
<section
className={`relative border-t border-line/30 py-16 lg:py-24 ${
i % 2 === 0 ? "bg-[#FAFAFA]" : "bg-white"
}`}
>
<div className="mx-auto max-w-7xl px-5 lg:px-8">
{sec.image ? (
<div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
<div className="reveal order-2 min-w-0 lg:order-1">
{sec.eyebrow ? (
<p className="mb-4 text-sm text-muted">{sec.eyebrow}</p>
) : null}
<h2 className="text-[clamp(1.05rem,3vw,2rem)] font-normal tracking-tight text-ink">
{sec.title}
</h2>
{sec.titleEn && (
<p className="latin mt-3 text-[11px] uppercase tracking-widest text-muted">
{sec.titleEn}
</p>
)}
{sec.headline ? (
<h2 className="mt-8 text-[clamp(1rem,2.5vw,1.65rem)] font-normal tracking-tight text-ink">
{sec.headline}
</h2>
) : null}
<p className="text-body mt-5 text-sm leading-8 text-ink-soft md:text-base">
{sec.body}
</p>
</div>
<div className="reveal reveal-delay-1 order-1 relative mx-auto w-full lg:order-2">
{sec.imageFrame ? (
<FramedImage src={sec.image} alt={sec.title} aspect="natural" />
) : (
<img
src={sec.image}
alt={sec.title}
className="mx-auto w-full max-w-xl object-contain lg:max-w-none"
/>
)}
</div>
</div>
) : (
<div className="reveal mb-10 min-w-0">
<h2 className="text-[clamp(1.05rem,3vw,2rem)] font-normal tracking-tight text-ink">
{sec.title}
</h2>
{sec.titleEn && (
<p className="latin mt-3 text-[11px] uppercase tracking-widest text-muted">
{sec.titleEn}
</p>
)}
<p className="text-body mt-5 w-full text-sm leading-8 text-ink-soft md:text-base">
{sec.body}
</p>
</div>
)}
{sec.features && sec.features.length > 0 && (
<ul
className={`grid gap-4 lg:gap-5 ${
sec.featureColumns === 2
? "sm:grid-cols-2"
: sec.featureLayout === "team"
? "sm:grid-cols-2"
: "sm:grid-cols-2 lg:grid-cols-3"
} ${sec.image ? "mt-12" : ""}`}
>
{sec.features.map((f, fi) => (
<li
key={fi}
className={`reveal ${
fi % 3 === 1
? "reveal-delay-1"
: fi % 3 === 2
? "reveal-delay-2"
: ""
}`}
>
<FeatureCard
feature={f}
layout={sec.featureLayout ?? "horizontal"}
/>
</li>
))}
</ul>
)}
</div>
</section>
{sectionInjections
.filter((item) => item.after === i)
.map((item, injectionIndex) => (
<div key={`inject-${i}-${injectionIndex}`}>{item.node}</div>
))}
</div>
))}
{children}
<section className="border-t border-line/30 bg-ink py-14 text-center">
<div className="mx-auto max-w-2xl px-5">
<h2 className="mb-3 text-[clamp(1.1rem,2.8vw,1.75rem)] font-normal text-surface">
آماده‌اید شروع کنیم؟
</h2>
<p className="mb-8 text-sm leading-8 text-surface/60">
با تیم مشکی تماس بگیرید تا درباره پروژه‌تان صحبت کنیم.
</p>
<div className="flex flex-wrap justify-center gap-4">
<a
href={site.phoneHref}
className="latin inline-flex items-center gap-2 rounded-full bg-white px-6 py-3.5 text-sm text-ink transition hover:[background:var(--brand-gradient)] hover:text-white"
dir="ltr"
>
{site.phone}
</a>
<Link
href="/"
className="inline-flex items-center gap-2 rounded-full border border-surface/20 px-6 py-3.5 text-sm text-surface/80 transition hover:border-surface/60 hover:text-surface"
>
بازگشت به خانه
</Link>
</div>
</div>
</section>
<Footer />
</>
);
}