Compare commits
No commits in common. "2eaa34d052614fb8e89aac1fc89bf51b08b2afa0" and "1489408b40b311625147b4ce141c8363f7076ac4" have entirely different histories.
2eaa34d052
...
1489408b40
3
.env
3
.env
|
|
@ -1,2 +1 @@
|
|||
MEDOLS_CLIENT_KEY=bb65b1ad-e954-4a1a-b4d0-74df5bb0b640
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8800
|
||||
MEDOLS_CLIENT_KEY=bb65b1ad-e954-4a1a-b4d0-74df5bb0b640
|
||||
49
app/page.tsx
49
app/page.tsx
|
|
@ -5,50 +5,19 @@ 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<CmsHeroContent | null>("/hero-contents"),
|
||||
publicFetch<CmsAboutContent[]>("/about-us-contents"),
|
||||
publicFetch<CmsProductContent[] | CmsProductContent>("/our-product-contents"),
|
||||
publicFetch<CmsServiceContent[] | CmsServiceContent>("/our-service-contents"),
|
||||
publicFetch<CmsPartnerContent[]>("/partner-contents"),
|
||||
publicFetch<CmsPopupContent[]>("/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 (
|
||||
<div className="relative min-h-screen bg-white font-[family-name:var(--font-geist-sans)]">
|
||||
{/* FIXED MENU */}
|
||||
<FloatingMenu />
|
||||
<PopupNewsBanner popups={popups} />
|
||||
<Header hero={hero} />
|
||||
<AboutSection about={about} />
|
||||
<ProductSection products={products} />
|
||||
<ServiceSection services={services} />
|
||||
<Technology partners={partners ?? []} />
|
||||
|
||||
{/* PAGE CONTENT */}
|
||||
<Header />
|
||||
<AboutSection />
|
||||
<ProductSection />
|
||||
<ServiceSection />
|
||||
<Technology />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,28 +2,15 @@
|
|||
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import type { CmsAboutContent } from "@/types/cms-landing";
|
||||
|
||||
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);
|
||||
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 messages = [
|
||||
{ id: 1, text: "Dimana posisi Ayah saya sekarang?", type: "user" },
|
||||
|
|
@ -43,65 +30,58 @@ export default function AboutSection({
|
|||
return (
|
||||
<section className="relative bg-[#f7f0e3] py-24">
|
||||
<div className="container mx-auto grid grid-cols-1 items-center gap-16 px-6 md:grid-cols-2">
|
||||
{/* PHONE WRAPPER */}
|
||||
<div className="flex justify-center">
|
||||
<div className="relative h-[640px] w-[320px]">
|
||||
<div className="relative w-[320px] h-[640px]">
|
||||
{/* PHONE IMAGE */}
|
||||
<Image
|
||||
src="/image/phone.png"
|
||||
alt="App Preview"
|
||||
fill
|
||||
className="pointer-events-none z-10 object-contain"
|
||||
className="object-contain z-10 pointer-events-none"
|
||||
/>
|
||||
|
||||
<div className="absolute bottom-[120px] left-[25px] right-[25px] top-[120px] z-0 overflow-hidden rounded-[2rem] bg-black/5">
|
||||
{mediaUrl && isVideo ? (
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
<video
|
||||
src={mediaUrl}
|
||||
className="h-full w-full object-cover"
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
/>
|
||||
) : mediaUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={mediaUrl} alt="" className="h-full w-full object-cover" />
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 p-2">
|
||||
{messages.map((msg, index) => (
|
||||
<motion.div
|
||||
key={msg.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 1.2 }}
|
||||
className={`max-w-[80%] rounded-2xl px-4 py-3 text-sm shadow ${
|
||||
msg.type === "user"
|
||||
? "self-end rounded-br-sm bg-blue-600 text-white"
|
||||
: "self-start rounded-bl-sm bg-gray-200 text-gray-800"
|
||||
}`}
|
||||
>
|
||||
{msg.text}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* CHAT AREA */}
|
||||
<div className="absolute top-[120px] left-[25px] right-[25px] bottom-[120px] overflow-hidden z-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
{messages.map((msg, index) => (
|
||||
<motion.div
|
||||
key={msg.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 1.2 }}
|
||||
className={`max-w-[80%] px-4 py-3 rounded-2xl text-sm shadow ${
|
||||
msg.type === "user"
|
||||
? "bg-blue-600 text-white self-end rounded-br-sm"
|
||||
: "bg-gray-200 text-gray-800 self-start rounded-bl-sm"
|
||||
}`}
|
||||
>
|
||||
{msg.text}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TEXT CONTENT */}
|
||||
<div className="max-w-xl">
|
||||
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-gray-400">
|
||||
{kicker}
|
||||
About Us
|
||||
</p>
|
||||
|
||||
<h2 className="mb-6 text-4xl font-extrabold leading-tight whitespace-pre-line">
|
||||
<h2 className="mb-6 text-4xl font-extrabold leading-tight">
|
||||
Helping you find the right{" "}
|
||||
<span className="relative inline-block">
|
||||
<span className="absolute bottom-1 left-0 h-3 w-full bg-[#966314]/40"></span>
|
||||
<span className="relative">{headline}</span>
|
||||
<span className="relative">Solution</span>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<p className="text-sm leading-relaxed text-gray-600 whitespace-pre-line">{desc}</p>
|
||||
<p className="text-sm leading-relaxed text-gray-600">
|
||||
PT Qudo Buana Nawakara adalah perusahaan nasional Indonesia yang
|
||||
berfokus pada pengembangan aplikasi...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -8,25 +8,11 @@ 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";
|
||||
|
||||
function isExternalUrl(url: string) {
|
||||
return /^https?:\/\//i.test(url);
|
||||
}
|
||||
|
||||
export default function Header({ hero }: { hero?: CmsHeroContent | null }) {
|
||||
export default function Header() {
|
||||
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 (
|
||||
<>
|
||||
<header className="relative w-full bg-white overflow-hidden">
|
||||
|
|
@ -61,58 +47,32 @@ export default function Header({ hero }: { hero?: CmsHeroContent | null }) {
|
|||
{/* HERO */}
|
||||
<div className="container mx-auto flex min-h-[90vh] items-center px-6">
|
||||
<div className="flex-1 space-y-6">
|
||||
<h1 className="text-4xl font-extrabold leading-tight whitespace-pre-line md:text-6xl">
|
||||
{title}
|
||||
<h1 className="text-4xl font-extrabold leading-tight md:text-6xl">
|
||||
<span className="relative inline-block">
|
||||
<span className="absolute bottom-1 left-0 h-3 w-full bg-[#966314]"></span>
|
||||
<span className="relative">Beyond Expectations</span>
|
||||
</span>
|
||||
<br />
|
||||
Build <span className="text-[#966314]">Reputation.</span>
|
||||
</h1>
|
||||
{subtitle ? (
|
||||
<p className="text-lg text-gray-600 md:text-xl">{subtitle}</p>
|
||||
) : null}
|
||||
{lead ? (
|
||||
<p className="max-w-xl text-sm leading-relaxed text-gray-600 md:text-base whitespace-pre-line">
|
||||
{lead}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => setContactOpen(true)}
|
||||
className="rounded-full bg-[#966314] px-8 py-6 text-base hover:bg-[#7c520f]"
|
||||
>
|
||||
{primaryCta}
|
||||
</Button>
|
||||
{secondaryCta ? (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="rounded-full border-[#966314] px-8 py-6 text-base text-[#966314]"
|
||||
type="button"
|
||||
>
|
||||
{secondaryCta}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => setContactOpen(true)}
|
||||
className="rounded-full bg-[#966314] px-8 py-6 text-base hover:bg-[#7c520f]"
|
||||
>
|
||||
Contact Us
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative hidden max-h-[min(90vh,520px)] flex-1 justify-end md:flex">
|
||||
{heroImg && isExternalUrl(heroImg) ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={heroImg}
|
||||
alt=""
|
||||
width={520}
|
||||
height={520}
|
||||
className="max-h-[520px] w-auto object-contain"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={heroImg || "/image/img1.png"}
|
||||
alt="Illustration"
|
||||
width={520}
|
||||
height={520}
|
||||
className="object-contain"
|
||||
/>
|
||||
)}
|
||||
<div className="relative hidden flex-1 justify-end md:flex">
|
||||
<Image
|
||||
src="/image/img1.png"
|
||||
alt="Illustration"
|
||||
width={520}
|
||||
height={520}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
"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 (
|
||||
<div className="fixed bottom-6 left-4 right-4 z-[120] mx-auto max-w-lg md:left-auto md:right-6 md:mx-0">
|
||||
<div className="relative overflow-hidden rounded-2xl border border-amber-200 bg-white shadow-2xl">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="absolute right-3 top-3 z-10 rounded-full bg-black/5 p-1 text-gray-600 hover:bg-black/10"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
{list.length > 1 ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIdx((i) => (i - 1 + list.length) % list.length)}
|
||||
className="absolute left-2 top-1/2 z-10 -translate-y-1/2 rounded-full bg-black/5 p-1.5 text-gray-700 hover:bg-black/10"
|
||||
aria-label="Previous"
|
||||
>
|
||||
<ChevronLeft size={18} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIdx((i) => (i + 1) % list.length)}
|
||||
className="absolute right-2 top-1/2 z-10 -translate-y-1/2 rounded-full bg-black/5 p-1.5 text-gray-700 hover:bg-black/10"
|
||||
aria-label="Next"
|
||||
>
|
||||
<ChevronRight size={18} />
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
{img ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={img} alt="" className="h-32 w-full object-cover md:h-40" />
|
||||
) : null}
|
||||
<div className="p-4 pr-10">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-amber-800">
|
||||
{popup.secondary_title || "News"}
|
||||
</p>
|
||||
<h3 className="mt-1 text-lg font-bold text-gray-900">{popup.primary_title}</h3>
|
||||
{popup.description ? (
|
||||
<p className="mt-2 line-clamp-3 text-sm text-gray-600">{popup.description}</p>
|
||||
) : null}
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{popup.primary_cta ? (
|
||||
<span className="inline-flex rounded-full bg-[#966314] px-4 py-2 text-xs font-semibold text-white">
|
||||
{popup.primary_cta}
|
||||
</span>
|
||||
) : null}
|
||||
{popup.secondary_cta_text ? (
|
||||
<span className="inline-flex rounded-full border border-[#966314] px-4 py-2 text-xs font-semibold text-[#966314]">
|
||||
{popup.secondary_cta_text}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{list.length > 1 ? (
|
||||
<div className="mt-3 flex justify-center gap-1.5">
|
||||
{list.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => setIdx(i)}
|
||||
className={`h-1.5 w-1.5 rounded-full ${
|
||||
i === idx % list.length ? "bg-[#966314]" : "bg-gray-300"
|
||||
}`}
|
||||
aria-label={`Go to item ${i + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,170 +1,187 @@
|
|||
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 (
|
||||
<section className="bg-white py-32">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="mb-20 text-center">
|
||||
<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="grid grid-cols-1 items-center gap-16 md:grid-cols-2">
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/image/p1.png"
|
||||
alt="Product illustration"
|
||||
width={520}
|
||||
height={420}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-xl">
|
||||
<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">
|
||||
MediaHUB Content Aggregator
|
||||
</h3>
|
||||
<p className="mb-8 text-sm leading-relaxed text-gray-600 whitespace-pre-line">
|
||||
{DEFAULT_BODY}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm font-semibold text-[#966314] hover:underline"
|
||||
>
|
||||
Learn More →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className="bg-white py-32">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="mb-16 text-center">
|
||||
{/* TITLE */}
|
||||
<div className="mb-20 text-center">
|
||||
<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">
|
||||
{list.length > 1 ? "Our Products" : sectionTitle}
|
||||
|
||||
<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>
|
||||
{subtitle && list.length > 1 ? (
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-gray-600">{subtitle}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-10 md:grid-cols-2 lg:grid-cols-3">
|
||||
{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 ?? "";
|
||||
{/* CONTENT */}
|
||||
<div className="grid grid-cols-1 items-center gap-16 md:grid-cols-2">
|
||||
{/* LEFT IMAGE */}
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/image/p1.png"
|
||||
alt="Product Illustration"
|
||||
width={520}
|
||||
height={420}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex flex-col overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm"
|
||||
>
|
||||
<div className="relative flex aspect-[4/3] items-center justify-center bg-gray-50">
|
||||
{external ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={imgSrc}
|
||||
alt=""
|
||||
width={480}
|
||||
height={360}
|
||||
className="object-contain"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col p-6">
|
||||
<div className="mb-4 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-3 text-xl font-bold text-gray-900 whitespace-pre-line">
|
||||
{p.primary_title?.trim() || "Product"}
|
||||
</h3>
|
||||
{p.secondary_title?.trim() ? (
|
||||
<p className="mb-3 text-sm font-medium text-gray-500">
|
||||
{p.secondary_title.trim()}
|
||||
</p>
|
||||
) : null}
|
||||
{useBullets ? (
|
||||
<ul className="mb-4 space-y-3">
|
||||
{lines.map((item) => (
|
||||
<li key={item} className="flex gap-3 text-sm text-gray-600">
|
||||
<span className="mt-1 flex h-8 w-16 shrink-0 items-center justify-center rounded-full bg-[#fdecc8]">
|
||||
<Check size={12} className="text-[#966314]" />
|
||||
</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mb-4 flex-1 text-sm leading-relaxed text-gray-600 whitespace-pre-line line-clamp-6">
|
||||
{bodyText}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="mt-auto text-left text-sm font-semibold text-[#966314] hover:underline"
|
||||
>
|
||||
Learn More →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* RIGHT CONTENT */}
|
||||
<div className="max-w-xl">
|
||||
{/* ICON */}
|
||||
<div className="mb-6 flex h-12 w-12 items-center justify-center rounded-xl bg-[#fdecc8]">
|
||||
<Image
|
||||
src="/image/product-icon.png"
|
||||
alt="Product Icon"
|
||||
width={22}
|
||||
height={22}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="mb-4 text-2xl font-bold text-gray-900">
|
||||
MediaHUB Content Aggregator
|
||||
</h3>
|
||||
|
||||
<p className="mb-8 text-sm leading-relaxed text-gray-600">
|
||||
Social media marketing services are provided by companies or
|
||||
individuals who specialize in marketing strategies through social
|
||||
media platforms.
|
||||
</p>
|
||||
|
||||
{/* FEATURES */}
|
||||
<ul className="mb-6 space-y-4">
|
||||
{features.map((item) => (
|
||||
<li key={item} className="flex gap-3 text-sm text-gray-600">
|
||||
<span className="mt-1 flex h-8 w-16 items-center justify-center rounded-full bg-[#fdecc8]">
|
||||
<Check size={12} className="text-[#966314]" />
|
||||
</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* CTA */}
|
||||
<button className="text-sm font-semibold text-[#966314] hover:underline">
|
||||
Learn More →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 items-center gap-16 md:grid-cols-2 mt-10">
|
||||
{/* LEFT IMAGE */}
|
||||
|
||||
{/* RIGHT CONTENT */}
|
||||
<div className="max-w-xl ml-10">
|
||||
{/* ICON */}
|
||||
<div className="mb-6 flex h-12 w-12 items-center justify-center rounded-xl bg-[#fdecc8]">
|
||||
<Image
|
||||
src="/image/product-icon.png"
|
||||
alt="Product Icon"
|
||||
width={22}
|
||||
height={22}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="mb-4 text-2xl font-bold text-gray-900">
|
||||
Multipool Reputation Management
|
||||
</h3>
|
||||
|
||||
<p className="mb-8 text-sm leading-relaxed text-gray-600">
|
||||
Social media marketing services are provided by companies or
|
||||
individuals who specialize in marketing strategies through social
|
||||
media platforms.
|
||||
</p>
|
||||
|
||||
{/* FEATURES */}
|
||||
<ul className="mb-6 space-y-4">
|
||||
{features.map((item) => (
|
||||
<li key={item} className="flex gap-3 text-sm text-gray-600">
|
||||
<span className="mt-1 flex h-8 w-16 items-center justify-center rounded-full bg-[#fdecc8]">
|
||||
<Check size={12} className="text-[#966314]" />
|
||||
</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* CTA */}
|
||||
<button className="text-sm font-semibold text-[#966314] hover:underline">
|
||||
Learn More →
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/image/p2.png"
|
||||
alt="Product Illustration"
|
||||
width={520}
|
||||
height={420}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 items-center gap-16 md:grid-cols-2 mt-10">
|
||||
{/* LEFT IMAGE */}
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/image/p3.png"
|
||||
alt="Product Illustration"
|
||||
width={520}
|
||||
height={420}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* RIGHT CONTENT */}
|
||||
<div className="max-w-xl">
|
||||
{/* ICON */}
|
||||
<div className="mb-6 flex h-12 w-12 items-center justify-center rounded-xl bg-[#fdecc8]">
|
||||
<Image
|
||||
src="/image/product-icon.png"
|
||||
alt="Product Icon"
|
||||
width={22}
|
||||
height={22}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="mb-4 text-2xl font-bold text-gray-900">
|
||||
PR Room Opinion Management
|
||||
</h3>
|
||||
|
||||
<p className="mb-8 text-sm leading-relaxed text-gray-600">
|
||||
Social media marketing services are provided by companies or
|
||||
individuals who specialize in marketing strategies through social
|
||||
media platforms.
|
||||
</p>
|
||||
|
||||
{/* FEATURES */}
|
||||
<ul className="mb-6 space-y-4">
|
||||
{features.map((item) => (
|
||||
<li key={item} className="flex gap-3 text-sm text-gray-600">
|
||||
<span className="mt-1 flex h-8 w-16 items-center justify-center rounded-full bg-[#fdecc8]">
|
||||
<Check size={12} className="text-[#966314]" />
|
||||
</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* CTA */}
|
||||
<button className="text-sm font-semibold text-[#966314] hover:underline">
|
||||
Learn More →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,111 +1,187 @@
|
|||
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 (
|
||||
<section className="bg-white py-20">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="mb-16 text-center">
|
||||
<p className="text-sm uppercase tracking-widest text-gray-400">Our Services</p>
|
||||
<h2 className="mt-2 text-3xl font-bold text-gray-900 md:text-4xl whitespace-pre-line">
|
||||
{DEFAULT_HEADING}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid items-center gap-12 md:grid-cols-2">
|
||||
<div className="overflow-hidden rounded-2xl shadow-lg">
|
||||
<Image
|
||||
src={imgSrc}
|
||||
alt="Service"
|
||||
width={600}
|
||||
height={400}
|
||||
className="h-auto w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="leading-relaxed text-gray-600 whitespace-pre-line">{DEFAULT_BODY}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ServiceSection() {
|
||||
return (
|
||||
<section className="bg-white py-20">
|
||||
<section className="py-20 bg-white">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="mb-16 text-center">
|
||||
<p className="text-sm uppercase tracking-widest text-gray-400">Our Services</p>
|
||||
<h2 className="mt-2 text-3xl font-bold text-gray-900 md:text-4xl whitespace-pre-line">
|
||||
{list.length > 1 ? "What we deliver" : list[0]?.primary_title?.trim() || DEFAULT_HEADING}
|
||||
{/* Heading */}
|
||||
<div className="text-center mb-16">
|
||||
<p className="text-sm uppercase tracking-widest text-gray-400">
|
||||
Our Services
|
||||
</p>
|
||||
<h2 className="mt-2 text-3xl md:text-4xl font-bold text-gray-900">
|
||||
Innovative solutions for your{" "}
|
||||
<span className="relative inline-block">
|
||||
business growth
|
||||
<span className="absolute left-0 -bottom-1 w-full h-2 bg-yellow-300/60 -z-10" />
|
||||
</span>
|
||||
.
|
||||
</h2>
|
||||
{list.length > 1 && list[0]?.secondary_title?.trim() ? (
|
||||
<p className="mx-auto mt-4 max-w-2xl text-gray-600">
|
||||
{list[0].secondary_title.trim()}
|
||||
</p>
|
||||
) : list.length === 1 && list[0]?.secondary_title?.trim() ? (
|
||||
<p className="mx-auto mt-4 max-w-2xl text-gray-600">
|
||||
{list[0].secondary_title.trim()}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{list.map((s) => {
|
||||
const imgSrc = bannerUrl(s);
|
||||
const external = /^https?:\/\//i.test(imgSrc);
|
||||
const body = s.description?.trim() || DEFAULT_BODY;
|
||||
return (
|
||||
<div
|
||||
key={s.id}
|
||||
className="flex flex-col overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-md"
|
||||
>
|
||||
<div className="relative aspect-video w-full overflow-hidden bg-gray-100">
|
||||
{external ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt=""
|
||||
width={600}
|
||||
height={400}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={imgSrc}
|
||||
alt=""
|
||||
width={600}
|
||||
height={400}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col p-5">
|
||||
<h3 className="text-lg font-bold text-gray-900 whitespace-pre-line">
|
||||
{s.primary_title?.trim() || "Service"}
|
||||
</h3>
|
||||
<p className="mt-2 line-clamp-4 text-sm leading-relaxed text-gray-600 whitespace-pre-line">
|
||||
{body}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Service 1 */}
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center mb-20">
|
||||
{/* Image */}
|
||||
<div className="rounded-2xl overflow-hidden shadow-lg">
|
||||
<Image
|
||||
src="/image/s1.png"
|
||||
alt="Artifintel Soundworks"
|
||||
width={600}
|
||||
height={400}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||
Artifintel
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
<li>✔ AI Music Composition & Songwriting</li>
|
||||
<li>✔ Vocal Synthesis & AI Musicians</li>
|
||||
<li>✔ Genre Exploration & Sound Innovation</li>
|
||||
<li>✔ AI Collaboration & Creative Experimentation</li>
|
||||
<li>✔ Music Release & Digital Distribution</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service 2 */}
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center mb-20">
|
||||
{/* Content */}
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||
Produksi Video Animasi
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
<li>✔ 2D & 3D Animation Production</li>
|
||||
<li>✔ Motion Graphics & Visual Effects</li>
|
||||
<li>✔ Character Design & Storyboarding</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div className="rounded-2xl overflow-hidden shadow-lg">
|
||||
<Image
|
||||
src="/image/s2.png"
|
||||
alt="Animasee"
|
||||
width={600}
|
||||
height={400}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center mb-20 mt-10">
|
||||
{/* Image */}
|
||||
<div className="rounded-2xl overflow-hidden shadow-lg">
|
||||
<Image
|
||||
src="/image/s3.png"
|
||||
alt="Artifintel Soundworks"
|
||||
width={600}
|
||||
height={400}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||
Reelithic
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
<li>✔ AI Music Composition & Songwriting</li>
|
||||
<li>✔ Vocal Synthesis & AI Musicians</li>
|
||||
<li>✔ Genre Exploration & Sound Innovation</li>
|
||||
<li>✔ AI Collaboration & Creative Experimentation</li>
|
||||
<li>✔ Music Release & Digital Distribution</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service 3 */}
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center mb-20">
|
||||
{/* Content */}
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||
Qudoin
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
<li>✔ 2D & 3D Animation Production</li>
|
||||
<li>✔ Motion Graphics & Visual Effects</li>
|
||||
<li>✔ Character Design & Storyboarding</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div className="rounded-2xl overflow-hidden shadow-lg">
|
||||
<Image
|
||||
src="/image/s4.png"
|
||||
alt="Animasee"
|
||||
width={600}
|
||||
height={400}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center mb-20 mt-10">
|
||||
{/* Image */}
|
||||
<div className="rounded-2xl overflow-hidden shadow-lg">
|
||||
<Image
|
||||
src="/image/s5.png"
|
||||
alt="Artifintel Soundworks"
|
||||
width={600}
|
||||
height={400}
|
||||
className="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||
Talkshow AI
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<ul className="space-y-3 text-gray-700">
|
||||
<li>✔ AI Music Composition & Songwriting</li>
|
||||
<li>✔ Vocal Synthesis & AI Musicians</li>
|
||||
<li>✔ Genre Exploration & Sound Innovation</li>
|
||||
<li>✔ AI Collaboration & Creative Experimentation</li>
|
||||
<li>✔ Music Release & Digital Distribution</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { CmsPartnerContent } from "@/types/cms-landing";
|
||||
import Image from "next/image";
|
||||
|
||||
const FALLBACK: { name: string; src: string }[] = [
|
||||
const technologies = [
|
||||
{ name: "Tableau", src: "/image/tableu.png" },
|
||||
{ name: "TVU Networks", src: "/image/tvu.png" },
|
||||
{ name: "AWS", src: "/image/aws.png" },
|
||||
|
|
@ -9,56 +9,31 @@ const FALLBACK: { name: string; src: string }[] = [
|
|||
{ name: "Ui", src: "/image/uipath.png" },
|
||||
];
|
||||
|
||||
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];
|
||||
|
||||
export default function Technology() {
|
||||
return (
|
||||
<section className="relative overflow-hidden bg-gradient-to-r from-[#faf6ee] to-[#f4efe6] py-14">
|
||||
<div className="container mx-auto px-6">
|
||||
<p className="mb-8 text-center text-lg font-semibold tracking-widest text-gray-500">
|
||||
{/* Title */}
|
||||
<p className="text-center text-lg font-semibold tracking-widest text-gray-500 mb-8">
|
||||
TECHNOLOGY PARTNERS
|
||||
</p>
|
||||
|
||||
{/* Slider */}
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<div className="flex w-max animate-tech-scroll gap-14">
|
||||
{loop.map((tech, index) => (
|
||||
{/* duplicated for seamless loop */}
|
||||
{[...technologies, ...technologies].map((tech, index) => (
|
||||
<div
|
||||
key={`${tech.name}-${index}`}
|
||||
className="flex min-w-[140px] items-center justify-center opacity-80 transition hover:opacity-100"
|
||||
key={index}
|
||||
className="flex items-center justify-center min-w-[140px] opacity-80 hover:opacity-100 transition"
|
||||
>
|
||||
{tech.src && /^https?:\/\//i.test(tech.src) ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={tech.src}
|
||||
alt={tech.name}
|
||||
width={120}
|
||||
height={50}
|
||||
className="max-h-[50px] object-contain"
|
||||
/>
|
||||
) : tech.src ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={tech.src}
|
||||
alt={tech.name}
|
||||
width={120}
|
||||
height={50}
|
||||
className="object-contain"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm text-gray-500">{tech.name}</span>
|
||||
)}
|
||||
<Image
|
||||
src={tech.src}
|
||||
alt={tech.name}
|
||||
width={120}
|
||||
height={50}
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,27 +0,0 @@
|
|||
const DEFAULT_CLIENT_KEY = "9ca7f706-a8b0-4520-b467-5e8321df36fb";
|
||||
|
||||
function clientKey() {
|
||||
return process.env.NEXT_PUBLIC_X_CLIENT_KEY ?? DEFAULT_CLIENT_KEY;
|
||||
}
|
||||
|
||||
function apiBase() {
|
||||
const base = process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "");
|
||||
return base ?? "";
|
||||
}
|
||||
|
||||
/** Server-side fetch for public landing data (requires `X-Client-Key`). */
|
||||
export async function publicFetch<T>(path: string): Promise<T | null> {
|
||||
const base = apiBase();
|
||||
if (!base) return null;
|
||||
const url = `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": clientKey(),
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const json = (await res.json()) as { data?: T };
|
||||
return json.data ?? null;
|
||||
}
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
import {
|
||||
httpDeleteInterceptor,
|
||||
httpGetInterceptor,
|
||||
httpPostInterceptor,
|
||||
httpPutInterceptor,
|
||||
} from "./http-config/http-interceptor-services";
|
||||
|
||||
/** Axios wrapper returns `{ error, data }` where `data` is full API body `{ success, messages, data }`. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function apiPayload<T>(res: any): T | null {
|
||||
if (!res || typeof res !== "object") return null;
|
||||
const r = res as { error?: boolean; data?: { data?: T } | null };
|
||||
if (r.error || r.data == null) return null;
|
||||
return r.data.data ?? null;
|
||||
}
|
||||
|
||||
export function apiRows<T>(res: any): T[] {
|
||||
const rows = apiPayload<T[]>(res);
|
||||
return Array.isArray(rows) ? rows : [];
|
||||
}
|
||||
|
||||
/** Normalizes list endpoints that return either a single object or an array. */
|
||||
export function apiDataList<T>(res: unknown): T[] {
|
||||
const raw = apiPayload<T[] | T>(res);
|
||||
if (Array.isArray(raw)) return raw;
|
||||
if (raw != null && typeof raw === "object") return [raw as T];
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function getHeroContent() {
|
||||
return await httpGetInterceptor("/hero-contents");
|
||||
}
|
||||
|
||||
export async function saveHeroContent(body: {
|
||||
primary_title: string;
|
||||
secondary_title?: string;
|
||||
description?: string;
|
||||
primary_cta?: string;
|
||||
secondary_cta_text?: string;
|
||||
}) {
|
||||
return await httpPostInterceptor("/hero-contents", body);
|
||||
}
|
||||
|
||||
export async function updateHeroContent(
|
||||
id: string,
|
||||
body: {
|
||||
primary_title: string;
|
||||
secondary_title?: string;
|
||||
description?: string;
|
||||
primary_cta?: string;
|
||||
secondary_cta_text?: string;
|
||||
},
|
||||
) {
|
||||
return await httpPutInterceptor(`/hero-contents/${id}`, body);
|
||||
}
|
||||
|
||||
export async function getHeroImages(heroId: string) {
|
||||
return await httpGetInterceptor(`/hero-content-images/${heroId}`);
|
||||
}
|
||||
|
||||
export async function saveHeroImage(body: {
|
||||
hero_content_id: string;
|
||||
image_path?: string;
|
||||
image_url?: string;
|
||||
}) {
|
||||
return await httpPostInterceptor("/hero-content-images", body);
|
||||
}
|
||||
|
||||
export async function updateHeroImage(
|
||||
id: string,
|
||||
body: { image_path?: string; image_url?: string },
|
||||
) {
|
||||
return await httpPutInterceptor(`/hero-content-images/${id}`, body);
|
||||
}
|
||||
|
||||
export async function getAboutContentsList() {
|
||||
return await httpGetInterceptor("/about-us-contents");
|
||||
}
|
||||
|
||||
export async function saveAboutContent(body: Record<string, string>) {
|
||||
return await httpPostInterceptor("/about-us-contents", body);
|
||||
}
|
||||
|
||||
export async function updateAboutContent(
|
||||
id: number,
|
||||
body: Record<string, string>,
|
||||
) {
|
||||
return await httpPutInterceptor(`/about-us-contents/${id}`, body);
|
||||
}
|
||||
|
||||
export async function saveAboutUsMediaUrl(body: {
|
||||
about_us_content_id: number;
|
||||
media_url: string;
|
||||
media_type?: string;
|
||||
}) {
|
||||
return await httpPostInterceptor("/about-us-content-images/url", body);
|
||||
}
|
||||
|
||||
export async function deleteAboutUsContentImage(id: number) {
|
||||
return await httpDeleteInterceptor(`/about-us-content-images/${id}`);
|
||||
}
|
||||
|
||||
export async function getOurProductContent() {
|
||||
return await httpGetInterceptor("/our-product-contents");
|
||||
}
|
||||
|
||||
export async function saveOurProductContent(body: {
|
||||
primary_title: string;
|
||||
secondary_title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
return await httpPostInterceptor("/our-product-contents", body);
|
||||
}
|
||||
|
||||
export async function updateOurProductContent(
|
||||
id: string,
|
||||
body: {
|
||||
primary_title: string;
|
||||
secondary_title?: string;
|
||||
description?: string;
|
||||
},
|
||||
) {
|
||||
return await httpPutInterceptor(`/our-product-contents/${id}`, body);
|
||||
}
|
||||
|
||||
export async function deleteOurProductContent(id: string) {
|
||||
return await httpDeleteInterceptor(`/our-product-contents/${id}`);
|
||||
}
|
||||
|
||||
export async function getOurProductImages(productContentId: string) {
|
||||
return await httpGetInterceptor(
|
||||
`/our-product-content-images/${productContentId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveOurProductImage(body: {
|
||||
our_product_content_id: string;
|
||||
image_path?: string;
|
||||
image_url?: string;
|
||||
}) {
|
||||
return await httpPostInterceptor("/our-product-content-images", body);
|
||||
}
|
||||
|
||||
export async function updateOurProductImage(
|
||||
id: string,
|
||||
body: { image_path?: string; image_url?: string },
|
||||
) {
|
||||
return await httpPutInterceptor(`/our-product-content-images/${id}`, body);
|
||||
}
|
||||
|
||||
export async function getOurServiceContent() {
|
||||
return await httpGetInterceptor("/our-service-contents");
|
||||
}
|
||||
|
||||
export async function saveOurServiceContent(body: {
|
||||
primary_title: string;
|
||||
secondary_title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
return await httpPostInterceptor("/our-service-contents", body);
|
||||
}
|
||||
|
||||
export async function updateOurServiceContent(
|
||||
id: number,
|
||||
body: {
|
||||
primary_title: string;
|
||||
secondary_title?: string;
|
||||
description?: string;
|
||||
},
|
||||
) {
|
||||
return await httpPutInterceptor(`/our-service-contents/${id}`, body);
|
||||
}
|
||||
|
||||
export async function deleteOurServiceContent(id: number) {
|
||||
return await httpDeleteInterceptor(`/our-service-contents/${id}`);
|
||||
}
|
||||
|
||||
export async function getOurServiceImages(serviceContentId: number) {
|
||||
return await httpGetInterceptor(
|
||||
`/our-service-content-images/${serviceContentId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveOurServiceImage(body: {
|
||||
our_service_content_id: number;
|
||||
image_path?: string;
|
||||
image_url?: string;
|
||||
}) {
|
||||
return await httpPostInterceptor("/our-service-content-images", body);
|
||||
}
|
||||
|
||||
export async function updateOurServiceImage(
|
||||
id: string,
|
||||
body: { image_path?: string; image_url?: string },
|
||||
) {
|
||||
return await httpPutInterceptor(`/our-service-content-images/${id}`, body);
|
||||
}
|
||||
|
||||
export async function getPartnerContents() {
|
||||
return await httpGetInterceptor("/partner-contents");
|
||||
}
|
||||
|
||||
export async function savePartnerContent(body: {
|
||||
primary_title: string;
|
||||
image_path?: string;
|
||||
image_url?: string;
|
||||
}) {
|
||||
return await httpPostInterceptor("/partner-contents", body);
|
||||
}
|
||||
|
||||
export async function updatePartnerContent(
|
||||
id: string,
|
||||
body: {
|
||||
primary_title: string;
|
||||
image_path?: string;
|
||||
image_url?: string;
|
||||
},
|
||||
) {
|
||||
return await httpPutInterceptor(`/partner-contents/${id}`, body);
|
||||
}
|
||||
|
||||
export async function deletePartnerContent(id: string) {
|
||||
return await httpDeleteInterceptor(`/partner-contents/${id}`);
|
||||
}
|
||||
|
||||
export async function getPopupNewsList(page = 1, limit = 10) {
|
||||
return await httpGetInterceptor(
|
||||
`/popup-news-contents?page=${page}&limit=${limit}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPopupNewsById(id: number) {
|
||||
return await httpGetInterceptor(`/popup-news-contents/${id}`);
|
||||
}
|
||||
|
||||
export async function savePopupNews(body: {
|
||||
primary_title: string;
|
||||
secondary_title?: string;
|
||||
description?: string;
|
||||
primary_cta?: string;
|
||||
secondary_cta_text?: string;
|
||||
}) {
|
||||
return await httpPostInterceptor("/popup-news-contents", body);
|
||||
}
|
||||
|
||||
export async function updatePopupNews(
|
||||
id: number,
|
||||
body: {
|
||||
id: number;
|
||||
primary_title: string;
|
||||
secondary_title?: string;
|
||||
description?: string;
|
||||
primary_cta?: string;
|
||||
secondary_cta_text?: string;
|
||||
},
|
||||
) {
|
||||
return await httpPutInterceptor(`/popup-news-contents/${id}`, body);
|
||||
}
|
||||
|
||||
export async function savePopupNewsImage(body: {
|
||||
popup_news_content_id: number;
|
||||
media_path?: string;
|
||||
media_url?: string;
|
||||
}) {
|
||||
return await httpPostInterceptor("/popup-news-content-images", body);
|
||||
}
|
||||
|
||||
export async function deletePopupNews(id: number) {
|
||||
return await httpDeleteInterceptor(`/popup-news-contents/${id}`);
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
/** Qudo backend API base URL (set in `.env` as `NEXT_PUBLIC_API_URL`, e.g. `https://qudo.id/api` or `http://localhost:8800`). */
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "";
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import axios from "axios";
|
||||
import { API_BASE_URL } from "./api-base-url";
|
||||
|
||||
const baseURL = "https://qudo.id/api";
|
||||
|
||||
const axiosBaseInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
baseURL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": "9ca7f706-a8b0-4520-b467-5e8321df36fb",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import axios from "axios";
|
||||
import { postSignIn } from "../master-user";
|
||||
import Cookies from "js-cookie";
|
||||
import { API_BASE_URL } from "./api-base-url";
|
||||
|
||||
const baseURL = "https://qudo.id/api";
|
||||
|
||||
const refreshToken = Cookies.get("refresh_token");
|
||||
|
||||
const axiosInterceptorInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
baseURL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": "9ca7f706-a8b0-4520-b467-5e8321df36fb",
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
export type CmsHeroContent = {
|
||||
id: string;
|
||||
primary_title: string;
|
||||
secondary_title: string;
|
||||
description: string;
|
||||
primary_cta: string;
|
||||
secondary_cta_text: string;
|
||||
images?: CmsHeroImage[];
|
||||
};
|
||||
|
||||
export type CmsHeroImage = {
|
||||
id: string;
|
||||
hero_content_id: string;
|
||||
image_path: string;
|
||||
image_url: string;
|
||||
};
|
||||
|
||||
export type CmsAboutContent = {
|
||||
id: number;
|
||||
primary_title: string;
|
||||
secondary_title: string;
|
||||
description: string;
|
||||
primary_cta: string;
|
||||
secondary_cta_text: string;
|
||||
images?: { id: number; media_url: string; media_type?: string }[];
|
||||
};
|
||||
|
||||
export type CmsProductContent = {
|
||||
id: string;
|
||||
primary_title: string;
|
||||
secondary_title: string;
|
||||
description: string;
|
||||
images?: { id: string; image_url: string; image_path: string }[];
|
||||
};
|
||||
|
||||
export type CmsServiceContent = {
|
||||
id: number;
|
||||
primary_title: string;
|
||||
secondary_title: string;
|
||||
description: string;
|
||||
images?: { id: string; image_url: string; image_path: string }[];
|
||||
};
|
||||
|
||||
export type CmsPartnerContent = {
|
||||
id: string;
|
||||
primary_title: string;
|
||||
image_path: string;
|
||||
image_url: string;
|
||||
};
|
||||
|
||||
export type CmsPopupContent = {
|
||||
id: number;
|
||||
primary_title: string;
|
||||
secondary_title: string;
|
||||
description: string;
|
||||
primary_cta: string;
|
||||
secondary_cta_text: string;
|
||||
images?: { id: number; media_url: string; media_path: string }[];
|
||||
};
|
||||
Loading…
Reference in New Issue