diff --git a/app/page.tsx b/app/page.tsx index 06f1c2e..c640c36 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -5,19 +5,50 @@ import ServiceSection from "@/components/landing-page/service"; import Technology from "@/components/landing-page/technology"; import Footer from "@/components/landing-page/footer"; import FloatingMenu from "@/components/landing-page/floating"; +import PopupNewsBanner from "@/components/landing-page/popup-news"; +import { publicFetch } from "@/lib/public-api"; +import type { + CmsAboutContent, + CmsHeroContent, + CmsPartnerContent, + CmsPopupContent, + CmsProductContent, + CmsServiceContent, +} from "@/types/cms-landing"; + +export default async function Home() { + const [hero, aboutList, productList, serviceList, partners, popupList] = + await Promise.all([ + publicFetch("/hero-contents"), + publicFetch("/about-us-contents"), + publicFetch("/our-product-contents"), + publicFetch("/our-service-contents"), + publicFetch("/partner-contents"), + publicFetch("/popup-news-contents?page=1&limit=20"), + ]); + + const about = aboutList?.[0] ?? null; + const products = Array.isArray(productList) + ? productList + : productList + ? [productList] + : []; + const services = Array.isArray(serviceList) + ? serviceList + : serviceList + ? [serviceList] + : []; + const popups = Array.isArray(popupList) ? popupList : []; -export default function Home() { return (
- {/* FIXED MENU */} - - {/* PAGE CONTENT */} -
- - - - + +
+ + + +
); diff --git a/components/landing-page/about.tsx b/components/landing-page/about.tsx index 4ffcad4..9a5ff0a 100644 --- a/components/landing-page/about.tsx +++ b/components/landing-page/about.tsx @@ -2,15 +2,28 @@ import Image from "next/image"; import { motion } from "framer-motion"; +import type { CmsAboutContent } from "@/types/cms-landing"; -export default function AboutSection() { - const socials = [ - { name: "Facebook", icon: "/image/fb.png" }, - { name: "Instagram", icon: "/image/ig.png" }, - { name: "X", icon: "/image/x.png" }, - { name: "Youtube", icon: "/image/yt.png" }, - { name: "Tiktok", icon: "/image/tt.png" }, - ]; +const DEFAULT_KICKER = "About Us"; +const DEFAULT_HEADLINE = "Helping you find the right Solution"; +const DEFAULT_DESC = + "PT Qudo Buana Nawakara adalah perusahaan nasional Indonesia yang berfokus pada pengembangan aplikasi..."; + +export default function AboutSection({ + about, +}: { + about?: CmsAboutContent | null; +}) { + const kicker = about?.secondary_title?.trim() || DEFAULT_KICKER; + const headline = about?.primary_title?.trim() || DEFAULT_HEADLINE; + const desc = about?.description?.trim() || DEFAULT_DESC; + + const media = about?.images?.[0]; + const mediaUrl = media?.media_url?.trim(); + const isVideo = + media?.media_type?.startsWith("video") || + /\.(mp4|webm)(\?|$)/i.test(mediaUrl ?? "") || + (mediaUrl?.toLowerCase().includes("video") ?? false); const messages = [ { id: 1, text: "Dimana posisi Ayah saya sekarang?", type: "user" }, @@ -30,58 +43,65 @@ export default function AboutSection() { return (
- {/* PHONE WRAPPER */}
-
- {/* PHONE IMAGE */} +
App Preview - {/* CHAT AREA */} -
-
- {messages.map((msg, index) => ( - - {msg.text} - - ))} -
+
+ {mediaUrl && isVideo ? ( + // eslint-disable-next-line jsx-a11y/media-has-caption +
- {/* TEXT CONTENT */}

- About Us + {kicker}

-

- Helping you find the right{" "} +

- Solution + {headline}

-

- PT Qudo Buana Nawakara adalah perusahaan nasional Indonesia yang - berfokus pada pengembangan aplikasi... -

+

{desc}

diff --git a/components/landing-page/headers.tsx b/components/landing-page/headers.tsx index c63f10e..89753ed 100644 --- a/components/landing-page/headers.tsx +++ b/components/landing-page/headers.tsx @@ -8,11 +8,25 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { Textarea } from "../ui/textarea"; import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; +import type { CmsHeroContent } from "@/types/cms-landing"; -export default function Header() { +function isExternalUrl(url: string) { + return /^https?:\/\//i.test(url); +} + +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."; + const subtitle = hero?.secondary_title?.trim(); + const lead = hero?.description?.trim(); + const primaryCta = hero?.primary_cta?.trim() || "Contact Us"; + const secondaryCta = hero?.secondary_cta_text?.trim(); + const heroImg = hero?.images?.[0]?.image_url?.trim(); + return ( <>
@@ -47,32 +61,58 @@ export default function Header() { {/* HERO */}
-

- - - Beyond Expectations - -
- Build Reputation. +

+ {title}

+ {subtitle ? ( +

{subtitle}

+ ) : null} + {lead ? ( +

+ {lead} +

+ ) : null} - +
+ + {secondaryCta ? ( + + ) : null} +
-
- Illustration +
+ {heroImg && isExternalUrl(heroImg) ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : ( + Illustration + )}
diff --git a/components/landing-page/popup-news.tsx b/components/landing-page/popup-news.tsx new file mode 100644 index 0000000..45ee6f0 --- /dev/null +++ b/components/landing-page/popup-news.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { useState } from "react"; +import { ChevronLeft, ChevronRight, X } from "lucide-react"; +import type { CmsPopupContent } from "@/types/cms-landing"; + +export default function PopupNewsBanner({ + popups, +}: { + popups?: CmsPopupContent[] | null; +}) { + const list = popups?.filter((p) => p.primary_title?.trim()) ?? []; + const [open, setOpen] = useState(true); + const [idx, setIdx] = useState(0); + + if (!open || list.length === 0) return null; + + const popup = list[idx % list.length]; + const img = popup.images?.[0]?.media_url?.trim(); + + return ( +
+
+ + {list.length > 1 ? ( + <> + + + + ) : null} + {img ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : null} +
+

+ {popup.secondary_title || "News"} +

+

{popup.primary_title}

+ {popup.description ? ( +

{popup.description}

+ ) : null} +
+ {popup.primary_cta ? ( + + {popup.primary_cta} + + ) : null} + {popup.secondary_cta_text ? ( + + {popup.secondary_cta_text} + + ) : null} +
+ {list.length > 1 ? ( +
+ {list.map((_, i) => ( +
+ ) : null} +
+
+
+ ); +} diff --git a/components/landing-page/product.tsx b/components/landing-page/product.tsx index d841849..3c6a2cd 100644 --- a/components/landing-page/product.tsx +++ b/components/landing-page/product.tsx @@ -1,187 +1,170 @@ import Image from "next/image"; 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"; +} + +export default function ProductSection({ + products, +}: { + products?: CmsProductContent[] | null; +}) { + const list = products?.filter((p) => p.id) ?? []; + + if (list.length === 0) { + return ( +
+
+
+

+ Our Product +

+

+ {DEFAULT_TITLE} +

+
+
+
+ Product illustration +
+
+
+ +
+

+ MediaHUB Content Aggregator +

+

+ {DEFAULT_BODY} +

+ +
+
+
+
+ ); + } + + const first = list[0]; + const sectionTitle = + first?.primary_title?.trim() || "Products tailored to your business"; + const subtitle = first?.secondary_title?.trim(); -export default function ProductSection() { - const features = [ - "Content Creation: Producing creative and engaging content such as posts, images, videos, and stories that align with the brand and attract audience attention.", - "Social Media Account Management: Managing business social media accounts, including scheduling posts, monitoring interactions, and engaging with followers.", - "Paid Advertising Campaigns: Designing, executing, and managing paid advertising campaigns on various social media platforms to reach a more specific target audience and improve ROI (Return on Investment).", - ]; return (
- {/* TITLE */} -
+

Our Product

- -

- The product we offer is{" "} - - - designed - {" "} - to meet your business needs. +

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

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

{subtitle}

+ ) : null}
- {/* CONTENT */} -
- {/* LEFT IMAGE */} -
- Product Illustration -
+
+ {list.map((p) => { + 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 ?? ""; - {/* RIGHT CONTENT */} -
- {/* ICON */} -
- Product Icon -
- -

- MediaHUB Content Aggregator -

- -

- Social media marketing services are provided by companies or - individuals who specialize in marketing strategies through social - media platforms. -

- - {/* FEATURES */} -
    - {features.map((item) => ( -
  • - - - - {item} -
  • - ))} -
- - {/* CTA */} - -
-
-
- {/* LEFT IMAGE */} - - {/* RIGHT CONTENT */} -
- {/* ICON */} -
- Product Icon -
- -

- Multipool Reputation Management -

- -

- Social media marketing services are provided by companies or - individuals who specialize in marketing strategies through social - media platforms. -

- - {/* FEATURES */} -
    - {features.map((item) => ( -
  • - - - - {item} -
  • - ))} -
- - {/* CTA */} - -
-
- Product Illustration -
-
-
- {/* LEFT IMAGE */} -
- Product Illustration -
- - {/* RIGHT CONTENT */} -
- {/* ICON */} -
- Product Icon -
- -

- PR Room Opinion Management -

- -

- Social media marketing services are provided by companies or - individuals who specialize in marketing strategies through social - media platforms. -

- - {/* FEATURES */} -
    - {features.map((item) => ( -
  • - - - - {item} -
  • - ))} -
- - {/* CTA */} - -
+ 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 8000805..7234627 100644 --- a/components/landing-page/service.tsx +++ b/components/landing-page/service.tsx @@ -1,187 +1,111 @@ import Image from "next/image"; +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) { + return s.images?.[0]?.image_url?.trim() || "/image/s1.png"; +} + +export default function ServiceSection({ + services, +}: { + services?: CmsServiceContent[] | null; +}) { + const list = services?.filter((s) => s.id) ?? []; + + if (list.length === 0) { + const imgSrc = "/image/s1.png"; + return ( +
+
+
+

Our Services

+

+ {DEFAULT_HEADING} +

+
+
+
+ Service +
+
+

{DEFAULT_BODY}

+
+
+
+
+ ); + } -export default function ServiceSection() { return ( -
+
- {/* Heading */} -
-

- Our Services -

-

- Innovative solutions for your{" "} - - business growth - - - . +
+

Our Services

+

+ {list.length > 1 ? "What we deliver" : list[0]?.primary_title?.trim() || 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}
- {/* Service 1 */} -
- {/* Image */} -
- Artifintel Soundworks -
- - {/* Content */} -
-

- Artifintel -

-

- Artifintel Soundworks adalah pionir musik AI yang menghadirkan - karya dan kolaborasi dari musisi AI penuh kreativitas dan emosi. - Sejak 2024, Artifintel telah merilis belasan hingga puluhan lagu - dengan melodi memukau dan ritme inovatif. -

- -
    -
  • ✔ AI Music Composition & Songwriting
  • -
  • ✔ Vocal Synthesis & AI Musicians
  • -
  • ✔ Genre Exploration & Sound Innovation
  • -
  • ✔ AI Collaboration & Creative Experimentation
  • -
  • ✔ Music Release & Digital Distribution
  • -
-
-
- - {/* Service 2 */} -
- {/* Content */} -
-

- Produksi Video Animasi -

-

- Professional animation production services that bring your ideas - to life. From explainer videos to brand storytelling, we create - engaging animated content that resonates with your audience. -

- -
    -
  • ✔ 2D & 3D Animation Production
  • -
  • ✔ Motion Graphics & Visual Effects
  • -
  • ✔ Character Design & Storyboarding
  • -
-
- - {/* Image */} -
- Animasee -
-
-
- {/* Image */} -
- Artifintel Soundworks -
- - {/* Content */} -
-

- Reelithic -

-

- Artifintel Soundworks adalah pionir musik AI yang menghadirkan - karya dan kolaborasi dari musisi AI penuh kreativitas dan emosi. - Sejak 2024, Artifintel telah merilis belasan hingga puluhan lagu - dengan melodi memukau dan ritme inovatif. -

- -
    -
  • ✔ AI Music Composition & Songwriting
  • -
  • ✔ Vocal Synthesis & AI Musicians
  • -
  • ✔ Genre Exploration & Sound Innovation
  • -
  • ✔ AI Collaboration & Creative Experimentation
  • -
  • ✔ Music Release & Digital Distribution
  • -
-
-
- - {/* Service 3 */} -
- {/* Content */} -
-

- Qudoin -

-

- Professional animation production services that bring your ideas - to life. From explainer videos to brand storytelling, we create - engaging animated content that resonates with your audience. -

- -
    -
  • ✔ 2D & 3D Animation Production
  • -
  • ✔ Motion Graphics & Visual Effects
  • -
  • ✔ Character Design & Storyboarding
  • -
-
- - {/* Image */} -
- Animasee -
-
-
- {/* Image */} -
- Artifintel Soundworks -
- - {/* Content */} -
-

- Talkshow AI -

-

- Artifintel Soundworks adalah pionir musik AI yang menghadirkan - karya dan kolaborasi dari musisi AI penuh kreativitas dan emosi. - Sejak 2024, Artifintel telah merilis belasan hingga puluhan lagu - dengan melodi memukau dan ritme inovatif. -

- -
    -
  • ✔ AI Music Composition & Songwriting
  • -
  • ✔ Vocal Synthesis & AI Musicians
  • -
  • ✔ Genre Exploration & Sound Innovation
  • -
  • ✔ AI Collaboration & Creative Experimentation
  • -
  • ✔ Music Release & Digital Distribution
  • -
-
+
+ {list.map((s) => { + const imgSrc = bannerUrl(s); + const external = /^https?:\/\//i.test(imgSrc); + const body = s.description?.trim() || DEFAULT_BODY; + 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 637b7db..2c4c8d8 100644 --- a/components/landing-page/technology.tsx +++ b/components/landing-page/technology.tsx @@ -1,6 +1,6 @@ -import Image from "next/image"; +import type { CmsPartnerContent } from "@/types/cms-landing"; -const technologies = [ +const FALLBACK: { name: string; src: string }[] = [ { name: "Tableau", src: "/image/tableu.png" }, { name: "TVU Networks", src: "/image/tvu.png" }, { name: "AWS", src: "/image/aws.png" }, @@ -9,31 +9,56 @@ const technologies = [ { name: "Ui", src: "/image/uipath.png" }, ]; -export default function Technology() { +export default function Technology({ + partners, +}: { + partners?: CmsPartnerContent[] | null; +}) { + const list = + partners && partners.length > 0 + ? partners.map((p) => ({ + name: p.primary_title, + src: p.image_url || p.image_path || "", + })) + : FALLBACK; + + const loop = [...list, ...list]; + return (
- {/* Title */} -

+

TECHNOLOGY PARTNERS

- {/* Slider */}
- {/* duplicated for seamless loop */} - {[...technologies, ...technologies].map((tech, index) => ( + {loop.map((tech, index) => (
- {tech.name} + {tech.src && /^https?:\/\//i.test(tech.src) ? ( + // eslint-disable-next-line @next/next/no-img-element + {tech.name} + ) : tech.src ? ( + // eslint-disable-next-line @next/next/no-img-element + {tech.name} + ) : ( + {tech.name} + )}
))}
diff --git a/components/main/content-website.tsx b/components/main/content-website.tsx index 836a3a1..f36c5d4 100644 --- a/components/main/content-website.tsx +++ b/components/main/content-website.tsx @@ -1,45 +1,717 @@ "use client"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Eye, Pencil, ImageIcon } from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Eye, Pencil, ImageIcon, Loader2, Plus, Trash2 } from "lucide-react"; +import Swal from "sweetalert2"; +import type { + CmsAboutContent, + CmsHeroContent, + CmsPartnerContent, + CmsPopupContent, + CmsProductContent, + CmsServiceContent, +} from "@/types/cms-landing"; +import { + apiDataList, + apiPayload, + apiRows, + deleteAboutUsContentImage, + deleteOurProductContent, + deleteOurServiceContent, + deletePartnerContent, + deletePopupNews, + getAboutContentsList, + getHeroContent, + getOurProductContent, + getOurServiceContent, + getPartnerContents, + getPopupNewsList, + saveAboutContent, + saveAboutUsMediaUrl, + saveHeroContent, + saveHeroImage, + saveOurProductContent, + saveOurServiceContent, + savePartnerContent, + saveOurProductImage, + saveOurServiceImage, + savePopupNews, + savePopupNewsImage, + updateAboutContent, + updateHeroContent, + updateHeroImage, + updateOurProductContent, + updateOurProductImage, + updateOurServiceContent, + updateOurServiceImage, + updatePartnerContent, + updatePopupNews, +} from "@/service/cms-landing"; export default function ContentWebsite() { const [activeTab, setActiveTab] = useState("hero"); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + const [heroId, setHeroId] = useState(null); + const [heroImageId, setHeroImageId] = useState(null); + const [heroPrimary, setHeroPrimary] = useState(""); + const [heroSecondary, setHeroSecondary] = useState(""); + const [heroDesc, setHeroDesc] = useState(""); + const [heroCta1, setHeroCta1] = useState(""); + const [heroCta2, setHeroCta2] = useState(""); + const [heroImgUrl, setHeroImgUrl] = useState(""); + + const [aboutId, setAboutId] = useState(null); + const [aboutPrimary, setAboutPrimary] = useState(""); + const [aboutSecondary, setAboutSecondary] = useState(""); + const [aboutDesc, setAboutDesc] = useState(""); + const [aboutCta1, setAboutCta1] = useState(""); + const [aboutCta2, setAboutCta2] = useState(""); + const [aboutMediaUrl, setAboutMediaUrl] = useState(""); + const [aboutMediaImageId, setAboutMediaImageId] = useState( + null, + ); + const [aboutMediaLoadedUrl, setAboutMediaLoadedUrl] = useState(""); + + const [products, setProducts] = useState([]); + const [productEditId, setProductEditId] = useState(null); + const [productPrimary, setProductPrimary] = useState(""); + const [productSecondary, setProductSecondary] = useState(""); + const [productDesc, setProductDesc] = useState(""); + const [productImgUrl, setProductImgUrl] = useState(""); + const [productImageId, setProductImageId] = useState(null); + const [productModalOpen, setProductModalOpen] = useState(false); + + const [services, setServices] = useState([]); + const [serviceEditId, setServiceEditId] = useState(null); + const [servicePrimary, setServicePrimary] = useState(""); + const [serviceSecondary, setServiceSecondary] = useState(""); + const [serviceDesc, setServiceDesc] = useState(""); + const [serviceImgUrl, setServiceImgUrl] = useState(""); + const [serviceImageId, setServiceImageId] = useState(null); + const [serviceModalOpen, setServiceModalOpen] = useState(false); + + const [partners, setPartners] = useState([]); + const [partnerTitle, setPartnerTitle] = useState(""); + const [partnerImgUrl, setPartnerImgUrl] = useState(""); + const [editingPartnerId, setEditingPartnerId] = useState(null); + const [partnerModalOpen, setPartnerModalOpen] = useState(false); + + const [popups, setPopups] = useState([]); + const [popupEditId, setPopupEditId] = useState(null); + const [popupPrimary, setPopupPrimary] = useState(""); + const [popupSecondary, setPopupSecondary] = useState(""); + const [popupDesc, setPopupDesc] = useState(""); + const [popupCta1, setPopupCta1] = useState(""); + const [popupCta2, setPopupCta2] = useState(""); + const [popupImgUrl, setPopupImgUrl] = useState(""); + const [popupModalOpen, setPopupModalOpen] = useState(false); + + const loadAll = useCallback(async () => { + setLoading(true); + try { + const heroRes = await getHeroContent(); + const hero = apiPayload(heroRes) as CmsHeroContent | null; + if (hero?.id) { + setHeroId(hero.id); + setHeroPrimary(hero.primary_title ?? ""); + setHeroSecondary(hero.secondary_title ?? ""); + setHeroDesc(hero.description ?? ""); + setHeroCta1(hero.primary_cta ?? ""); + setHeroCta2(hero.secondary_cta_text ?? ""); + const first = hero.images?.[0]; + if (first?.image_url) { + setHeroImgUrl(first.image_url); + setHeroImageId(first.id ?? null); + } else { + setHeroImgUrl(""); + setHeroImageId(null); + } + } else { + setHeroId(null); + setHeroImageId(null); + setHeroPrimary(""); + setHeroSecondary(""); + setHeroDesc(""); + setHeroCta1(""); + setHeroCta2(""); + setHeroImgUrl(""); + } + + const aboutRes = await getAboutContentsList(); + const aboutList = apiRows(aboutRes) as CmsAboutContent[]; + const ab = aboutList[0]; + if (ab) { + setAboutId(ab.id); + setAboutPrimary(ab.primary_title ?? ""); + setAboutSecondary(ab.secondary_title ?? ""); + setAboutDesc(ab.description ?? ""); + setAboutCta1(ab.primary_cta ?? ""); + setAboutCta2(ab.secondary_cta_text ?? ""); + const am = ab.images?.[0]; + const murl = am?.media_url?.trim() ?? ""; + setAboutMediaUrl(murl); + setAboutMediaLoadedUrl(murl); + setAboutMediaImageId(am?.id ?? null); + } else { + setAboutId(null); + setAboutPrimary(""); + setAboutSecondary(""); + setAboutDesc(""); + setAboutCta1(""); + setAboutCta2(""); + setAboutMediaUrl(""); + setAboutMediaLoadedUrl(""); + setAboutMediaImageId(null); + } + + const prRes = await getOurProductContent(); + setProducts(apiDataList(prRes)); + + const svRes = await getOurServiceContent(); + setServices(apiDataList(svRes)); + + const parRes = await getPartnerContents(); + const parList = apiPayload(parRes) as CmsPartnerContent[] | null; + setPartners(Array.isArray(parList) ? parList : []); + + const popRes = await getPopupNewsList(1, 50); + setPopups(apiRows(popRes) as CmsPopupContent[]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadAll(); + }, [loadAll]); + + async function saveHeroTab() { + if (!heroPrimary.trim()) { + await Swal.fire({ icon: "warning", title: "Main title is required" }); + return; + } + setSaving(true); + try { + const body = { + primary_title: heroPrimary, + secondary_title: heroSecondary, + description: heroDesc, + primary_cta: heroCta1, + secondary_cta_text: heroCta2, + }; + let hid = heroId; + if (!hid) { + const res = await saveHeroContent(body); + const created = apiPayload(res) as CmsHeroContent | null; + if (!created?.id) { + await Swal.fire({ icon: "error", title: "Failed to create hero content" }); + return; + } + hid = created.id; + setHeroId(hid); + } else { + const res = await updateHeroContent(hid, body); + if (res?.error) { + await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") }); + return; + } + } + if (heroImgUrl.trim() && hid) { + if (heroImageId) { + await updateHeroImage(heroImageId, { image_url: heroImgUrl.trim() }); + } else { + await saveHeroImage({ + hero_content_id: hid, + image_url: heroImgUrl.trim(), + }); + } + } + await Swal.fire({ icon: "success", title: "Hero section saved", timer: 1600, showConfirmButton: false }); + await loadAll(); + } finally { + setSaving(false); + } + } + + async function saveAboutTab() { + if (!aboutPrimary.trim()) { + await Swal.fire({ icon: "warning", title: "Main title is required" }); + return; + } + setSaving(true); + try { + const body: Record = { + primary_title: aboutPrimary, + secondary_title: aboutSecondary, + description: aboutDesc, + primary_cta: aboutCta1, + secondary_cta_text: aboutCta2, + }; + let aid = aboutId; + if (aboutId == null) { + const res = await saveAboutContent(body); + if (res?.error) { + await Swal.fire({ icon: "error", title: "Save failed", text: String(res.message ?? "") }); + return; + } + const created = apiPayload(res) as CmsAboutContent | null; + aid = created?.id ?? null; + if (aid != null) setAboutId(aid); + } else { + const res = await updateAboutContent(aboutId, body); + if (res?.error) { + await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") }); + return; + } + } + + const url = aboutMediaUrl.trim(); + if (aid != null) { + if (!url) { + if (aboutMediaImageId != null) { + await deleteAboutUsContentImage(aboutMediaImageId); + } + } else if (url !== aboutMediaLoadedUrl || aboutMediaImageId == null) { + if (aboutMediaImageId != null) { + await deleteAboutUsContentImage(aboutMediaImageId); + } + const mres = await saveAboutUsMediaUrl({ + about_us_content_id: aid, + media_url: url, + }); + if (mres?.error) { + await Swal.fire({ + icon: "error", + title: "Media URL failed", + text: String(mres.message ?? ""), + }); + return; + } + } + } + + await Swal.fire({ icon: "success", title: "About Us saved", timer: 1600, showConfirmButton: false }); + await loadAll(); + } finally { + setSaving(false); + } + } + + function beginEditProduct(p: CmsProductContent | null) { + if (!p) { + setProductEditId(null); + setProductPrimary(""); + setProductSecondary(""); + setProductDesc(""); + setProductImgUrl(""); + setProductImageId(null); + return; + } + setProductEditId(p.id); + setProductPrimary(p.primary_title ?? ""); + setProductSecondary(p.secondary_title ?? ""); + setProductDesc(p.description ?? ""); + const im = p.images?.[0]; + setProductImgUrl(im?.image_url?.trim() ?? ""); + setProductImageId(im?.id ?? null); + } + + function openProductModalCreate() { + beginEditProduct(null); + setProductModalOpen(true); + } + + function openProductModalEdit(p: CmsProductContent) { + beginEditProduct(p); + setProductModalOpen(true); + } + + async function saveProductDraft() { + if (!productPrimary.trim()) { + await Swal.fire({ icon: "warning", title: "Product title is required" }); + return; + } + setSaving(true); + try { + const body = { + primary_title: productPrimary, + secondary_title: productSecondary, + description: productDesc, + }; + let pid = productEditId; + if (!pid) { + const res = await saveOurProductContent(body); + const created = apiPayload(res) as CmsProductContent | null; + if (!created?.id) { + await Swal.fire({ icon: "error", title: "Failed to create product" }); + return; + } + pid = created.id; + } else { + const res = await updateOurProductContent(pid, body); + if (res?.error) { + await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") }); + return; + } + } + if (productImgUrl.trim() && pid) { + if (productImageId) { + await updateOurProductImage(productImageId, { image_url: productImgUrl.trim() }); + } else { + await saveOurProductImage({ + our_product_content_id: pid, + image_url: productImgUrl.trim(), + }); + } + } + await Swal.fire({ icon: "success", title: "Product saved", timer: 1400, showConfirmButton: false }); + await loadAll(); + beginEditProduct(null); + setProductModalOpen(false); + } finally { + setSaving(false); + } + } + + async function removeProduct(id: string) { + const ok = await Swal.fire({ + icon: "warning", + title: "Delete this product?", + showCancelButton: true, + }); + if (!ok.isConfirmed) return; + setSaving(true); + try { + await deleteOurProductContent(id); + if (productEditId === id) { + beginEditProduct(null); + setProductModalOpen(false); + } + await loadAll(); + } finally { + setSaving(false); + } + } + + function beginEditService(s: CmsServiceContent | null) { + if (!s) { + setServiceEditId(null); + setServicePrimary(""); + setServiceSecondary(""); + setServiceDesc(""); + setServiceImgUrl(""); + setServiceImageId(null); + return; + } + setServiceEditId(s.id); + setServicePrimary(s.primary_title ?? ""); + setServiceSecondary(s.secondary_title ?? ""); + setServiceDesc(s.description ?? ""); + const im = s.images?.[0]; + setServiceImgUrl(im?.image_url?.trim() ?? ""); + setServiceImageId(im?.id != null ? String(im.id) : null); + } + + function openServiceModalCreate() { + beginEditService(null); + setServiceModalOpen(true); + } + + function openServiceModalEdit(s: CmsServiceContent) { + beginEditService(s); + setServiceModalOpen(true); + } + + async function saveServiceDraft() { + if (!servicePrimary.trim()) { + await Swal.fire({ icon: "warning", title: "Service title is required" }); + return; + } + setSaving(true); + try { + const body = { + primary_title: servicePrimary, + secondary_title: serviceSecondary, + description: serviceDesc, + }; + let sid = serviceEditId; + if (sid == null) { + const res = await saveOurServiceContent(body); + const created = apiPayload(res) as CmsServiceContent | null; + if (created?.id == null) { + await Swal.fire({ icon: "error", title: "Failed to create service" }); + return; + } + sid = created.id; + } else { + const res = await updateOurServiceContent(sid, body); + if (res?.error) { + await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") }); + return; + } + } + if (serviceImgUrl.trim() && sid != null) { + if (serviceImageId) { + await updateOurServiceImage(serviceImageId, { image_url: serviceImgUrl.trim() }); + } else { + await saveOurServiceImage({ + our_service_content_id: sid, + image_url: serviceImgUrl.trim(), + }); + } + } + await Swal.fire({ icon: "success", title: "Service saved", timer: 1400, showConfirmButton: false }); + await loadAll(); + beginEditService(null); + setServiceModalOpen(false); + } finally { + setSaving(false); + } + } + + async function removeService(id: number) { + const ok = await Swal.fire({ + icon: "warning", + title: "Delete this service?", + showCancelButton: true, + }); + if (!ok.isConfirmed) return; + setSaving(true); + try { + await deleteOurServiceContent(id); + if (serviceEditId === id) { + beginEditService(null); + setServiceModalOpen(false); + } + await loadAll(); + } finally { + setSaving(false); + } + } + + function beginEditPartner(p: CmsPartnerContent | null) { + if (!p) { + setEditingPartnerId(null); + setPartnerTitle(""); + setPartnerImgUrl(""); + return; + } + setEditingPartnerId(p.id); + setPartnerTitle(p.primary_title ?? ""); + setPartnerImgUrl(p.image_url ?? ""); + } + + function openPartnerModalCreate() { + beginEditPartner(null); + setPartnerModalOpen(true); + } + + function openPartnerModalEdit(p: CmsPartnerContent) { + beginEditPartner(p); + setPartnerModalOpen(true); + } + + async function savePartnerRow() { + if (!partnerTitle.trim()) { + await Swal.fire({ icon: "warning", title: "Partner name is required" }); + return; + } + setSaving(true); + try { + const body = { + primary_title: partnerTitle.trim(), + image_url: partnerImgUrl.trim() || undefined, + }; + if (editingPartnerId) { + const res = await updatePartnerContent(editingPartnerId, body); + if (res?.error) { + await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") }); + return; + } + } else { + const res = await savePartnerContent(body); + if (res?.error) { + await Swal.fire({ icon: "error", title: "Save failed", text: String(res.message ?? "") }); + return; + } + } + beginEditPartner(null); + setPartnerModalOpen(false); + await Swal.fire({ icon: "success", title: "Partner saved", timer: 1400, showConfirmButton: false }); + await loadAll(); + } finally { + setSaving(false); + } + } + + async function removePartner(id: string) { + const ok = await Swal.fire({ + icon: "warning", + title: "Delete this partner?", + showCancelButton: true, + }); + if (!ok.isConfirmed) return; + setSaving(true); + try { + await deletePartnerContent(id); + if (editingPartnerId === id) { + beginEditPartner(null); + setPartnerModalOpen(false); + } + await loadAll(); + } finally { + setSaving(false); + } + } + + function beginEditPopup(p: CmsPopupContent | null) { + if (!p) { + setPopupEditId(null); + setPopupPrimary(""); + setPopupSecondary(""); + setPopupDesc(""); + setPopupCta1(""); + setPopupCta2(""); + setPopupImgUrl(""); + return; + } + setPopupEditId(p.id); + setPopupPrimary(p.primary_title ?? ""); + setPopupSecondary(p.secondary_title ?? ""); + setPopupDesc(p.description ?? ""); + setPopupCta1(p.primary_cta ?? ""); + setPopupCta2(p.secondary_cta_text ?? ""); + setPopupImgUrl(p.images?.[0]?.media_url?.trim() ?? ""); + } + + function openPopupModalCreate() { + beginEditPopup(null); + setPopupModalOpen(true); + } + + function openPopupModalEdit(p: CmsPopupContent) { + beginEditPopup(p); + setPopupModalOpen(true); + } + + async function savePopupDraft() { + if (!popupPrimary.trim()) { + await Swal.fire({ icon: "warning", title: "Main title is required" }); + return; + } + setSaving(true); + try { + const body = { + primary_title: popupPrimary, + secondary_title: popupSecondary, + description: popupDesc, + primary_cta: popupCta1, + secondary_cta_text: popupCta2, + }; + let pid = popupEditId; + if (pid == null) { + const res = await savePopupNews(body); + if (res?.error) { + await Swal.fire({ icon: "error", title: "Save failed", text: String(res.message ?? "") }); + return; + } + const listRes = await getPopupNewsList(1, 50); + const rows = apiRows(listRes) as CmsPopupContent[]; + pid = + rows.length > 0 ? Math.max(...rows.map((r) => r.id)) : null; + } else { + const res = await updatePopupNews(pid, { id: pid, ...body }); + if (res?.error) { + await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") }); + return; + } + } + if (popupImgUrl.trim() && pid != null) { + await savePopupNewsImage({ + popup_news_content_id: pid, + media_url: popupImgUrl.trim(), + }); + } + await Swal.fire({ icon: "success", title: "Pop up saved", timer: 1600, showConfirmButton: false }); + await loadAll(); + beginEditPopup(null); + setPopupModalOpen(false); + } finally { + setSaving(false); + } + } + + async function removePopup(id: number) { + const ok = await Swal.fire({ + icon: "warning", + title: "Delete this pop up?", + showCancelButton: true, + }); + if (!ok.isConfirmed) return; + setSaving(true); + try { + await deletePopupNews(id); + if (popupEditId === id) { + beginEditPopup(null); + setPopupModalOpen(false); + } + await loadAll(); + } finally { + setSaving(false); + } + } + + if (loading) { + return ( +
+ +
+ ); + } return (
- {/* ================= HEADER ================= */} -
+
-

- Content Website -

-

- Update homepage content, products, services, and partners +

Content Website

+

+ Update homepage content, products, services, and partners.

- -
- - -
- {/* ================= TABS ================= */} - + Hero Section @@ -59,72 +731,790 @@ export default function ContentWebsite() { Pop Up + + + + +
+
+ + setHeroPrimary(e.target.value)} + placeholder="Headline" + /> +
+
+ + setHeroSecondary(e.target.value)} + /> +
+
+
+ +