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}
-
- setContactOpen(true)}
- className="rounded-full bg-[#966314] px-8 py-6 text-base hover:bg-[#7c520f]"
- >
- {primaryCta}
-
- {secondaryCta ? (
-
- {secondaryCta}
-
- ) : null}
-
+ {(primaryCta || secondaryCta) ? (
+
+ {primaryCta ? (
+ setContactOpen(true)}
+ className="rounded-full bg-[#966314] px-8 py-6 text-base hover:bg-[#7c520f]"
+ >
+ {primaryCta}
+
+ ) : null}
+ {secondaryCta ? (
+
+ {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
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+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}
-
-
-
-
-
+
+
+
-
+
-
+
MediaHUB Content Aggregator
-
+
{DEFAULT_BODY}
-
- Learn More →
-
+
@@ -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}
-
- )}
-
- Learn More →
-
-
+
+
);
})}
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
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+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}
-
-
-
-
-
-
{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() {
- Description
+
+ Description (optional)
+
@@ -929,19 +952,25 @@ export default function ContentWebsite() {
@@ -1177,7 +1206,9 @@ export default function ContentWebsite() {
{productEditId ? "Edit product" : "Add Product"}
- Fill in the details below. Image URL is shown on the product card on the homepage.
+ Homepage shows each product as a full-width row (image and text alternate). Use one line per bullet in
+ Description; optional "Label: detail" formats the label in bold. Product link powers the Learn
+ More action.
@@ -1207,6 +1238,19 @@ export default function ContentWebsite() {
onChange={(e) => setProductDesc(e.target.value)}
/>
+
+
Product link (optional)
+
setProductLinkUrl(e.target.value)}
+ />
+
+ Shown as "Learn More" on the landing page. Leave empty to disable the link.
+
+