feat: update content popup
continuous-integration/drone/push Build is failing Details

This commit is contained in:
hanif salafi 2026-04-10 15:18:11 +07:00
parent 2eaa34d052
commit 6176567557
4 changed files with 479 additions and 124 deletions

View File

@ -1,6 +1,12 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import {
useCallback,
useEffect,
useRef,
useState,
type MutableRefObject,
} from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -40,27 +46,47 @@ import {
getPartnerContents,
getPopupNewsList,
saveAboutContent,
saveAboutUsMediaUrl,
saveAboutUsMediaUpload,
saveHeroContent,
saveHeroImage,
saveHeroImageUpload,
saveOurProductContent,
saveOurServiceContent,
savePartnerContent,
saveOurProductImage,
saveOurServiceImage,
saveOurProductImageUpload,
saveOurServiceImageUpload,
savePopupNews,
savePopupNewsImage,
savePopupNewsImageUpload,
updateAboutContent,
updateHeroContent,
updateHeroImage,
updateHeroImageUpload,
updateOurProductContent,
updateOurProductImage,
updateOurProductImageUpload,
updateOurServiceContent,
updateOurServiceImage,
updateOurServiceImageUpload,
updatePartnerContent,
updatePopupNews,
uploadPartnerLogo,
} from "@/service/cms-landing";
function revokeBlobRef(ref: MutableRefObject<string | null>) {
if (ref.current) {
URL.revokeObjectURL(ref.current);
ref.current = null;
}
}
function setPickedFile(
file: File | null,
ref: MutableRefObject<string | null>,
setter: (f: File | null) => void,
) {
revokeBlobRef(ref);
if (file) {
ref.current = URL.createObjectURL(file);
}
setter(file);
}
export default function ContentWebsite() {
const [activeTab, setActiveTab] = useState("hero");
const [loading, setLoading] = useState(true);
@ -73,7 +99,9 @@ export default function ContentWebsite() {
const [heroDesc, setHeroDesc] = useState("");
const [heroCta1, setHeroCta1] = useState("");
const [heroCta2, setHeroCta2] = useState("");
const [heroImgUrl, setHeroImgUrl] = useState("");
const [heroRemoteUrl, setHeroRemoteUrl] = useState("");
const [heroPendingFile, setHeroPendingFile] = useState<File | null>(null);
const heroBlobUrlRef = useRef<string | null>(null);
const [aboutId, setAboutId] = useState<number | null>(null);
const [aboutPrimary, setAboutPrimary] = useState("");
@ -81,18 +109,23 @@ export default function ContentWebsite() {
const [aboutDesc, setAboutDesc] = useState("");
const [aboutCta1, setAboutCta1] = useState("");
const [aboutCta2, setAboutCta2] = useState("");
const [aboutMediaUrl, setAboutMediaUrl] = useState("");
const [aboutRemoteMediaUrl, setAboutRemoteMediaUrl] = useState("");
const [aboutPendingFile, setAboutPendingFile] = useState<File | null>(null);
const aboutBlobUrlRef = useRef<string | null>(null);
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 [productRemoteUrl, setProductRemoteUrl] = useState("");
const [productPendingFile, setProductPendingFile] = useState<File | null>(
null,
);
const productBlobUrlRef = useRef<string | null>(null);
const [productImageId, setProductImageId] = useState<string | null>(null);
const [productModalOpen, setProductModalOpen] = useState(false);
@ -101,13 +134,22 @@ export default function ContentWebsite() {
const [servicePrimary, setServicePrimary] = useState("");
const [serviceSecondary, setServiceSecondary] = useState("");
const [serviceDesc, setServiceDesc] = useState("");
const [serviceImgUrl, setServiceImgUrl] = useState("");
const [serviceRemoteUrl, setServiceRemoteUrl] = useState("");
const [servicePendingFile, setServicePendingFile] = useState<File | null>(
null,
);
const serviceBlobUrlRef = useRef<string | null>(null);
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 [partnerRemoteUrl, setPartnerRemoteUrl] = useState("");
const [partnerStoredPath, setPartnerStoredPath] = useState("");
const [partnerPendingFile, setPartnerPendingFile] = useState<File | null>(
null,
);
const partnerBlobUrlRef = useRef<string | null>(null);
const [editingPartnerId, setEditingPartnerId] = useState<string | null>(null);
const [partnerModalOpen, setPartnerModalOpen] = useState(false);
@ -118,7 +160,9 @@ export default function ContentWebsite() {
const [popupDesc, setPopupDesc] = useState("");
const [popupCta1, setPopupCta1] = useState("");
const [popupCta2, setPopupCta2] = useState("");
const [popupImgUrl, setPopupImgUrl] = useState("");
const [popupRemoteUrl, setPopupRemoteUrl] = useState("");
const [popupPendingFile, setPopupPendingFile] = useState<File | null>(null);
const popupBlobUrlRef = useRef<string | null>(null);
const [popupModalOpen, setPopupModalOpen] = useState(false);
const loadAll = useCallback(async () => {
@ -134,11 +178,13 @@ export default function ContentWebsite() {
setHeroCta1(hero.primary_cta ?? "");
setHeroCta2(hero.secondary_cta_text ?? "");
const first = hero.images?.[0];
revokeBlobRef(heroBlobUrlRef);
setHeroPendingFile(null);
if (first?.image_url) {
setHeroImgUrl(first.image_url);
setHeroRemoteUrl(first.image_url);
setHeroImageId(first.id ?? null);
} else {
setHeroImgUrl("");
setHeroRemoteUrl("");
setHeroImageId(null);
}
} else {
@ -149,7 +195,9 @@ export default function ContentWebsite() {
setHeroDesc("");
setHeroCta1("");
setHeroCta2("");
setHeroImgUrl("");
revokeBlobRef(heroBlobUrlRef);
setHeroPendingFile(null);
setHeroRemoteUrl("");
}
const aboutRes = await getAboutContentsList();
@ -164,8 +212,9 @@ export default function ContentWebsite() {
setAboutCta2(ab.secondary_cta_text ?? "");
const am = ab.images?.[0];
const murl = am?.media_url?.trim() ?? "";
setAboutMediaUrl(murl);
setAboutMediaLoadedUrl(murl);
revokeBlobRef(aboutBlobUrlRef);
setAboutPendingFile(null);
setAboutRemoteMediaUrl(murl);
setAboutMediaImageId(am?.id ?? null);
} else {
setAboutId(null);
@ -174,8 +223,9 @@ export default function ContentWebsite() {
setAboutDesc("");
setAboutCta1("");
setAboutCta2("");
setAboutMediaUrl("");
setAboutMediaLoadedUrl("");
revokeBlobRef(aboutBlobUrlRef);
setAboutPendingFile(null);
setAboutRemoteMediaUrl("");
setAboutMediaImageId(null);
}
@ -231,15 +281,26 @@ export default function ContentWebsite() {
return;
}
}
if (heroImgUrl.trim() && hid) {
if (heroPendingFile && hid) {
const fd = new FormData();
fd.append("file", heroPendingFile);
let imgRes;
if (heroImageId) {
await updateHeroImage(heroImageId, { image_url: heroImgUrl.trim() });
imgRes = await updateHeroImageUpload(heroImageId, fd);
} else {
await saveHeroImage({
hero_content_id: hid,
image_url: heroImgUrl.trim(),
});
fd.append("hero_content_id", hid);
imgRes = await saveHeroImageUpload(fd);
}
if (imgRes?.error) {
await Swal.fire({
icon: "error",
title: "Hero image upload failed",
text: String(imgRes.message ?? ""),
});
return;
}
revokeBlobRef(heroBlobUrlRef);
setHeroPendingFile(null);
}
await Swal.fire({ icon: "success", title: "Hero section saved", timer: 1600, showConfirmButton: false });
await loadAll();
@ -280,29 +341,24 @@ export default function ContentWebsite() {
}
}
const url = aboutMediaUrl.trim();
if (aid != null) {
if (!url) {
if (aid != null && aboutPendingFile) {
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,
});
const fd = new FormData();
fd.append("about_us_content_id", String(aid));
fd.append("file", aboutPendingFile);
const mres = await saveAboutUsMediaUpload(fd);
if (mres?.error) {
await Swal.fire({
icon: "error",
title: "Media URL failed",
title: "Media upload failed",
text: String(mres.message ?? ""),
});
return;
}
}
revokeBlobRef(aboutBlobUrlRef);
setAboutPendingFile(null);
}
await Swal.fire({ icon: "success", title: "About Us saved", timer: 1600, showConfirmButton: false });
@ -318,7 +374,9 @@ export default function ContentWebsite() {
setProductPrimary("");
setProductSecondary("");
setProductDesc("");
setProductImgUrl("");
revokeBlobRef(productBlobUrlRef);
setProductPendingFile(null);
setProductRemoteUrl("");
setProductImageId(null);
return;
}
@ -327,7 +385,9 @@ export default function ContentWebsite() {
setProductSecondary(p.secondary_title ?? "");
setProductDesc(p.description ?? "");
const im = p.images?.[0];
setProductImgUrl(im?.image_url?.trim() ?? "");
revokeBlobRef(productBlobUrlRef);
setProductPendingFile(null);
setProductRemoteUrl(im?.image_url?.trim() ?? "");
setProductImageId(im?.id ?? null);
}
@ -369,15 +429,26 @@ export default function ContentWebsite() {
return;
}
}
if (productImgUrl.trim() && pid) {
if (productPendingFile && pid) {
const fd = new FormData();
fd.append("file", productPendingFile);
let ires;
if (productImageId) {
await updateOurProductImage(productImageId, { image_url: productImgUrl.trim() });
ires = await updateOurProductImageUpload(productImageId, fd);
} else {
await saveOurProductImage({
our_product_content_id: pid,
image_url: productImgUrl.trim(),
});
fd.append("our_product_content_id", pid);
ires = await saveOurProductImageUpload(fd);
}
if (ires?.error) {
await Swal.fire({
icon: "error",
title: "Product image upload failed",
text: String(ires.message ?? ""),
});
return;
}
revokeBlobRef(productBlobUrlRef);
setProductPendingFile(null);
}
await Swal.fire({ icon: "success", title: "Product saved", timer: 1400, showConfirmButton: false });
await loadAll();
@ -414,7 +485,9 @@ export default function ContentWebsite() {
setServicePrimary("");
setServiceSecondary("");
setServiceDesc("");
setServiceImgUrl("");
revokeBlobRef(serviceBlobUrlRef);
setServicePendingFile(null);
setServiceRemoteUrl("");
setServiceImageId(null);
return;
}
@ -423,7 +496,9 @@ export default function ContentWebsite() {
setServiceSecondary(s.secondary_title ?? "");
setServiceDesc(s.description ?? "");
const im = s.images?.[0];
setServiceImgUrl(im?.image_url?.trim() ?? "");
revokeBlobRef(serviceBlobUrlRef);
setServicePendingFile(null);
setServiceRemoteUrl(im?.image_url?.trim() ?? "");
setServiceImageId(im?.id != null ? String(im.id) : null);
}
@ -465,15 +540,26 @@ export default function ContentWebsite() {
return;
}
}
if (serviceImgUrl.trim() && sid != null) {
if (servicePendingFile && sid != null) {
const fd = new FormData();
fd.append("file", servicePendingFile);
let ires;
if (serviceImageId) {
await updateOurServiceImage(serviceImageId, { image_url: serviceImgUrl.trim() });
ires = await updateOurServiceImageUpload(serviceImageId, fd);
} else {
await saveOurServiceImage({
our_service_content_id: sid,
image_url: serviceImgUrl.trim(),
});
fd.append("our_service_content_id", String(sid));
ires = await saveOurServiceImageUpload(fd);
}
if (ires?.error) {
await Swal.fire({
icon: "error",
title: "Service image upload failed",
text: String(ires.message ?? ""),
});
return;
}
revokeBlobRef(serviceBlobUrlRef);
setServicePendingFile(null);
}
await Swal.fire({ icon: "success", title: "Service saved", timer: 1400, showConfirmButton: false });
await loadAll();
@ -508,12 +594,18 @@ export default function ContentWebsite() {
if (!p) {
setEditingPartnerId(null);
setPartnerTitle("");
setPartnerImgUrl("");
revokeBlobRef(partnerBlobUrlRef);
setPartnerPendingFile(null);
setPartnerRemoteUrl("");
setPartnerStoredPath("");
return;
}
setEditingPartnerId(p.id);
setPartnerTitle(p.primary_title ?? "");
setPartnerImgUrl(p.image_url ?? "");
revokeBlobRef(partnerBlobUrlRef);
setPartnerPendingFile(null);
setPartnerRemoteUrl(p.image_url ?? "");
setPartnerStoredPath(p.image_path ?? "");
}
function openPartnerModalCreate() {
@ -535,10 +627,14 @@ export default function ContentWebsite() {
try {
const body = {
primary_title: partnerTitle.trim(),
image_url: partnerImgUrl.trim() || undefined,
};
let partnerId = editingPartnerId;
if (editingPartnerId) {
const res = await updatePartnerContent(editingPartnerId, body);
const res = await updatePartnerContent(editingPartnerId, {
primary_title: body.primary_title,
image_path: partnerStoredPath,
image_url: partnerRemoteUrl,
});
if (res?.error) {
await Swal.fire({ icon: "error", title: "Update failed", text: String(res.message ?? "") });
return;
@ -549,6 +645,23 @@ export default function ContentWebsite() {
await Swal.fire({ icon: "error", title: "Save failed", text: String(res.message ?? "") });
return;
}
const created = apiPayload(res) as CmsPartnerContent | null;
partnerId = created?.id ?? null;
}
if (partnerPendingFile && partnerId) {
const fd = new FormData();
fd.append("file", partnerPendingFile);
const up = await uploadPartnerLogo(partnerId, fd);
if (up?.error) {
await Swal.fire({
icon: "error",
title: "Logo upload failed",
text: String(up.message ?? ""),
});
return;
}
revokeBlobRef(partnerBlobUrlRef);
setPartnerPendingFile(null);
}
beginEditPartner(null);
setPartnerModalOpen(false);
@ -587,7 +700,9 @@ export default function ContentWebsite() {
setPopupDesc("");
setPopupCta1("");
setPopupCta2("");
setPopupImgUrl("");
revokeBlobRef(popupBlobUrlRef);
setPopupPendingFile(null);
setPopupRemoteUrl("");
return;
}
setPopupEditId(p.id);
@ -596,7 +711,9 @@ export default function ContentWebsite() {
setPopupDesc(p.description ?? "");
setPopupCta1(p.primary_cta ?? "");
setPopupCta2(p.secondary_cta_text ?? "");
setPopupImgUrl(p.images?.[0]?.media_url?.trim() ?? "");
revokeBlobRef(popupBlobUrlRef);
setPopupPendingFile(null);
setPopupRemoteUrl(p.images?.[0]?.media_url?.trim() ?? "");
}
function openPopupModalCreate() {
@ -630,10 +747,16 @@ export default function ContentWebsite() {
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;
const created = apiPayload(res) as CmsPopupContent | null;
pid = created?.id ?? null;
if (pid == null) {
await Swal.fire({
icon: "error",
title: "Save failed",
text: "Could not read new pop-up id from server.",
});
return;
}
} else {
const res = await updatePopupNews(pid, { id: pid, ...body });
if (res?.error) {
@ -641,11 +764,21 @@ export default function ContentWebsite() {
return;
}
}
if (popupImgUrl.trim() && pid != null) {
await savePopupNewsImage({
popup_news_content_id: pid,
media_url: popupImgUrl.trim(),
if (popupPendingFile && pid != null) {
const fd = new FormData();
fd.append("popup_news_content_id", String(pid));
fd.append("file", popupPendingFile);
const imgRes = await savePopupNewsImageUpload(fd);
if (imgRes?.error) {
await Swal.fire({
icon: "error",
title: "Popup image upload failed",
text: String(imgRes.message ?? ""),
});
return;
}
revokeBlobRef(popupBlobUrlRef);
setPopupPendingFile(null);
}
await Swal.fire({ icon: "success", title: "Pop up saved", timer: 1600, showConfirmButton: false });
await loadAll();
@ -764,25 +897,33 @@ export default function ContentWebsite() {
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Hero image URL</label>
<label className="text-sm font-medium text-slate-700">Hero image</label>
<p className="mb-2 text-xs text-slate-500">
Paste a public image URL (CDN / MinIO). Shown on the landing hero.
Upload a JPG, PNG, GIF, or WebP file. Stored in MinIO and shown on the landing hero.
</p>
<Input
className="mt-1"
value={heroImgUrl}
onChange={(e) => setHeroImgUrl(e.target.value)}
placeholder="https://..."
className="mt-1 cursor-pointer"
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
onChange={(e) => {
const f = e.target.files?.[0] ?? null;
setPickedFile(f, heroBlobUrlRef, setHeroPendingFile);
e.target.value = "";
}}
/>
{heroImgUrl ? (
{heroBlobUrlRef.current || heroRemoteUrl ? (
<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" />
<img
src={heroBlobUrlRef.current || heroRemoteUrl}
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>
<p className="text-sm text-slate-500">No hero image yet</p>
</div>
)}
</div>
@ -843,23 +984,27 @@ export default function ContentWebsite() {
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Media URL</label>
<label className="text-sm font-medium text-slate-700">Media (image or video)</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.
Upload JPG/PNG/GIF/WebP or MP4/WebM. Stored in MinIO; 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"
className="mt-1 cursor-pointer"
type="file"
accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm"
onChange={(e) => {
const f = e.target.files?.[0] ?? null;
setPickedFile(f, aboutBlobUrlRef, setAboutPendingFile);
e.target.value = "";
}}
/>
{aboutMediaUrl.trim() ? (
{aboutBlobUrlRef.current || aboutRemoteMediaUrl ? (
<div className="mt-4 flex justify-center rounded-xl border bg-slate-50 p-6">
{/\.(mp4|webm)(\?|$)/i.test(aboutMediaUrl) ||
aboutMediaUrl.toLowerCase().includes("video") ? (
{aboutPendingFile?.type.startsWith("video/") ||
/\.(mp4|webm)(\?|$)/i.test(aboutRemoteMediaUrl) ? (
// eslint-disable-next-line jsx-a11y/media-has-caption
<video
src={aboutMediaUrl}
src={aboutBlobUrlRef.current || aboutRemoteMediaUrl}
controls
className="max-h-56 max-w-full rounded-lg"
playsInline
@ -867,7 +1012,7 @@ export default function ContentWebsite() {
) : (
// eslint-disable-next-line @next/next/no-img-element
<img
src={aboutMediaUrl}
src={aboutBlobUrlRef.current || aboutRemoteMediaUrl}
alt=""
className="max-h-56 object-contain"
/>
@ -876,9 +1021,44 @@ export default function ContentWebsite() {
) : (
<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>
<p className="text-sm text-slate-500">No media yet</p>
</div>
)}
{aboutMediaImageId != null ? (
<Button
type="button"
variant="outline"
size="sm"
className="mt-3"
onClick={async () => {
const ok = await Swal.fire({
icon: "question",
title: "Remove current media?",
showCancelButton: true,
});
if (!ok.isConfirmed) return;
setSaving(true);
try {
await deleteAboutUsContentImage(aboutMediaImageId);
revokeBlobRef(aboutBlobUrlRef);
setAboutPendingFile(null);
setAboutRemoteMediaUrl("");
setAboutMediaImageId(null);
await Swal.fire({
icon: "success",
title: "Media removed",
timer: 1200,
showConfirmButton: false,
});
await loadAll();
} finally {
setSaving(false);
}
}}
>
Remove current media
</Button>
) : null}
</div>
<div className="grid gap-6 md:grid-cols-2">
<div>
@ -1028,13 +1208,27 @@ export default function ContentWebsite() {
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Card image URL</label>
<label className="text-sm font-medium text-slate-700">Card image</label>
<Input
className="mt-2"
value={productImgUrl}
onChange={(e) => setProductImgUrl(e.target.value)}
placeholder="https://..."
className="mt-2 cursor-pointer"
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
onChange={(e) => {
const f = e.target.files?.[0] ?? null;
setPickedFile(f, productBlobUrlRef, setProductPendingFile);
e.target.value = "";
}}
/>
{productBlobUrlRef.current || productRemoteUrl ? (
<div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-3">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={productBlobUrlRef.current || productRemoteUrl}
alt=""
className="max-h-36 object-contain"
/>
</div>
) : null}
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
@ -1179,13 +1373,27 @@ export default function ContentWebsite() {
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Banner image URL</label>
<label className="text-sm font-medium text-slate-700">Banner image</label>
<Input
className="mt-2"
value={serviceImgUrl}
onChange={(e) => setServiceImgUrl(e.target.value)}
placeholder="https://..."
className="mt-2 cursor-pointer"
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
onChange={(e) => {
const f = e.target.files?.[0] ?? null;
setPickedFile(f, serviceBlobUrlRef, setServicePendingFile);
e.target.value = "";
}}
/>
{serviceBlobUrlRef.current || serviceRemoteUrl ? (
<div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-3">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={serviceBlobUrlRef.current || serviceRemoteUrl}
alt=""
className="max-h-36 object-contain"
/>
</div>
) : null}
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
@ -1302,7 +1510,7 @@ export default function ContentWebsite() {
{editingPartnerId ? "Edit partner" : "Add Partner"}
</DialogTitle>
<DialogDescription>
Partner name and logo URL are shown on the homepage technology partners section.
Partner name and logo are shown on the homepage technology partners section. Logo is stored in MinIO.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-2">
@ -1315,13 +1523,27 @@ export default function ContentWebsite() {
/>
</div>
<div>
<label className="text-sm font-medium text-slate-700">Logo image URL</label>
<label className="text-sm font-medium text-slate-700">Logo image</label>
<Input
className="mt-2"
value={partnerImgUrl}
onChange={(e) => setPartnerImgUrl(e.target.value)}
placeholder="https://..."
className="mt-2 cursor-pointer"
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
onChange={(e) => {
const f = e.target.files?.[0] ?? null;
setPickedFile(f, partnerBlobUrlRef, setPartnerPendingFile);
e.target.value = "";
}}
/>
{partnerBlobUrlRef.current || partnerRemoteUrl ? (
<div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={partnerBlobUrlRef.current || partnerRemoteUrl}
alt=""
className="max-h-24 max-w-full object-contain"
/>
</div>
) : null}
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
@ -1483,14 +1705,28 @@ export default function ContentWebsite() {
</div>
<div>
<label className="text-sm font-medium text-slate-700">
Banner image URL (optional)
Banner image (optional)
</label>
<Input
className="mt-2"
value={popupImgUrl}
onChange={(e) => setPopupImgUrl(e.target.value)}
placeholder="https://..."
className="mt-2 cursor-pointer"
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
onChange={(e) => {
const f = e.target.files?.[0] ?? null;
setPickedFile(f, popupBlobUrlRef, setPopupPendingFile);
e.target.value = "";
}}
/>
{popupBlobUrlRef.current || popupRemoteUrl ? (
<div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-3">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={popupBlobUrlRef.current || popupRemoteUrl}
alt=""
className="max-h-36 object-contain"
/>
</div>
) : null}
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">

View File

@ -1,7 +1,9 @@
import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPostFormDataInterceptor,
httpPostInterceptor,
httpPutFormDataInterceptor,
httpPutInterceptor,
} from "./http-config/http-interceptor-services";
@ -66,6 +68,11 @@ export async function saveHeroImage(body: {
return await httpPostInterceptor("/hero-content-images", body);
}
/** Multipart: fields `hero_content_id`, `file` */
export async function saveHeroImageUpload(formData: FormData) {
return await httpPostFormDataInterceptor("/hero-content-images", formData);
}
export async function updateHeroImage(
id: string,
body: { image_path?: string; image_url?: string },
@ -73,6 +80,11 @@ export async function updateHeroImage(
return await httpPutInterceptor(`/hero-content-images/${id}`, body);
}
/** Multipart: field `file` */
export async function updateHeroImageUpload(id: string, formData: FormData) {
return await httpPutFormDataInterceptor(`/hero-content-images/${id}`, formData);
}
export async function getAboutContentsList() {
return await httpGetInterceptor("/about-us-contents");
}
@ -96,6 +108,11 @@ export async function saveAboutUsMediaUrl(body: {
return await httpPostInterceptor("/about-us-content-images/url", body);
}
/** Multipart: fields `about_us_content_id`, `file` (image or mp4/webm) */
export async function saveAboutUsMediaUpload(formData: FormData) {
return await httpPostFormDataInterceptor("/about-us-content-images", formData);
}
export async function deleteAboutUsContentImage(id: number) {
return await httpDeleteInterceptor(`/about-us-content-images/${id}`);
}
@ -141,6 +158,11 @@ export async function saveOurProductImage(body: {
return await httpPostInterceptor("/our-product-content-images", body);
}
/** Multipart: fields `our_product_content_id`, `file` */
export async function saveOurProductImageUpload(formData: FormData) {
return await httpPostFormDataInterceptor("/our-product-content-images", formData);
}
export async function updateOurProductImage(
id: string,
body: { image_path?: string; image_url?: string },
@ -148,6 +170,11 @@ export async function updateOurProductImage(
return await httpPutInterceptor(`/our-product-content-images/${id}`, body);
}
/** Multipart: field `file` */
export async function updateOurProductImageUpload(id: string, formData: FormData) {
return await httpPutFormDataInterceptor(`/our-product-content-images/${id}`, formData);
}
export async function getOurServiceContent() {
return await httpGetInterceptor("/our-service-contents");
}
@ -189,6 +216,11 @@ export async function saveOurServiceImage(body: {
return await httpPostInterceptor("/our-service-content-images", body);
}
/** Multipart: fields `our_service_content_id`, `file` */
export async function saveOurServiceImageUpload(formData: FormData) {
return await httpPostFormDataInterceptor("/our-service-content-images", formData);
}
export async function updateOurServiceImage(
id: string,
body: { image_path?: string; image_url?: string },
@ -196,6 +228,11 @@ export async function updateOurServiceImage(
return await httpPutInterceptor(`/our-service-content-images/${id}`, body);
}
/** Multipart: field `file` */
export async function updateOurServiceImageUpload(id: string, formData: FormData) {
return await httpPutFormDataInterceptor(`/our-service-content-images/${id}`, formData);
}
export async function getPartnerContents() {
return await httpGetInterceptor("/partner-contents");
}
@ -219,6 +256,11 @@ export async function updatePartnerContent(
return await httpPutInterceptor(`/partner-contents/${id}`, body);
}
/** Multipart: field `file` — updates logo in MinIO */
export async function uploadPartnerLogo(id: string, formData: FormData) {
return await httpPostFormDataInterceptor(`/partner-contents/${id}/logo`, formData);
}
export async function deletePartnerContent(id: string) {
return await httpDeleteInterceptor(`/partner-contents/${id}`);
}
@ -265,6 +307,11 @@ export async function savePopupNewsImage(body: {
return await httpPostInterceptor("/popup-news-content-images", body);
}
/** Multipart: fields `popup_news_content_id`, `file`; optional `is_thumbnail` */
export async function savePopupNewsImageUpload(formData: FormData) {
return await httpPostFormDataInterceptor("/popup-news-content-images", formData);
}
export async function deletePopupNews(id: number) {
return await httpDeleteInterceptor(`/popup-news-contents/${id}`);
}

View File

@ -17,6 +17,10 @@ const axiosInterceptorInstance = axios.create({
// Request interceptor
axiosInterceptorInstance.interceptors.request.use(
(config) => {
// Default instance uses application/json; FormData must not use JSON or File becomes {}.
if (config.data instanceof FormData) {
config.headers.delete("Content-Type");
}
console.log("Config interceptor : ", config);
const accessToken = Cookies.get("access_token");
if (accessToken) {

View File

@ -35,6 +35,74 @@ export async function httpGetInterceptor(pathUrl: any) {
}
}
const clientKeyHeaders = {
"X-Client-Key": defaultHeaders["X-Client-Key"],
};
/** POST multipart (do not set Content-Type — browser sets boundary). */
export async function httpPostFormDataInterceptor(pathUrl: string, formData: FormData) {
const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.csrf_token;
const mergedHeaders: Record<string, string> = {
...clientKeyHeaders,
...(csrfToken ? { "X-CSRF-TOKEN": csrfToken } : {}),
};
const response = await axiosInterceptorInstance
.post(pathUrl, formData, { headers: mergedHeaders })
.catch((error) => error.response);
console.log("Response interceptor : ", response);
if (response?.status == 200 || response?.status == 201) {
return {
error: false,
message: "success",
data: response?.data,
};
} else if (response?.status == 401) {
Cookies.set("is_logout", "true");
window.location.href = "/";
} else {
return {
error: true,
message: response?.data?.message || response?.data || null,
data: null,
};
}
}
/** PUT multipart (do not set Content-Type — browser sets boundary). */
export async function httpPutFormDataInterceptor(pathUrl: string, formData: FormData) {
const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.csrf_token;
const mergedHeaders: Record<string, string> = {
...clientKeyHeaders,
...(csrfToken ? { "X-CSRF-TOKEN": csrfToken } : {}),
};
const response = await axiosInterceptorInstance
.put(pathUrl, formData, { headers: mergedHeaders })
.catch((error) => error.response);
console.log("Response interceptor : ", response);
if (response?.status == 200 || response?.status == 201) {
return {
error: false,
message: "success",
data: response?.data,
};
} else if (response?.status == 401) {
Cookies.set("is_logout", "true");
window.location.href = "/";
} else {
return {
error: true,
message: response?.data?.message || response?.data || null,
data: null,
};
}
}
export async function httpPostInterceptor(
pathUrl: any,
data: any,