From 617656755761f9e4ce0df68878bcfbe3b61f8be2 Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Fri, 10 Apr 2026 15:18:11 +0700 Subject: [PATCH] feat: update content popup --- components/main/content-website.tsx | 484 +++++++++++++----- service/cms-landing.ts | 47 ++ .../http-config/axios-interceptor-instance.ts | 4 + .../http-config/http-interceptor-services.ts | 68 +++ 4 files changed, 479 insertions(+), 124 deletions(-) diff --git a/components/main/content-website.tsx b/components/main/content-website.tsx index f36c5d4..a483539 100644 --- a/components/main/content-website.tsx +++ b/components/main/content-website.tsx @@ -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) { + if (ref.current) { + URL.revokeObjectURL(ref.current); + ref.current = null; + } +} + +function setPickedFile( + file: File | null, + ref: MutableRefObject, + 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(null); + const heroBlobUrlRef = useRef(null); const [aboutId, setAboutId] = useState(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(null); + const aboutBlobUrlRef = useRef(null); const [aboutMediaImageId, setAboutMediaImageId] = useState( null, ); - const [aboutMediaLoadedUrl, setAboutMediaLoadedUrl] = useState(""); const [products, setProducts] = useState([]); const [productEditId, setProductEditId] = useState(null); const [productPrimary, setProductPrimary] = useState(""); const [productSecondary, setProductSecondary] = useState(""); const [productDesc, setProductDesc] = useState(""); - const [productImgUrl, setProductImgUrl] = useState(""); + const [productRemoteUrl, setProductRemoteUrl] = useState(""); + const [productPendingFile, setProductPendingFile] = useState( + null, + ); + const productBlobUrlRef = useRef(null); const [productImageId, setProductImageId] = useState(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( + null, + ); + const serviceBlobUrlRef = useRef(null); const [serviceImageId, setServiceImageId] = useState(null); const [serviceModalOpen, setServiceModalOpen] = useState(false); const [partners, setPartners] = useState([]); const [partnerTitle, setPartnerTitle] = useState(""); - const [partnerImgUrl, setPartnerImgUrl] = useState(""); + const [partnerRemoteUrl, setPartnerRemoteUrl] = useState(""); + const [partnerStoredPath, setPartnerStoredPath] = useState(""); + const [partnerPendingFile, setPartnerPendingFile] = useState( + null, + ); + const partnerBlobUrlRef = useRef(null); const [editingPartnerId, setEditingPartnerId] = useState(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(null); + const popupBlobUrlRef = useRef(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 (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; - } + if (aid != null && aboutPendingFile) { + if (aboutMediaImageId != null) { + await deleteAboutUsContentImage(aboutMediaImageId); } + 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 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() { />
- +

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

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 ? (
{/* eslint-disable-next-line @next/next/no-img-element */} - +
) : (
-

No image URL yet

+

No hero image yet

)}
@@ -843,23 +984,27 @@ export default function ContentWebsite() { />
- +

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

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 ? (
- {/\.(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
@@ -1028,13 +1208,27 @@ export default function ContentWebsite() { />
- + 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 ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+ ) : null}
@@ -1179,13 +1373,27 @@ export default function ContentWebsite() { />
- + 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 ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+ ) : null}
@@ -1302,7 +1510,7 @@ export default function ContentWebsite() { {editingPartnerId ? "Edit partner" : "Add Partner"} - 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.
@@ -1315,13 +1523,27 @@ export default function ContentWebsite() { />
- + 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 ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+ ) : null}
@@ -1483,14 +1705,28 @@ export default function ContentWebsite() {
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 ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+ ) : null}
diff --git a/service/cms-landing.ts b/service/cms-landing.ts index 8d64f12..d6a0a60 100644 --- a/service/cms-landing.ts +++ b/service/cms-landing.ts @@ -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}`); } diff --git a/service/http-config/axios-interceptor-instance.ts b/service/http-config/axios-interceptor-instance.ts index ab21afd..6b9f002 100644 --- a/service/http-config/axios-interceptor-instance.ts +++ b/service/http-config/axios-interceptor-instance.ts @@ -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) { diff --git a/service/http-config/http-interceptor-services.ts b/service/http-config/http-interceptor-services.ts index 89c0331..38e5bef 100644 --- a/service/http-config/http-interceptor-services.ts +++ b/service/http-config/http-interceptor-services.ts @@ -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 = { + ...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 = { + ...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,