feat: update content popup
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
2eaa34d052
commit
6176567557
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue