qudoco-fe/components/landing-page/product.tsx

220 lines
7.4 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
id="products"
className="scroll-mt-24 bg-white py-24 md:scroll-mt-[5.5rem] 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
id="products"
className="scroll-mt-24 bg-white py-24 md:scroll-mt-[5.5rem] 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>
);
}