Compare commits

..

No commits in common. "2eaa34d052614fb8e89aac1fc89bf51b08b2afa0" and "1489408b40b311625147b4ce141c8363f7076ac4" have entirely different histories.

15 changed files with 528 additions and 2393 deletions

3
.env
View File

@ -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

View File

@ -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>
);

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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;
}

View File

@ -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}`);
}

View File

@ -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 ?? "";

View File

@ -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",

View File

@ -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",

View File

@ -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 }[];
};