qudoco-fe/components/main/content-website.tsx

1521 lines
56 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Eye, Pencil, ImageIcon, Loader2, Plus, Trash2 } from "lucide-react";
import Swal from "sweetalert2";
import type {
CmsAboutContent,
CmsHeroContent,
CmsPartnerContent,
CmsPopupContent,
CmsProductContent,
CmsServiceContent,
} from "@/types/cms-landing";
import {
apiDataList,
apiPayload,
apiRows,
deleteAboutUsContentImage,
deleteOurProductContent,
deleteOurServiceContent,
deletePartnerContent,
deletePopupNews,
getAboutContentsList,
getHeroContent,
getOurProductContent,
getOurServiceContent,
getPartnerContents,
getPopupNewsList,
saveAboutContent,
saveAboutUsMediaUrl,
saveHeroContent,
saveHeroImage,
saveOurProductContent,
saveOurServiceContent,
savePartnerContent,
saveOurProductImage,
saveOurServiceImage,
savePopupNews,
savePopupNewsImage,
updateAboutContent,
updateHeroContent,
updateHeroImage,
updateOurProductContent,
updateOurProductImage,
updateOurServiceContent,
updateOurServiceImage,
updatePartnerContent,
updatePopupNews,
} from "@/service/cms-landing";
export default function ContentWebsite() {
const [activeTab, setActiveTab] = useState("hero");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [heroId, setHeroId] = useState<string | null>(null);
const [heroImageId, setHeroImageId] = useState<string | null>(null);
const [heroPrimary, setHeroPrimary] = useState("");
const [heroSecondary, setHeroSecondary] = useState("");
const [heroDesc, setHeroDesc] = useState("");
const [heroCta1, setHeroCta1] = useState("");
const [heroCta2, setHeroCta2] = useState("");
const [heroImgUrl, setHeroImgUrl] = useState("");
const [aboutId, setAboutId] = useState<number | null>(null);
const [aboutPrimary, setAboutPrimary] = useState("");
const [aboutSecondary, setAboutSecondary] = useState("");
const [aboutDesc, setAboutDesc] = useState("");
const [aboutCta1, setAboutCta1] = useState("");
const [aboutCta2, setAboutCta2] = useState("");
const [aboutMediaUrl, setAboutMediaUrl] = useState("");
const [aboutMediaImageId, setAboutMediaImageId] = useState<number | null>(
null,
);
const [aboutMediaLoadedUrl, setAboutMediaLoadedUrl] = useState("");
const [products, setProducts] = useState<CmsProductContent[]>([]);
const [productEditId, setProductEditId] = useState<string | null>(null);
const [productPrimary, setProductPrimary] = useState("");
const [productSecondary, setProductSecondary] = useState("");
const [productDesc, setProductDesc] = useState("");
const [productImgUrl, setProductImgUrl] = useState("");
const [productImageId, setProductImageId] = useState<string | null>(null);
const [productModalOpen, setProductModalOpen] = useState(false);
const [services, setServices] = useState<CmsServiceContent[]>([]);
const [serviceEditId, setServiceEditId] = useState<number | null>(null);
const [servicePrimary, setServicePrimary] = useState("");
const [serviceSecondary, setServiceSecondary] = useState("");
const [serviceDesc, setServiceDesc] = useState("");
const [serviceImgUrl, setServiceImgUrl] = useState("");
const [serviceImageId, setServiceImageId] = useState<string | null>(null);
const [serviceModalOpen, setServiceModalOpen] = useState(false);
const [partners, setPartners] = useState<CmsPartnerContent[]>([]);
const [partnerTitle, setPartnerTitle] = useState("");
const [partnerImgUrl, setPartnerImgUrl] = useState("");
const [editingPartnerId, setEditingPartnerId] = useState<string | null>(null);
const [partnerModalOpen, setPartnerModalOpen] = useState(false);
const [popups, setPopups] = useState<CmsPopupContent[]>([]);
const [popupEditId, setPopupEditId] = useState<number | null>(null);
const [popupPrimary, setPopupPrimary] = useState("");
const [popupSecondary, setPopupSecondary] = useState("");
const [popupDesc, setPopupDesc] = useState("");
const [popupCta1, setPopupCta1] = useState("");
const [popupCta2, setPopupCta2] = useState("");
const [popupImgUrl, setPopupImgUrl] = useState("");
const [popupModalOpen, setPopupModalOpen] = useState(false);
const loadAll = useCallback(async () => {
setLoading(true);
try {
const heroRes = await getHeroContent();
const hero = apiPayload(heroRes) as CmsHeroContent | null;
if (hero?.id) {
setHeroId(hero.id);
setHeroPrimary(hero.primary_title ?? "");
setHeroSecondary(hero.secondary_title ?? "");
setHeroDesc(hero.description ?? "");
setHeroCta1(hero.primary_cta ?? "");
setHeroCta2(hero.secondary_cta_text ?? "");
const first = hero.images?.[0];
if (first?.image_url) {
setHeroImgUrl(first.image_url);
setHeroImageId(first.id ?? null);
} else {
setHeroImgUrl("");
setHeroImageId(null);
}
} else {
setHeroId(null);
setHeroImageId(null);
setHeroPrimary("");
setHeroSecondary("");
setHeroDesc("");
setHeroCta1("");
setHeroCta2("");
setHeroImgUrl("");
}
const aboutRes = await getAboutContentsList();
const aboutList = apiRows(aboutRes) as CmsAboutContent[];
const ab = aboutList[0];
if (ab) {
setAboutId(ab.id);
setAboutPrimary(ab.primary_title ?? "");
setAboutSecondary(ab.secondary_title ?? "");
setAboutDesc(ab.description ?? "");
setAboutCta1(ab.primary_cta ?? "");
setAboutCta2(ab.secondary_cta_text ?? "");
const am = ab.images?.[0];
const murl = am?.media_url?.trim() ?? "";
setAboutMediaUrl(murl);
setAboutMediaLoadedUrl(murl);
setAboutMediaImageId(am?.id ?? null);
} else {
setAboutId(null);
setAboutPrimary("");
setAboutSecondary("");
setAboutDesc("");
setAboutCta1("");
setAboutCta2("");
setAboutMediaUrl("");
setAboutMediaLoadedUrl("");
setAboutMediaImageId(null);
}
const prRes = await getOurProductContent();
setProducts(apiDataList<CmsProductContent>(prRes));
const svRes = await getOurServiceContent();
setServices(apiDataList<CmsServiceContent>(svRes));
const parRes = await getPartnerContents();
const parList = apiPayload(parRes) as CmsPartnerContent[] | null;
setPartners(Array.isArray(parList) ? parList : []);
const popRes = await getPopupNewsList(1, 50);
setPopups(apiRows(popRes) as CmsPopupContent[]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadAll();
}, [loadAll]);
async function saveHeroTab() {
if (!heroPrimary.trim()) {
await Swal.fire({ icon: "warning", title: "Main title is required" });
return;
}
setSaving(true);
try {
const body = {
primary_title: heroPrimary,
secondary_title: heroSecondary,
description: heroDesc,
primary_cta: heroCta1,
secondary_cta_text: heroCta2,
};
let hid = heroId;
if (!hid) {
const res = await saveHeroContent(body);
const created = apiPayload(res) as CmsHeroContent | null;
if (!created?.id) {
await Swal.fire({ icon: "error", title: "Failed to create hero content" });
return;
}
hid = created.id;
setHeroId(hid);
} else {
const res = await updateHeroContent(hid, body);
if (res?.error) {
await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") });
return;
}
}
if (heroImgUrl.trim() && hid) {
if (heroImageId) {
await updateHeroImage(heroImageId, { image_url: heroImgUrl.trim() });
} else {
await saveHeroImage({
hero_content_id: hid,
image_url: heroImgUrl.trim(),
});
}
}
await Swal.fire({ icon: "success", title: "Hero section saved", timer: 1600, showConfirmButton: false });
await loadAll();
} finally {
setSaving(false);
}
}
async function saveAboutTab() {
if (!aboutPrimary.trim()) {
await Swal.fire({ icon: "warning", title: "Main title is required" });
return;
}
setSaving(true);
try {
const body: Record<string, string> = {
primary_title: aboutPrimary,
secondary_title: aboutSecondary,
description: aboutDesc,
primary_cta: aboutCta1,
secondary_cta_text: aboutCta2,
};
let aid = aboutId;
if (aboutId == null) {
const res = await saveAboutContent(body);
if (res?.error) {
await Swal.fire({ icon: "error", title: "Save failed", text: String(res.message ?? "") });
return;
}
const created = apiPayload(res) as CmsAboutContent | null;
aid = created?.id ?? null;
if (aid != null) setAboutId(aid);
} else {
const res = await updateAboutContent(aboutId, body);
if (res?.error) {
await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") });
return;
}
}
const url = aboutMediaUrl.trim();
if (aid != null) {
if (!url) {
if (aboutMediaImageId != null) {
await deleteAboutUsContentImage(aboutMediaImageId);
}
} else if (url !== aboutMediaLoadedUrl || aboutMediaImageId == null) {
if (aboutMediaImageId != null) {
await deleteAboutUsContentImage(aboutMediaImageId);
}
const mres = await saveAboutUsMediaUrl({
about_us_content_id: aid,
media_url: url,
});
if (mres?.error) {
await Swal.fire({
icon: "error",
title: "Media URL failed",
text: String(mres.message ?? ""),
});
return;
}
}
}
await Swal.fire({ icon: "success", title: "About Us saved", timer: 1600, showConfirmButton: false });
await loadAll();
} finally {
setSaving(false);
}
}
function beginEditProduct(p: CmsProductContent | null) {
if (!p) {
setProductEditId(null);
setProductPrimary("");
setProductSecondary("");
setProductDesc("");
setProductImgUrl("");
setProductImageId(null);
return;
}
setProductEditId(p.id);
setProductPrimary(p.primary_title ?? "");
setProductSecondary(p.secondary_title ?? "");
setProductDesc(p.description ?? "");
const im = p.images?.[0];
setProductImgUrl(im?.image_url?.trim() ?? "");
setProductImageId(im?.id ?? null);
}
function openProductModalCreate() {
beginEditProduct(null);
setProductModalOpen(true);
}
function openProductModalEdit(p: CmsProductContent) {
beginEditProduct(p);
setProductModalOpen(true);
}
async function saveProductDraft() {
if (!productPrimary.trim()) {
await Swal.fire({ icon: "warning", title: "Product title is required" });
return;
}
setSaving(true);
try {
const body = {
primary_title: productPrimary,
secondary_title: productSecondary,
description: productDesc,
};
let pid = productEditId;
if (!pid) {
const res = await saveOurProductContent(body);
const created = apiPayload(res) as CmsProductContent | null;
if (!created?.id) {
await Swal.fire({ icon: "error", title: "Failed to create product" });
return;
}
pid = created.id;
} else {
const res = await updateOurProductContent(pid, body);
if (res?.error) {
await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") });
return;
}
}
if (productImgUrl.trim() && pid) {
if (productImageId) {
await updateOurProductImage(productImageId, { image_url: productImgUrl.trim() });
} else {
await saveOurProductImage({
our_product_content_id: pid,
image_url: productImgUrl.trim(),
});
}
}
await Swal.fire({ icon: "success", title: "Product saved", timer: 1400, showConfirmButton: false });
await loadAll();
beginEditProduct(null);
setProductModalOpen(false);
} finally {
setSaving(false);
}
}
async function removeProduct(id: string) {
const ok = await Swal.fire({
icon: "warning",
title: "Delete this product?",
showCancelButton: true,
});
if (!ok.isConfirmed) return;
setSaving(true);
try {
await deleteOurProductContent(id);
if (productEditId === id) {
beginEditProduct(null);
setProductModalOpen(false);
}
await loadAll();
} finally {
setSaving(false);
}
}
function beginEditService(s: CmsServiceContent | null) {
if (!s) {
setServiceEditId(null);
setServicePrimary("");
setServiceSecondary("");
setServiceDesc("");
setServiceImgUrl("");
setServiceImageId(null);
return;
}
setServiceEditId(s.id);
setServicePrimary(s.primary_title ?? "");
setServiceSecondary(s.secondary_title ?? "");
setServiceDesc(s.description ?? "");
const im = s.images?.[0];
setServiceImgUrl(im?.image_url?.trim() ?? "");
setServiceImageId(im?.id != null ? String(im.id) : null);
}
function openServiceModalCreate() {
beginEditService(null);
setServiceModalOpen(true);
}
function openServiceModalEdit(s: CmsServiceContent) {
beginEditService(s);
setServiceModalOpen(true);
}
async function saveServiceDraft() {
if (!servicePrimary.trim()) {
await Swal.fire({ icon: "warning", title: "Service title is required" });
return;
}
setSaving(true);
try {
const body = {
primary_title: servicePrimary,
secondary_title: serviceSecondary,
description: serviceDesc,
};
let sid = serviceEditId;
if (sid == null) {
const res = await saveOurServiceContent(body);
const created = apiPayload(res) as CmsServiceContent | null;
if (created?.id == null) {
await Swal.fire({ icon: "error", title: "Failed to create service" });
return;
}
sid = created.id;
} else {
const res = await updateOurServiceContent(sid, body);
if (res?.error) {
await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") });
return;
}
}
if (serviceImgUrl.trim() && sid != null) {
if (serviceImageId) {
await updateOurServiceImage(serviceImageId, { image_url: serviceImgUrl.trim() });
} else {
await saveOurServiceImage({
our_service_content_id: sid,
image_url: serviceImgUrl.trim(),
});
}
}
await Swal.fire({ icon: "success", title: "Service saved", timer: 1400, showConfirmButton: false });
await loadAll();
beginEditService(null);
setServiceModalOpen(false);
} finally {
setSaving(false);
}
}
async function removeService(id: number) {
const ok = await Swal.fire({
icon: "warning",
title: "Delete this service?",
showCancelButton: true,
});
if (!ok.isConfirmed) return;
setSaving(true);
try {
await deleteOurServiceContent(id);
if (serviceEditId === id) {
beginEditService(null);
setServiceModalOpen(false);
}
await loadAll();
} finally {
setSaving(false);
}
}
function beginEditPartner(p: CmsPartnerContent | null) {
if (!p) {
setEditingPartnerId(null);
setPartnerTitle("");
setPartnerImgUrl("");
return;
}
setEditingPartnerId(p.id);
setPartnerTitle(p.primary_title ?? "");
setPartnerImgUrl(p.image_url ?? "");
}
function openPartnerModalCreate() {
beginEditPartner(null);
setPartnerModalOpen(true);
}
function openPartnerModalEdit(p: CmsPartnerContent) {
beginEditPartner(p);
setPartnerModalOpen(true);
}
async function savePartnerRow() {
if (!partnerTitle.trim()) {
await Swal.fire({ icon: "warning", title: "Partner name is required" });
return;
}
setSaving(true);
try {
const body = {
primary_title: partnerTitle.trim(),
image_url: partnerImgUrl.trim() || undefined,
};
if (editingPartnerId) {
const res = await updatePartnerContent(editingPartnerId, body);
if (res?.error) {
await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") });
return;
}
} else {
const res = await savePartnerContent(body);
if (res?.error) {
await Swal.fire({ icon: "error", title: "Save failed", text: String(res.message ?? "") });
return;
}
}
beginEditPartner(null);
setPartnerModalOpen(false);
await Swal.fire({ icon: "success", title: "Partner saved", timer: 1400, showConfirmButton: false });
await loadAll();
} finally {
setSaving(false);
}
}
async function removePartner(id: string) {
const ok = await Swal.fire({
icon: "warning",
title: "Delete this partner?",
showCancelButton: true,
});
if (!ok.isConfirmed) return;
setSaving(true);
try {
await deletePartnerContent(id);
if (editingPartnerId === id) {
beginEditPartner(null);
setPartnerModalOpen(false);
}
await loadAll();
} finally {
setSaving(false);
}
}
function beginEditPopup(p: CmsPopupContent | null) {
if (!p) {
setPopupEditId(null);
setPopupPrimary("");
setPopupSecondary("");
setPopupDesc("");
setPopupCta1("");
setPopupCta2("");
setPopupImgUrl("");
return;
}
setPopupEditId(p.id);
setPopupPrimary(p.primary_title ?? "");
setPopupSecondary(p.secondary_title ?? "");
setPopupDesc(p.description ?? "");
setPopupCta1(p.primary_cta ?? "");
setPopupCta2(p.secondary_cta_text ?? "");
setPopupImgUrl(p.images?.[0]?.media_url?.trim() ?? "");
}
function openPopupModalCreate() {
beginEditPopup(null);
setPopupModalOpen(true);
}
function openPopupModalEdit(p: CmsPopupContent) {
beginEditPopup(p);
setPopupModalOpen(true);
}
async function savePopupDraft() {
if (!popupPrimary.trim()) {
await Swal.fire({ icon: "warning", title: "Main title is required" });
return;
}
setSaving(true);
try {
const body = {
primary_title: popupPrimary,
secondary_title: popupSecondary,
description: popupDesc,
primary_cta: popupCta1,
secondary_cta_text: popupCta2,
};
let pid = popupEditId;
if (pid == null) {
const res = await savePopupNews(body);
if (res?.error) {
await Swal.fire({ icon: "error", title: "Save failed", text: String(res.message ?? "") });
return;
}
const listRes = await getPopupNewsList(1, 50);
const rows = apiRows(listRes) as CmsPopupContent[];
pid =
rows.length > 0 ? Math.max(...rows.map((r) => r.id)) : null;
} else {
const res = await updatePopupNews(pid, { id: pid, ...body });
if (res?.error) {
await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") });
return;
}
}
if (popupImgUrl.trim() && pid != null) {
await savePopupNewsImage({
popup_news_content_id: pid,
media_url: popupImgUrl.trim(),
});
}
await Swal.fire({ icon: "success", title: "Pop up saved", timer: 1600, showConfirmButton: false });
await loadAll();
beginEditPopup(null);
setPopupModalOpen(false);
} finally {
setSaving(false);
}
}
async function removePopup(id: number) {
const ok = await Swal.fire({
icon: "warning",
title: "Delete this pop up?",
showCancelButton: true,
});
if (!ok.isConfirmed) return;
setSaving(true);
try {
await deletePopupNews(id);
if (popupEditId === id) {
beginEditPopup(null);
setPopupModalOpen(false);
}
await loadAll();
} finally {
setSaving(false);
}
}
if (loading) {
return (
<div className="flex min-h-[320px] items-center justify-center">
<Loader2 className="h-10 w-10 animate-spin text-[#966314]" />
</div>
);
}
return (
<div className="space-y-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 className="text-2xl font-semibold text-slate-800">Content Website</h1>
<p className="mt-1 text-sm text-slate-500">
Update homepage content, products, services, and partners.
</p>
</div>
<div className="flex flex-wrap gap-3">
<Button
type="button"
variant="outline"
className="rounded-lg"
onClick={() => window.open("/", "_blank")}
>
<Eye className="mr-2 h-4 w-4" />
Preview
</Button>
<Button type="button" className="rounded-lg bg-blue-600 hover:bg-blue-700" disabled>
<Pencil className="mr-2 h-4 w-4" />
Edit Mode
</Button>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="h-auto flex-wrap rounded-xl border bg-white p-1">
<TabsTrigger value="hero" className="rounded-lg">
Hero Section
</TabsTrigger>
<TabsTrigger value="about" className="rounded-lg">
About Us
</TabsTrigger>
<TabsTrigger value="products" className="rounded-lg">
Our Products
</TabsTrigger>
<TabsTrigger value="services" className="rounded-lg">
Our Services
</TabsTrigger>
<TabsTrigger value="partners" className="rounded-lg">
Technology Partners
</TabsTrigger>
<TabsTrigger value="popup" className="rounded-lg">
Pop Up
</TabsTrigger>
</TabsList>
<TabsContent value="hero" className="mt-4">
<Card className="rounded-2xl border shadow-sm">
<CardContent className="space-y-6 p-6">
<div className="grid gap-6 md:grid-cols-2">
<div>
<label className="text-sm font-medium text-slate-700">Main Title</label>
<Input
className="mt-2"
value={heroPrimary}
onChange={(e) => setHeroPrimary(e.target.value)}
placeholder="Headline"
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Subtitle</label>
<Input
className="mt-2"
value={heroSecondary}
onChange={(e) => setHeroSecondary(e.target.value)}
/>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Description</label>
<Textarea
className="mt-2 min-h-[120px]"
value={heroDesc}
onChange={(e) => setHeroDesc(e.target.value)}
placeholder="Supporting text"
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Hero image URL</label>
<p className="mb-2 text-xs text-slate-500">
Paste a public image URL (CDN / MinIO). Shown on the landing hero.
</p>
<Input
className="mt-1"
value={heroImgUrl}
onChange={(e) => setHeroImgUrl(e.target.value)}
placeholder="https://..."
/>
{heroImgUrl ? (
<div className="mt-4 flex justify-center rounded-xl border bg-slate-50 p-6">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={heroImgUrl} alt="" className="max-h-48 object-contain" />
</div>
) : (
<div className="mt-4 flex flex-col items-center justify-center rounded-xl border-2 border-dashed bg-slate-50 p-10 text-center">
<ImageIcon className="mb-2 h-8 w-8 text-slate-400" />
<p className="text-sm text-slate-500">No image URL yet</p>
</div>
)}
</div>
<div className="grid gap-6 md:grid-cols-2">
<div>
<label className="text-sm font-medium text-slate-700">Primary CTA Text</label>
<Input
className="mt-2"
value={heroCta1}
onChange={(e) => setHeroCta1(e.target.value)}
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Secondary CTA Text</label>
<Input
className="mt-2"
value={heroCta2}
onChange={(e) => setHeroCta2(e.target.value)}
/>
</div>
</div>
<Button type="button" onClick={saveHeroTab} disabled={saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save Hero
</Button>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="about" className="mt-4">
<Card className="rounded-2xl border shadow-sm">
<CardContent className="space-y-6 p-6">
<div className="grid gap-6 md:grid-cols-2">
<div>
<label className="text-sm font-medium text-slate-700">Main Title</label>
<Input
className="mt-2"
value={aboutPrimary}
onChange={(e) => setAboutPrimary(e.target.value)}
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Subtitle</label>
<Input
className="mt-2"
value={aboutSecondary}
onChange={(e) => setAboutSecondary(e.target.value)}
placeholder="—"
/>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Description</label>
<Textarea
className="mt-2 min-h-[120px]"
value={aboutDesc}
onChange={(e) => setAboutDesc(e.target.value)}
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Media URL</label>
<p className="mb-2 text-xs text-slate-500">
Image or video URL (e.g. .mp4 on CDN). Shown inside the phone mockup on the landing page.
</p>
<Input
className="mt-1"
value={aboutMediaUrl}
onChange={(e) => setAboutMediaUrl(e.target.value)}
placeholder="https://.../video.mp4"
/>
{aboutMediaUrl.trim() ? (
<div className="mt-4 flex justify-center rounded-xl border bg-slate-50 p-6">
{/\.(mp4|webm)(\?|$)/i.test(aboutMediaUrl) ||
aboutMediaUrl.toLowerCase().includes("video") ? (
// eslint-disable-next-line jsx-a11y/media-has-caption
<video
src={aboutMediaUrl}
controls
className="max-h-56 max-w-full rounded-lg"
playsInline
/>
) : (
// eslint-disable-next-line @next/next/no-img-element
<img
src={aboutMediaUrl}
alt=""
className="max-h-56 object-contain"
/>
)}
</div>
) : (
<div className="mt-4 flex flex-col items-center justify-center rounded-xl border-2 border-dashed bg-slate-50 p-10 text-center">
<ImageIcon className="mb-2 h-8 w-8 text-slate-400" />
<p className="text-sm text-slate-500">No media URL yet</p>
</div>
)}
</div>
<div className="grid gap-6 md:grid-cols-2">
<div>
<label className="text-sm font-medium text-slate-700">Primary CTA Text</label>
<Input
className="mt-2"
value={aboutCta1}
onChange={(e) => setAboutCta1(e.target.value)}
placeholder="—"
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Secondary CTA Text</label>
<Input
className="mt-2"
value={aboutCta2}
onChange={(e) => setAboutCta2(e.target.value)}
placeholder="—"
/>
</div>
</div>
<Button type="button" onClick={saveAboutTab} disabled={saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save About Us
</Button>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="products" className="mt-4">
<Card className="rounded-2xl border shadow-sm">
<CardContent className="space-y-6 p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<p className="text-sm text-slate-600">
Manage product listings on the homepage.
</p>
<Button
type="button"
className="shrink-0 rounded-lg bg-blue-600 hover:bg-blue-700"
onClick={openProductModalCreate}
>
<Plus className="mr-2 h-4 w-4" />
Add Product
</Button>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{products.map((p) => {
const img = p.images?.[0]?.image_url;
return (
<div
key={p.id}
className={`flex flex-col overflow-hidden rounded-xl border bg-white shadow-sm ${
productModalOpen && productEditId === p.id
? "ring-2 ring-blue-500"
: ""
}`}
>
<button
type="button"
onClick={() => openProductModalEdit(p)}
className="relative aspect-[4/3] w-full bg-slate-100"
>
{img ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={img} alt="" className="h-full w-full object-cover" />
) : (
<div className="flex h-full items-center justify-center text-slate-400">
<ImageIcon className="h-10 w-10" />
</div>
)}
</button>
<div className="flex flex-1 flex-col p-4">
<h3 className="line-clamp-2 font-semibold text-slate-900">
{p.primary_title || "Untitled"}
</h3>
<p className="mt-1 line-clamp-3 text-sm text-slate-600">
{p.description || "—"}
</p>
<div className="mt-3 flex gap-2">
<Button
type="button"
size="sm"
variant="outline"
className="flex-1"
onClick={() => openProductModalEdit(p)}
>
Edit
</Button>
<Button
type="button"
size="sm"
variant="ghost"
className="text-red-600"
onClick={() => removeProduct(p.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
})}
</div>
<Dialog
open={productModalOpen}
onOpenChange={(open) => {
setProductModalOpen(open);
if (!open) beginEditProduct(null);
}}
>
<DialogContent className="max-h-[min(90vh,640px)] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{productEditId ? "Edit product" : "Add Product"}
</DialogTitle>
<DialogDescription>
Fill in the details below. Image URL is shown on the product card on the homepage.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-2">
<div className="grid gap-4 sm:grid-cols-2">
<div className="sm:col-span-2">
<label className="text-sm font-medium text-slate-700">Product title</label>
<Input
className="mt-2"
value={productPrimary}
onChange={(e) => setProductPrimary(e.target.value)}
/>
</div>
<div className="sm:col-span-2">
<label className="text-sm font-medium text-slate-700">Subtitle (optional)</label>
<Input
className="mt-2"
value={productSecondary}
onChange={(e) => setProductSecondary(e.target.value)}
/>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Description</label>
<Textarea
className="mt-2 min-h-[100px]"
value={productDesc}
onChange={(e) => setProductDesc(e.target.value)}
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Card image URL</label>
<Input
className="mt-2"
value={productImgUrl}
onChange={(e) => setProductImgUrl(e.target.value)}
placeholder="https://..."
/>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
type="button"
variant="outline"
onClick={() => {
setProductModalOpen(false);
beginEditProduct(null);
}}
>
Cancel
</Button>
<Button type="button" onClick={saveProductDraft} disabled={saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="services" className="mt-4">
<Card className="rounded-2xl border shadow-sm">
<CardContent className="space-y-6 p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<p className="text-sm text-slate-600">
Manage service listings on the homepage.
</p>
<Button
type="button"
className="shrink-0 rounded-lg bg-blue-600 hover:bg-blue-700"
onClick={openServiceModalCreate}
>
<Plus className="mr-2 h-4 w-4" />
Add Service
</Button>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{services.map((s) => {
const img = s.images?.[0]?.image_url;
return (
<div
key={s.id}
className={`flex flex-col overflow-hidden rounded-xl border bg-white shadow-sm ${
serviceModalOpen && serviceEditId === s.id
? "ring-2 ring-blue-500"
: ""
}`}
>
<button
type="button"
onClick={() => openServiceModalEdit(s)}
className="relative aspect-video w-full bg-slate-100"
>
{img ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={img} alt="" className="h-full w-full object-cover" />
) : (
<div className="flex h-full items-center justify-center text-slate-400">
<ImageIcon className="h-10 w-10" />
</div>
)}
</button>
<div className="flex flex-1 flex-col p-4">
<h3 className="line-clamp-2 font-semibold text-slate-900">
{s.primary_title || "Untitled"}
</h3>
<p className="mt-1 line-clamp-3 text-sm text-slate-600">
{s.description || "—"}
</p>
<div className="mt-3 flex gap-2">
<Button
type="button"
size="sm"
variant="outline"
className="flex-1"
onClick={() => openServiceModalEdit(s)}
>
Edit
</Button>
<Button
type="button"
size="sm"
variant="ghost"
className="text-red-600"
onClick={() => removeService(s.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
})}
</div>
<Dialog
open={serviceModalOpen}
onOpenChange={(open) => {
setServiceModalOpen(open);
if (!open) beginEditService(null);
}}
>
<DialogContent className="max-h-[min(90vh,640px)] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{serviceEditId != null ? "Edit service" : "Add Service"}
</DialogTitle>
<DialogDescription>
Banner image appears on the service card on the homepage.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-2">
<div className="grid gap-4 sm:grid-cols-2">
<div className="sm:col-span-2">
<label className="text-sm font-medium text-slate-700">Service title</label>
<Input
className="mt-2"
value={servicePrimary}
onChange={(e) => setServicePrimary(e.target.value)}
/>
</div>
<div className="sm:col-span-2">
<label className="text-sm font-medium text-slate-700">Subtitle (optional)</label>
<Input
className="mt-2"
value={serviceSecondary}
onChange={(e) => setServiceSecondary(e.target.value)}
/>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Description</label>
<Textarea
className="mt-2 min-h-[100px]"
value={serviceDesc}
onChange={(e) => setServiceDesc(e.target.value)}
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Banner image URL</label>
<Input
className="mt-2"
value={serviceImgUrl}
onChange={(e) => setServiceImgUrl(e.target.value)}
placeholder="https://..."
/>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
type="button"
variant="outline"
onClick={() => {
setServiceModalOpen(false);
beginEditService(null);
}}
>
Cancel
</Button>
<Button type="button" onClick={saveServiceDraft} disabled={saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="partners" className="mt-4">
<Card className="rounded-2xl border shadow-sm">
<CardContent className="space-y-6 p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<p className="text-sm text-slate-600">
Manage technology partner listings on the homepage.
</p>
<Button
type="button"
className="shrink-0 rounded-lg bg-blue-600 hover:bg-blue-700"
onClick={openPartnerModalCreate}
>
<Plus className="mr-2 h-4 w-4" />
Add Partner
</Button>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{partners.map((p) => {
const img = p.image_url;
return (
<div
key={p.id}
className={`flex flex-col overflow-hidden rounded-xl border bg-white shadow-sm ${
partnerModalOpen && editingPartnerId === p.id
? "ring-2 ring-blue-500"
: ""
}`}
>
<button
type="button"
onClick={() => openPartnerModalEdit(p)}
className="relative flex aspect-[4/3] w-full items-center justify-center bg-slate-100"
>
{img ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={img}
alt=""
className="max-h-full max-w-full object-contain p-4"
/>
) : (
<ImageIcon className="h-10 w-10 text-slate-400" />
)}
</button>
<div className="flex flex-1 flex-col p-4">
<h3 className="line-clamp-2 font-semibold text-slate-900">
{p.primary_title || "Untitled"}
</h3>
<div className="mt-3 flex gap-2">
<Button
type="button"
size="sm"
variant="outline"
className="flex-1"
onClick={() => openPartnerModalEdit(p)}
>
Edit
</Button>
<Button
type="button"
size="sm"
variant="ghost"
className="text-red-600"
onClick={() => removePartner(p.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
})}
</div>
{partners.length === 0 ? (
<p className="text-center text-sm text-slate-500">No partners yet</p>
) : null}
<Dialog
open={partnerModalOpen}
onOpenChange={(open) => {
setPartnerModalOpen(open);
if (!open) beginEditPartner(null);
}}
>
<DialogContent className="max-h-[min(90vh,560px)] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{editingPartnerId ? "Edit partner" : "Add Partner"}
</DialogTitle>
<DialogDescription>
Partner name and logo URL are shown on the homepage technology partners section.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-2">
<div>
<label className="text-sm font-medium text-slate-700">Partner name</label>
<Input
className="mt-2"
value={partnerTitle}
onChange={(e) => setPartnerTitle(e.target.value)}
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Logo image URL</label>
<Input
className="mt-2"
value={partnerImgUrl}
onChange={(e) => setPartnerImgUrl(e.target.value)}
placeholder="https://..."
/>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
type="button"
variant="outline"
onClick={() => {
setPartnerModalOpen(false);
beginEditPartner(null);
}}
>
Cancel
</Button>
<Button type="button" onClick={savePartnerRow} disabled={saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="popup" className="mt-4">
<Card className="rounded-2xl border shadow-sm">
<CardContent className="space-y-6 p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<p className="text-sm text-slate-600">Manage pop ups on the homepage.</p>
<Button
type="button"
className="shrink-0 rounded-lg bg-blue-600 hover:bg-blue-700"
onClick={openPopupModalCreate}
>
<Plus className="mr-2 h-4 w-4" />
Add Pop Up
</Button>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{popups.map((p) => {
const thumb = p.images?.[0]?.media_url;
return (
<div
key={p.id}
className={`flex flex-col overflow-hidden rounded-xl border bg-white shadow-sm ${
popupModalOpen && popupEditId === p.id
? "ring-2 ring-blue-500"
: ""
}`}
>
<button
type="button"
onClick={() => openPopupModalEdit(p)}
className="relative aspect-[5/3] w-full bg-slate-100"
>
{thumb ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={thumb} alt="" className="h-full w-full object-cover" />
) : (
<div className="flex h-full items-center justify-center text-slate-400">
<ImageIcon className="h-10 w-10" />
</div>
)}
</button>
<div className="flex flex-1 flex-col p-4">
<h3 className="line-clamp-2 font-semibold text-slate-900">
{p.primary_title || "Untitled"}
</h3>
<p className="mt-1 line-clamp-2 text-sm text-slate-600">
{p.description || "—"}
</p>
<div className="mt-3 flex gap-2">
<Button
type="button"
size="sm"
variant="outline"
className="flex-1"
onClick={() => openPopupModalEdit(p)}
>
Edit
</Button>
<Button
type="button"
size="sm"
variant="ghost"
className="text-red-600"
onClick={() => removePopup(p.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
})}
</div>
<Dialog
open={popupModalOpen}
onOpenChange={(open) => {
setPopupModalOpen(open);
if (!open) beginEditPopup(null);
}}
>
<DialogContent className="max-h-[min(90vh,720px)] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{popupEditId != null ? "Edit pop up" : "Add Pop Up"}
</DialogTitle>
<DialogDescription>
Content appears in the homepage news pop-up banner.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-2">
<div className="grid gap-4 sm:grid-cols-2">
<div className="sm:col-span-2">
<label className="text-sm font-medium text-slate-700">Main Title</label>
<Input
className="mt-2"
value={popupPrimary}
onChange={(e) => setPopupPrimary(e.target.value)}
/>
</div>
<div className="sm:col-span-2">
<label className="text-sm font-medium text-slate-700">Subtitle</label>
<Input
className="mt-2"
value={popupSecondary}
onChange={(e) => setPopupSecondary(e.target.value)}
/>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Description</label>
<Textarea
className="mt-2 min-h-[100px]"
value={popupDesc}
onChange={(e) => setPopupDesc(e.target.value)}
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="text-sm font-medium text-slate-700">Primary CTA</label>
<Input
className="mt-2"
value={popupCta1}
onChange={(e) => setPopupCta1(e.target.value)}
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Secondary CTA</label>
<Input
className="mt-2"
value={popupCta2}
onChange={(e) => setPopupCta2(e.target.value)}
/>
</div>
</div>
<div>
<label className="text-sm font-medium text-slate-700">
Banner image URL (optional)
</label>
<Input
className="mt-2"
value={popupImgUrl}
onChange={(e) => setPopupImgUrl(e.target.value)}
placeholder="https://..."
/>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
type="button"
variant="outline"
onClick={() => {
setPopupModalOpen(false);
beginEditPopup(null);
}}
>
Cancel
</Button>
<Button type="button" onClick={savePopupDraft} disabled={saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}