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>
321 lines
10 KiB
TypeScript
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 />
|
|
</>
|
|
);
|
|
}
|