From b6fa161efb8765e1dc71caa149be3e41fbb51393 Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Sat, 11 Apr 2026 01:54:11 +0700 Subject: [PATCH] feat: update bug fixing content website and landing page --- .env | 4 +- components/landing-page/headers.tsx | 54 +++--- components/landing-page/product.tsx | 257 ++++++++++++++----------- components/landing-page/service.tsx | 198 ++++++++++++------- components/landing-page/technology.tsx | 6 +- components/main/content-website.tsx | 77 +++++++- package.json | 2 +- service/cms-landing.ts | 4 + types/cms-landing.ts | 2 + 9 files changed, 392 insertions(+), 212 deletions(-) diff --git a/.env b/.env index 5e5e04b..41dfb31 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ MEDOLS_CLIENT_KEY=bb65b1ad-e954-4a1a-b4d0-74df5bb0b640 -# NEXT_PUBLIC_API_URL=http://localhost:8800 -NEXT_PUBLIC_API_URL=https://qudo.id/api \ No newline at end of file +NEXT_PUBLIC_API_URL=http://localhost:8800 +# NEXT_PUBLIC_API_URL=https://qudo.id/api \ No newline at end of file diff --git a/components/landing-page/headers.tsx b/components/landing-page/headers.tsx index 89753ed..49b5c83 100644 --- a/components/landing-page/headers.tsx +++ b/components/landing-page/headers.tsx @@ -18,12 +18,16 @@ export default function Header({ hero }: { hero?: CmsHeroContent | null }) { const [open, setOpen] = useState(false); const [contactOpen, setContactOpen] = useState(false); - const title = - hero?.primary_title?.trim() || - "Beyond Expectations to Build Reputation."; + // Only main title is required in CMS; optional fields stay empty (no placeholder dashes or fake CTAs). + const fallbackHeadline = "Beyond Expectations to Build Reputation."; + const title = hero?.primary_title?.trim() + ? hero.primary_title.trim() + : !hero + ? fallbackHeadline + : ""; const subtitle = hero?.secondary_title?.trim(); const lead = hero?.description?.trim(); - const primaryCta = hero?.primary_cta?.trim() || "Contact Us"; + const primaryCta = hero?.primary_cta?.trim(); const secondaryCta = hero?.secondary_cta_text?.trim(); const heroImg = hero?.images?.[0]?.image_url?.trim(); @@ -73,25 +77,29 @@ export default function Header({ hero }: { hero?: CmsHeroContent | null }) {

) : null} -
- - {secondaryCta ? ( - - ) : null} -
+ {(primaryCta || secondaryCta) ? ( +
+ {primaryCta ? ( + + ) : null} + {secondaryCta ? ( + + ) : null} +
+ ) : null}
diff --git a/components/landing-page/product.tsx b/components/landing-page/product.tsx index 3c6a2cd..f61abaa 100644 --- a/components/landing-page/product.tsx +++ b/components/landing-page/product.tsx @@ -1,4 +1,5 @@ import Image from "next/image"; +import Link from "next/link"; import { Check } from "lucide-react"; import type { CmsProductContent } from "@/types/cms-landing"; @@ -11,6 +12,129 @@ 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 ( + + Learn More → + + ); + } + return ( + + Learn More → + + ); +} + +/** 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 ( + <> + {label}: {rest} + + ); + } + return <>{text}; +} + +function ProductImageBlock({ + imgSrc, + alt, +}: { + imgSrc: string; + alt: string; +}) { + const external = /^https?:\/\//i.test(imgSrc); + return ( +
+
+ {external ? ( + // eslint-disable-next-line @next/next/no-img-element + {alt} + ) : ( + {alt} + )} +
+
+ ); +} + +function ProductOrderBadge({ order }: { order: number }) { + return ( +
+ {order} +
+ ); +} + +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 ( +
+ +

+ {p.primary_title?.trim() || "Product"} +

+ {p.secondary_title?.trim() ? ( +

+ {p.secondary_title.trim()} +

+ ) : null} + {useBullets ? ( +
    + {lines.map((item, i) => ( +
  • + + + + + + +
  • + ))} +
+ ) : ( +

+ {bodyText} +

+ )} + +
+ ); +} + export default function ProductSection({ products, }: { @@ -20,9 +144,9 @@ export default function ProductSection({ if (list.length === 0) { return ( -
+
-
+

Our Product

@@ -30,37 +154,19 @@ export default function ProductSection({ {DEFAULT_TITLE}
-
-
- Product illustration -
-
+
+ +
- +
-

+

MediaHUB Content Aggregator

-

+

{DEFAULT_BODY}

- +
@@ -68,100 +174,35 @@ export default function ProductSection({ ); } - const first = list[0]; - const sectionTitle = - first?.primary_title?.trim() || "Products tailored to your business"; - const subtitle = first?.secondary_title?.trim(); - return ( -
+
-
+

Our Product

-

- {list.length > 1 ? "Our Products" : sectionTitle} +

+ The product we offer is + + + designed to meet your business needs.

- {subtitle && list.length > 1 ? ( -

{subtitle}

- ) : null}
-
- {list.map((p) => { +
+ {list.map((p, index) => { const imgSrc = cardImageUrl(p); - const external = /^https?:\/\//i.test(imgSrc); - 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 ?? ""; - + const alt = p.primary_title?.trim() || "Product"; + const reverse = index % 2 === 1; return (
-
- {external ? ( - // eslint-disable-next-line @next/next/no-img-element - - ) : ( - - )} -
-
-
- -
-

- {p.primary_title?.trim() || "Product"} -

- {p.secondary_title?.trim() ? ( -

- {p.secondary_title.trim()} -

- ) : null} - {useBullets ? ( -
    - {lines.map((item) => ( -
  • - - - - {item} -
  • - ))} -
- ) : ( -

- {bodyText} -

- )} - -
+ +
); })} diff --git a/components/landing-page/service.tsx b/components/landing-page/service.tsx index 7234627..903adc2 100644 --- a/components/landing-page/service.tsx +++ b/components/landing-page/service.tsx @@ -1,14 +1,114 @@ 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 bannerUrl(s: CmsServiceContent) { +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 ( + + Learn More → + + ); + } + return ( + + Learn More → + + ); +} + +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 ( + <> + {label}: {rest} + + ); + } + return <>{text}; +} + +function ServiceImageBlock({ imgSrc, alt }: { imgSrc: string; alt: string }) { + const external = /^https?:\/\//i.test(imgSrc); + return ( +
+
+ {external ? ( + // eslint-disable-next-line @next/next/no-img-element + {alt} + ) : ( + {alt} + )} +
+
+ ); +} + +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 ( +
+

+ {s.primary_title?.trim() || "Service"} +

+ {s.secondary_title?.trim() ? ( +

{s.secondary_title.trim()}

+ ) : null} + {useBullets ? ( +
    + {lines.map((item, i) => ( +
  • + + + + + + +
  • + ))} +
+ ) : ( +

+ {bodyText} +

+ )} + +
+ ); +} + export default function ServiceSection({ services, }: { @@ -17,28 +117,25 @@ export default function ServiceSection({ const list = services?.filter((s) => s.id) ?? []; if (list.length === 0) { - const imgSrc = "/image/s1.png"; return ( -
+
-
-

Our Services

-

+
+

+ Our Services +

+

{DEFAULT_HEADING}

-
-
- Service -
-
-

{DEFAULT_BODY}

+
+ +
+

Our services

+

+ {DEFAULT_BODY} +

+
@@ -47,62 +144,31 @@ export default function ServiceSection({ } return ( -
+
-
-

Our Services

-

- {list.length > 1 ? "What we deliver" : list[0]?.primary_title?.trim() || DEFAULT_HEADING} +
+

+ Our Services +

+

+ {DEFAULT_HEADING}

- {list.length > 1 && list[0]?.secondary_title?.trim() ? ( -

- {list[0].secondary_title.trim()} -

- ) : list.length === 1 && list[0]?.secondary_title?.trim() ? ( -

- {list[0].secondary_title.trim()} -

- ) : null}
-
- {list.map((s) => { - const imgSrc = bannerUrl(s); - const external = /^https?:\/\//i.test(imgSrc); - const body = s.description?.trim() || DEFAULT_BODY; +
+ {list.map((s, index) => { + const imgSrc = cardImageUrl(s); + const alt = s.primary_title?.trim() || "Service"; + const reverse = index % 2 === 1; return (
-
- {external ? ( - // eslint-disable-next-line @next/next/no-img-element - - ) : ( - - )} -
-
-

- {s.primary_title?.trim() || "Service"} -

-

- {body} -

-
+ +
); })} diff --git a/components/landing-page/technology.tsx b/components/landing-page/technology.tsx index 2c4c8d8..a41b50e 100644 --- a/components/landing-page/technology.tsx +++ b/components/landing-page/technology.tsx @@ -32,7 +32,7 @@ export default function Technology({

-
+
{loop.map((tech, index) => (
) : tech.src ? ( // eslint-disable-next-line @next/next/no-img-element diff --git a/components/main/content-website.tsx b/components/main/content-website.tsx index a483539..a76c968 100644 --- a/components/main/content-website.tsx +++ b/components/main/content-website.tsx @@ -121,6 +121,7 @@ export default function ContentWebsite() { const [productPrimary, setProductPrimary] = useState(""); const [productSecondary, setProductSecondary] = useState(""); const [productDesc, setProductDesc] = useState(""); + const [productLinkUrl, setProductLinkUrl] = useState(""); const [productRemoteUrl, setProductRemoteUrl] = useState(""); const [productPendingFile, setProductPendingFile] = useState( null, @@ -134,6 +135,7 @@ export default function ContentWebsite() { const [servicePrimary, setServicePrimary] = useState(""); const [serviceSecondary, setServiceSecondary] = useState(""); const [serviceDesc, setServiceDesc] = useState(""); + const [serviceLinkUrl, setServiceLinkUrl] = useState(""); const [serviceRemoteUrl, setServiceRemoteUrl] = useState(""); const [servicePendingFile, setServicePendingFile] = useState( null, @@ -374,6 +376,7 @@ export default function ContentWebsite() { setProductPrimary(""); setProductSecondary(""); setProductDesc(""); + setProductLinkUrl(""); revokeBlobRef(productBlobUrlRef); setProductPendingFile(null); setProductRemoteUrl(""); @@ -384,6 +387,7 @@ export default function ContentWebsite() { setProductPrimary(p.primary_title ?? ""); setProductSecondary(p.secondary_title ?? ""); setProductDesc(p.description ?? ""); + setProductLinkUrl(p.link_url ?? ""); const im = p.images?.[0]; revokeBlobRef(productBlobUrlRef); setProductPendingFile(null); @@ -412,6 +416,7 @@ export default function ContentWebsite() { primary_title: productPrimary, secondary_title: productSecondary, description: productDesc, + link_url: productLinkUrl, }; let pid = productEditId; if (!pid) { @@ -485,6 +490,7 @@ export default function ContentWebsite() { setServicePrimary(""); setServiceSecondary(""); setServiceDesc(""); + setServiceLinkUrl(""); revokeBlobRef(serviceBlobUrlRef); setServicePendingFile(null); setServiceRemoteUrl(""); @@ -495,6 +501,7 @@ export default function ContentWebsite() { setServicePrimary(s.primary_title ?? ""); setServiceSecondary(s.secondary_title ?? ""); setServiceDesc(s.description ?? ""); + setServiceLinkUrl(s.link_url ?? ""); const im = s.images?.[0]; revokeBlobRef(serviceBlobUrlRef); setServicePendingFile(null); @@ -798,12 +805,21 @@ export default function ContentWebsite() { if (!ok.isConfirmed) return; setSaving(true); try { - await deletePopupNews(id); + const res = await deletePopupNews(id); + if (res?.error) { + await Swal.fire({ + icon: "error", + title: "Delete failed", + text: String(res.message ?? "Could not delete pop up."), + }); + return; + } if (popupEditId === id) { beginEditPopup(null); setPopupModalOpen(false); } await loadAll(); + await Swal.fire({ icon: "success", title: "Deleted", timer: 1200, showConfirmButton: false }); } finally { setSaving(false); } @@ -870,7 +886,9 @@ export default function ContentWebsite() {
- +
- + setHeroSecondary(e.target.value)} + placeholder="" />
- +