180 lines
6.2 KiB
TypeScript
180 lines
6.2 KiB
TypeScript
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { Check } from "lucide-react";
|
|
import type { CmsServiceContent } from "@/types/cms-landing";
|
|
|
|
const DEFAULT_HEADING = "Innovative solutions for your business growth.";
|
|
const DEFAULT_BODY =
|
|
"Professional services tailored to your organization. Update this text from the CMS admin under Content Website → Our Services.";
|
|
|
|
function cardImageUrl(s: CmsServiceContent) {
|
|
return s.images?.[0]?.image_url?.trim() || "/image/s1.png";
|
|
}
|
|
|
|
function resolveServiceLink(raw: string | undefined | null): string | null {
|
|
const t = raw?.trim();
|
|
if (!t) return null;
|
|
if (/^(https?:|\/|#|mailto:)/i.test(t)) return t;
|
|
return `https://${t}`;
|
|
}
|
|
|
|
function LearnMoreCta({ href }: { href?: string | null }) {
|
|
const resolved = resolveServiceLink(href);
|
|
const className =
|
|
"inline-flex text-sm font-semibold text-[#966314] hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#966314]";
|
|
if (!resolved) return null;
|
|
if (resolved.startsWith("/")) {
|
|
return (
|
|
<Link href={resolved} className={className}>
|
|
Learn More →
|
|
</Link>
|
|
);
|
|
}
|
|
return (
|
|
<a href={resolved} target="_blank" rel="noopener noreferrer" className={className}>
|
|
Learn More →
|
|
</a>
|
|
);
|
|
}
|
|
|
|
function BulletLine({ text }: { text: string }) {
|
|
const idx = text.indexOf(":");
|
|
if (idx > 0 && idx < text.length - 1) {
|
|
const label = text.slice(0, idx).trim();
|
|
const rest = text.slice(idx + 1).trim();
|
|
return (
|
|
<>
|
|
<span className="font-semibold text-gray-900">{label}:</span> {rest}
|
|
</>
|
|
);
|
|
}
|
|
return <>{text}</>;
|
|
}
|
|
|
|
function ServiceImageBlock({ imgSrc, alt }: { imgSrc: string; alt: string }) {
|
|
const external = /^https?:\/\//i.test(imgSrc);
|
|
return (
|
|
<div className="relative flex w-full max-w-xl justify-center md:max-w-none md:flex-1">
|
|
<div className="relative flex aspect-[4/3] w-full max-w-[520px] items-center justify-center rounded-2xl bg-gray-50 md:aspect-[5/4]">
|
|
{external ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img src={imgSrc} alt={alt} className="h-full w-full rounded-2xl object-contain" />
|
|
) : (
|
|
<Image src={imgSrc} alt={alt} width={520} height={420} className="object-contain" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ServiceTextBlock({ s }: { s: CmsServiceContent }) {
|
|
const rawDesc = s.description?.trim();
|
|
const lines = rawDesc
|
|
? rawDesc.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)
|
|
: [];
|
|
const useBullets = lines.length > 1;
|
|
const bodyText = lines.length === 0 ? DEFAULT_BODY : rawDesc ?? "";
|
|
|
|
return (
|
|
<div className="w-full max-w-xl md:flex-1">
|
|
<h3 className="mb-4 text-2xl font-bold text-gray-900 md:text-3xl whitespace-pre-line">
|
|
{s.primary_title?.trim() || "Service"}
|
|
</h3>
|
|
{s.secondary_title?.trim() ? (
|
|
<p className="mb-6 text-base leading-relaxed text-gray-600">{s.secondary_title.trim()}</p>
|
|
) : null}
|
|
{useBullets ? (
|
|
<ul className="mb-8 space-y-4">
|
|
{lines.map((item, i) => (
|
|
<li
|
|
key={`${i}-${item.slice(0, 48)}`}
|
|
className="flex gap-4 text-sm leading-relaxed text-gray-600"
|
|
>
|
|
<span className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#fdecc8]">
|
|
<Check size={14} className="text-[#966314]" aria-hidden />
|
|
</span>
|
|
<span>
|
|
<BulletLine text={item} />
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="mb-8 text-sm leading-relaxed text-gray-600 whitespace-pre-line md:text-base">
|
|
{bodyText}
|
|
</p>
|
|
)}
|
|
<LearnMoreCta href={s.link_url} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ServiceSection({
|
|
services,
|
|
}: {
|
|
services?: CmsServiceContent[] | null;
|
|
}) {
|
|
const list = services?.filter((s) => s.id) ?? [];
|
|
|
|
if (list.length === 0) {
|
|
return (
|
|
<section className="bg-white py-24 md:py-32">
|
|
<div className="container mx-auto px-6">
|
|
<div className="mb-16 text-center md:mb-20">
|
|
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-gray-400">
|
|
Our Services
|
|
</p>
|
|
<h2 className="mx-auto max-w-3xl text-4xl font-extrabold leading-tight md:text-5xl whitespace-pre-line">
|
|
{DEFAULT_HEADING}
|
|
</h2>
|
|
</div>
|
|
<div className="flex flex-col items-center gap-12 md:flex-row md:items-center">
|
|
<ServiceImageBlock imgSrc="/image/s1.png" alt="Service illustration" />
|
|
<div className="w-full max-w-xl md:flex-1">
|
|
<h3 className="mb-4 text-2xl font-bold text-gray-900 md:text-3xl">Our services</h3>
|
|
<p className="mb-8 text-sm leading-relaxed text-gray-600 md:text-base whitespace-pre-line">
|
|
{DEFAULT_BODY}
|
|
</p>
|
|
<LearnMoreCta href={null} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section className="bg-white py-24 md:py-32">
|
|
<div className="container mx-auto px-6">
|
|
<div className="mb-16 text-center md:mb-20">
|
|
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-gray-400">
|
|
Our Services
|
|
</p>
|
|
<h2 className="mx-auto max-w-3xl text-4xl font-extrabold leading-tight md:text-5xl whitespace-pre-line">
|
|
{DEFAULT_HEADING}
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-20 md:gap-28">
|
|
{list.map((s, index) => {
|
|
const imgSrc = cardImageUrl(s);
|
|
const alt = s.primary_title?.trim() || "Service";
|
|
const reverse = index % 2 === 1;
|
|
return (
|
|
<div
|
|
key={s.id}
|
|
className={`flex flex-col items-stretch gap-12 md:flex-row md:items-center md:gap-16 lg:gap-24 ${
|
|
reverse ? "md:flex-row-reverse" : ""
|
|
}`}
|
|
>
|
|
<ServiceImageBlock imgSrc={imgSrc} alt={alt} />
|
|
<ServiceTextBlock s={s} />
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|