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>
This commit is contained in:
co-authored by
Cursor
parent
045d507cbe
commit
47bee1ce66
@@ -0,0 +1,320 @@
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user