214 lines
7.3 KiB
TypeScript
214 lines
7.3 KiB
TypeScript
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { Check } from "lucide-react";
|
|
import type { CmsProductContent } from "@/types/cms-landing";
|
|
|
|
const DEFAULT_TITLE =
|
|
"The product we offer is designed to meet your business needs.";
|
|
const DEFAULT_BODY =
|
|
"Social media marketing services are provided by companies or individuals who specialize in marketing strategies through social media platforms.";
|
|
|
|
function cardImageUrl(p: CmsProductContent) {
|
|
return p.images?.[0]?.image_url?.trim() || "/image/p1.png";
|
|
}
|
|
|
|
/** Normalize CMS link: relative paths and full URLs; bare domains get https:// */
|
|
function resolveProductLink(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 = resolveProductLink(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>
|
|
);
|
|
}
|
|
|
|
/** Renders "Label: rest" with a bold label when a colon is present (one colon split). */
|
|
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 ProductImageBlock({
|
|
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 ProductOrderBadge({ order }: { order: number }) {
|
|
return (
|
|
<div
|
|
className="mb-6 flex h-12 w-12 items-center justify-center rounded-xl bg-[#fdecc8]"
|
|
aria-hidden
|
|
>
|
|
<span className="text-lg font-bold tabular-nums text-[#966314]">{order}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ProductTextBlock({ p, order }: { p: CmsProductContent; order: number }) {
|
|
const rawDesc = p.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">
|
|
<ProductOrderBadge order={order} />
|
|
<h3 className="mb-4 text-2xl font-bold text-gray-900 md:text-3xl whitespace-pre-line">
|
|
{p.primary_title?.trim() || "Product"}
|
|
</h3>
|
|
{p.secondary_title?.trim() ? (
|
|
<p className="mb-6 text-base leading-relaxed text-gray-600">
|
|
{p.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={p.link_url} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ProductSection({
|
|
products,
|
|
}: {
|
|
products?: CmsProductContent[] | null;
|
|
}) {
|
|
const list = products?.filter((p) => p.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 Product
|
|
</p>
|
|
<h2 className="mx-auto max-w-3xl text-4xl font-extrabold leading-tight md:text-5xl whitespace-pre-line">
|
|
{DEFAULT_TITLE}
|
|
</h2>
|
|
</div>
|
|
<div className="flex flex-col items-center gap-12 md:flex-row md:items-center">
|
|
<ProductImageBlock imgSrc="/image/p1.png" alt="Product illustration" />
|
|
<div className="w-full max-w-xl md:flex-1">
|
|
<div className="mb-6 flex h-12 w-12 items-center justify-center rounded-xl bg-[#fdecc8]">
|
|
<Image src="/image/product-icon.png" alt="" width={22} height={22} />
|
|
</div>
|
|
<h3 className="mb-4 text-2xl font-bold text-gray-900 md:text-3xl">
|
|
MediaHUB Content Aggregator
|
|
</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 Product
|
|
</p>
|
|
<h2 className="mx-auto max-w-3xl text-4xl font-extrabold leading-tight md:text-5xl">
|
|
The product we offer is
|
|
<span className="relative inline-block">
|
|
<span className="absolute bottom-2 left-0 h-3 w-full bg-[#f5d28a]"></span>
|
|
<span className="relative italic text-[#966314]">designed</span></span> to meet your business needs.
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-20 md:gap-28">
|
|
{list.map((p, index) => {
|
|
const imgSrc = cardImageUrl(p);
|
|
const alt = p.primary_title?.trim() || "Product";
|
|
const reverse = index % 2 === 1;
|
|
return (
|
|
<div
|
|
key={p.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" : ""
|
|
}`}
|
|
>
|
|
<ProductImageBlock imgSrc={imgSrc} alt={alt} />
|
|
<ProductTextBlock p={p} order={index + 1} />
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|