feat: fixing conten website
continuous-integration/drone/push Build is passing Details

This commit is contained in:
hanif salafi 2026-04-14 11:29:43 +07:00
parent 71c65f9a60
commit 407f1474b1
2 changed files with 157 additions and 173 deletions

View File

@ -68,6 +68,10 @@ import {
uploadPartnerLogo, uploadPartnerLogo,
} from "@/service/cms-landing"; } from "@/service/cms-landing";
import { submitCmsContentSubmission } from "@/service/cms-content-submissions"; import { submitCmsContentSubmission } from "@/service/cms-content-submissions";
import {
parseMediaLibraryUploadPublicUrl,
uploadMediaLibraryFile,
} from "@/service/media-library";
function revokeBlobRef(ref: MutableRefObject<string | null>) { function revokeBlobRef(ref: MutableRefObject<string | null>) {
if (ref.current) { if (ref.current) {
@ -88,6 +92,26 @@ function setPickedFile(
setter(file); setter(file);
} }
/** Upload to Media Library; returns public URL for CMS submission payloads (contributor). */
async function uploadContributorAssetToPublicUrl(
file: File,
): Promise<string | null> {
const fd = new FormData();
fd.append("file", file);
const res = await uploadMediaLibraryFile(fd);
const url = parseMediaLibraryUploadPublicUrl(res);
if (url) return url;
await Swal.fire({
icon: "error",
title: "Unggah berkas gagal",
text: String(
(res as { message?: unknown })?.message ??
"Periksa koneksi dan akses Media Library, lalu coba lagi.",
),
});
return null;
}
type ContentWebsiteProps = { type ContentWebsiteProps = {
/** User level 2: changes go through approval instead of live CMS APIs. */ /** User level 2: changes go through approval instead of live CMS APIs. */
contributorMode?: boolean; contributorMode?: boolean;
@ -303,16 +327,14 @@ export default function ContentWebsite({
return; return;
} }
if (contributorMode && editMode) { if (contributorMode && editMode) {
if (heroPendingFile) {
await Swal.fire({
icon: "info",
title: "Gunakan URL gambar",
text: "Sebagai kontributor, unggah file tidak didukung. Salin URL dari Media Library ke bidang Image URL.",
});
return;
}
setSaving(true); setSaving(true);
try { try {
let imageUrl = (heroRemoteUrl ?? "").trim();
if (heroPendingFile) {
const up = await uploadContributorAssetToPublicUrl(heroPendingFile);
if (!up) return;
imageUrl = up;
}
const res = await submitCmsContentSubmission({ const res = await submitCmsContentSubmission({
domain: "hero", domain: "hero",
title: `Hero: ${heroPrimary.slice(0, 80)}`, title: `Hero: ${heroPrimary.slice(0, 80)}`,
@ -324,7 +346,7 @@ export default function ContentWebsite({
description: heroDesc, description: heroDesc,
primary_cta: heroCta1, primary_cta: heroCta1,
secondary_cta_text: heroCta2, secondary_cta_text: heroCta2,
image_url: (heroRemoteUrl ?? "").trim(), image_url: imageUrl,
}, },
}); });
if ((res as { error?: boolean })?.error) { if ((res as { error?: boolean })?.error) {
@ -335,6 +357,9 @@ export default function ContentWebsite({
}); });
return; return;
} }
revokeBlobRef(heroBlobUrlRef);
setHeroPendingFile(null);
if (imageUrl) setHeroRemoteUrl(imageUrl);
await Swal.fire({ await Swal.fire({
icon: "success", icon: "success",
title: "Diajukan untuk persetujuan", title: "Diajukan untuk persetujuan",
@ -407,16 +432,14 @@ export default function ContentWebsite({
return; return;
} }
if (contributorMode && editMode) { if (contributorMode && editMode) {
if (aboutPendingFile) {
await Swal.fire({
icon: "info",
title: "Gunakan URL media",
text: "Sebagai kontributor, unggah file tidak didukung. Gunakan URL dari Media Library.",
});
return;
}
setSaving(true); setSaving(true);
try { try {
let mediaUrl = (aboutRemoteMediaUrl ?? "").trim();
if (aboutPendingFile) {
const up = await uploadContributorAssetToPublicUrl(aboutPendingFile);
if (!up) return;
mediaUrl = up;
}
const res = await submitCmsContentSubmission({ const res = await submitCmsContentSubmission({
domain: "about", domain: "about",
title: `About: ${aboutPrimary.slice(0, 80)}`, title: `About: ${aboutPrimary.slice(0, 80)}`,
@ -428,7 +451,7 @@ export default function ContentWebsite({
description: aboutDesc, description: aboutDesc,
primary_cta: aboutCta1, primary_cta: aboutCta1,
secondary_cta_text: aboutCta2, secondary_cta_text: aboutCta2,
media_url: (aboutRemoteMediaUrl ?? "").trim(), media_url: mediaUrl,
}, },
}); });
if ((res as { error?: boolean })?.error) { if ((res as { error?: boolean })?.error) {
@ -439,6 +462,9 @@ export default function ContentWebsite({
}); });
return; return;
} }
revokeBlobRef(aboutBlobUrlRef);
setAboutPendingFile(null);
if (mediaUrl) setAboutRemoteMediaUrl(mediaUrl);
await Swal.fire({ await Swal.fire({
icon: "success", icon: "success",
title: "Diajukan untuk persetujuan", title: "Diajukan untuk persetujuan",
@ -545,16 +571,14 @@ export default function ContentWebsite({
return; return;
} }
if (contributorMode && editMode) { if (contributorMode && editMode) {
if (productPendingFile) {
await Swal.fire({
icon: "info",
title: "Gunakan URL gambar",
text: "Sebagai kontributor, gunakan URL dari Media Library untuk gambar produk.",
});
return;
}
setSaving(true); setSaving(true);
try { try {
let imageUrl = (productRemoteUrl ?? "").trim();
if (productPendingFile) {
const up = await uploadContributorAssetToPublicUrl(productPendingFile);
if (!up) return;
imageUrl = up;
}
const res = await submitCmsContentSubmission({ const res = await submitCmsContentSubmission({
domain: "product", domain: "product",
title: `Product: ${productPrimary.slice(0, 80)}`, title: `Product: ${productPrimary.slice(0, 80)}`,
@ -565,7 +589,7 @@ export default function ContentWebsite({
secondary_title: productSecondary, secondary_title: productSecondary,
description: productDesc, description: productDesc,
link_url: productLinkUrl, link_url: productLinkUrl,
image_url: (productRemoteUrl ?? "").trim(), image_url: imageUrl,
}, },
}); });
if ((res as { error?: boolean })?.error) { if ((res as { error?: boolean })?.error) {
@ -576,6 +600,9 @@ export default function ContentWebsite({
}); });
return; return;
} }
revokeBlobRef(productBlobUrlRef);
setProductPendingFile(null);
if (imageUrl) setProductRemoteUrl(imageUrl);
await Swal.fire({ await Swal.fire({
icon: "success", icon: "success",
title: "Diajukan untuk persetujuan", title: "Diajukan untuk persetujuan",
@ -736,16 +763,14 @@ export default function ContentWebsite({
return; return;
} }
if (contributorMode && editMode) { if (contributorMode && editMode) {
if (servicePendingFile) {
await Swal.fire({
icon: "info",
title: "Gunakan URL gambar",
text: "Sebagai kontributor, gunakan URL dari Media Library untuk gambar layanan.",
});
return;
}
setSaving(true); setSaving(true);
try { try {
let imageUrl = (serviceRemoteUrl ?? "").trim();
if (servicePendingFile) {
const up = await uploadContributorAssetToPublicUrl(servicePendingFile);
if (!up) return;
imageUrl = up;
}
const res = await submitCmsContentSubmission({ const res = await submitCmsContentSubmission({
domain: "service", domain: "service",
title: `Service: ${servicePrimary.slice(0, 80)}`, title: `Service: ${servicePrimary.slice(0, 80)}`,
@ -756,7 +781,7 @@ export default function ContentWebsite({
secondary_title: serviceSecondary, secondary_title: serviceSecondary,
description: serviceDesc, description: serviceDesc,
link_url: serviceLinkUrl, link_url: serviceLinkUrl,
image_url: (serviceRemoteUrl ?? "").trim(), image_url: imageUrl,
}, },
}); });
if ((res as { error?: boolean })?.error) { if ((res as { error?: boolean })?.error) {
@ -767,6 +792,9 @@ export default function ContentWebsite({
}); });
return; return;
} }
revokeBlobRef(serviceBlobUrlRef);
setServicePendingFile(null);
if (imageUrl) setServiceRemoteUrl(imageUrl);
await Swal.fire({ await Swal.fire({
icon: "success", icon: "success",
title: "Diajukan untuk persetujuan", title: "Diajukan untuk persetujuan",
@ -919,24 +947,22 @@ export default function ContentWebsite({
return; return;
} }
if (contributorMode && editMode) { if (contributorMode && editMode) {
if (partnerPendingFile) {
await Swal.fire({
icon: "info",
title: "Gunakan URL logo",
text: "Sebagai kontributor, gunakan URL logo dari Media Library (isi URL gambar).",
});
return;
}
setSaving(true); setSaving(true);
try { try {
let imageUrl = (partnerRemoteUrl ?? "").trim();
if (partnerPendingFile) {
const up = await uploadContributorAssetToPublicUrl(partnerPendingFile);
if (!up) return;
imageUrl = up;
}
const res = await submitCmsContentSubmission({ const res = await submitCmsContentSubmission({
domain: "partner", domain: "partner",
title: `Partner: ${partnerTitle.slice(0, 80)}`, title: `Partner: ${partnerTitle.slice(0, 80)}`,
payload: { payload: {
partner_id: editingPartnerId ?? "", partner_id: editingPartnerId ?? "",
primary_title: partnerTitle.trim(), primary_title: partnerTitle.trim(),
image_path: partnerStoredPath, image_path: partnerPendingFile ? "" : partnerStoredPath,
image_url: (partnerRemoteUrl ?? "").trim(), image_url: imageUrl,
}, },
}); });
if ((res as { error?: boolean })?.error) { if ((res as { error?: boolean })?.error) {
@ -947,6 +973,9 @@ export default function ContentWebsite({
}); });
return; return;
} }
revokeBlobRef(partnerBlobUrlRef);
setPartnerPendingFile(null);
if (imageUrl) setPartnerRemoteUrl(imageUrl);
await Swal.fire({ await Swal.fire({
icon: "success", icon: "success",
title: "Diajukan untuk persetujuan", title: "Diajukan untuk persetujuan",
@ -1101,16 +1130,14 @@ export default function ContentWebsite({
return; return;
} }
if (contributorMode && editMode) { if (contributorMode && editMode) {
if (popupPendingFile) {
await Swal.fire({
icon: "info",
title: "Gunakan URL gambar",
text: "Sebagai kontributor, gunakan URL banner dari Media Library.",
});
return;
}
setSaving(true); setSaving(true);
try { try {
let mediaUrl = (popupRemoteUrl ?? "").trim();
if (popupPendingFile) {
const up = await uploadContributorAssetToPublicUrl(popupPendingFile);
if (!up) return;
mediaUrl = up;
}
const res = await submitCmsContentSubmission({ const res = await submitCmsContentSubmission({
domain: "popup", domain: "popup",
title: `Pop-up: ${popupPrimary.slice(0, 80)}`, title: `Pop-up: ${popupPrimary.slice(0, 80)}`,
@ -1121,7 +1148,7 @@ export default function ContentWebsite({
description: popupDesc, description: popupDesc,
primary_cta: popupCta1, primary_cta: popupCta1,
secondary_cta_text: popupCta2, secondary_cta_text: popupCta2,
media_url: (popupRemoteUrl ?? "").trim(), media_url: mediaUrl,
}, },
}); });
if ((res as { error?: boolean })?.error) { if ((res as { error?: boolean })?.error) {
@ -1132,6 +1159,9 @@ export default function ContentWebsite({
}); });
return; return;
} }
revokeBlobRef(popupBlobUrlRef);
setPopupPendingFile(null);
if (mediaUrl) setPopupRemoteUrl(mediaUrl);
await Swal.fire({ await Swal.fire({
icon: "success", icon: "success",
title: "Diajukan untuk persetujuan", title: "Diajukan untuk persetujuan",
@ -1325,8 +1355,8 @@ export default function ContentWebsite({
{contributorMode && !editMode && !viewOnly ? ( {contributorMode && !editMode && !viewOnly ? (
<p className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900"> <p className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
Aktifkan <strong>Edit Mode</strong> untuk mengusulkan perubahan. Perubahan akan masuk ke{" "} Aktifkan <strong>Edit Mode</strong> untuk mengusulkan perubahan. Perubahan akan masuk ke{" "}
<strong>My Content</strong> menunggu persetujuan approver. Unggah file gambar tidak tersedia sebagai kontributor; <strong>My Content</strong> menunggu persetujuan approver. Gambar dan media dapat diunggah dari perangkat Anda
gunakan URL dari Media Library pada bidang yang disediakan. (disimpan ke Media Library saat Anda menyimpan tab); website live baru berubah setelah disetujui.
</p> </p>
) : null} ) : null}
@ -1405,29 +1435,19 @@ export default function ContentWebsite({
<label className="text-sm font-medium text-slate-700">Hero image</label> <label className="text-sm font-medium text-slate-700">Hero image</label>
<p className="mb-2 text-xs text-slate-500"> <p className="mb-2 text-xs text-slate-500">
{contributorMode {contributorMode
? "Tempel URL gambar dari Media Library (kontributor tidak dapat mengunggah file langsung)." ? "Unggah JPG, PNG, GIF, atau WebP. Saat menyimpan, berkas diunggah ke Media Library dan dilampirkan ke pengajuan."
: "Upload a JPG, PNG, GIF, or WebP file. Stored in MinIO and shown on the landing hero."} : "Upload a JPG, PNG, GIF, or WebP file. Stored in MinIO and shown on the landing hero."}
</p> </p>
{contributorMode ? ( <Input
<Input className="mt-1 cursor-pointer"
className="mt-1" type="file"
type="url" accept="image/jpeg,image/png,image/gif,image/webp"
placeholder="https://…" onChange={(e) => {
value={heroRemoteUrl} const f = e.target.files?.[0] ?? null;
onChange={(e) => setHeroRemoteUrl(e.target.value)} setPickedFile(f, heroBlobUrlRef, setHeroPendingFile);
/> e.target.value = "";
) : ( }}
<Input />
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 = "";
}}
/>
)}
{heroBlobUrlRef.current || heroRemoteUrl ? ( {heroBlobUrlRef.current || heroRemoteUrl ? (
<div className="mt-4 flex justify-center rounded-xl border bg-slate-50 p-6"> <div className="mt-4 flex justify-center rounded-xl border bg-slate-50 p-6">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
@ -1515,29 +1535,19 @@ export default function ContentWebsite({
<label className="text-sm font-medium text-slate-700">Media (image or video)</label> <label className="text-sm font-medium text-slate-700">Media (image or video)</label>
<p className="mb-2 text-xs text-slate-500"> <p className="mb-2 text-xs text-slate-500">
{contributorMode {contributorMode
? "Tempel URL gambar atau video dari Media Library." ? "Unggah gambar atau video (MP4/WebM). Saat menyimpan, berkas diunggah ke Media Library dan dilampirkan ke pengajuan."
: "Upload JPG/PNG/GIF/WebP or MP4/WebM. Stored in MinIO; 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> </p>
{contributorMode ? ( <Input
<Input className="mt-1 cursor-pointer"
className="mt-1" type="file"
type="url" accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm"
placeholder="https://…" onChange={(e) => {
value={aboutRemoteMediaUrl} const f = e.target.files?.[0] ?? null;
onChange={(e) => setAboutRemoteMediaUrl(e.target.value)} setPickedFile(f, aboutBlobUrlRef, setAboutPendingFile);
/> e.target.value = "";
) : ( }}
<Input />
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 = "";
}}
/>
)}
{aboutBlobUrlRef.current || aboutRemoteMediaUrl ? ( {aboutBlobUrlRef.current || aboutRemoteMediaUrl ? (
<div className="mt-4 flex justify-center rounded-xl border bg-slate-50 p-6"> <div className="mt-4 flex justify-center rounded-xl border bg-slate-50 p-6">
{aboutPendingFile?.type.startsWith("video/") || {aboutPendingFile?.type.startsWith("video/") ||
@ -1769,26 +1779,16 @@ export default function ContentWebsite({
</div> </div>
<div> <div>
<label className="text-sm font-medium text-slate-700">Card image</label> <label className="text-sm font-medium text-slate-700">Card image</label>
{contributorMode ? ( <Input
<Input className="mt-2 cursor-pointer"
className="mt-2" type="file"
type="url" accept="image/jpeg,image/png,image/gif,image/webp"
placeholder="Image URL from Media Library" onChange={(e) => {
value={productRemoteUrl} const f = e.target.files?.[0] ?? null;
onChange={(e) => setProductRemoteUrl(e.target.value)} setPickedFile(f, productBlobUrlRef, setProductPendingFile);
/> e.target.value = "";
) : ( }}
<Input />
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 ? ( {productBlobUrlRef.current || productRemoteUrl ? (
<div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-3"> <div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-3">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
@ -1964,26 +1964,16 @@ export default function ContentWebsite({
</div> </div>
<div> <div>
<label className="text-sm font-medium text-slate-700">Banner image</label> <label className="text-sm font-medium text-slate-700">Banner image</label>
{contributorMode ? ( <Input
<Input className="mt-2 cursor-pointer"
className="mt-2" type="file"
type="url" accept="image/jpeg,image/png,image/gif,image/webp"
placeholder="Image URL from Media Library" onChange={(e) => {
value={serviceRemoteUrl} const f = e.target.files?.[0] ?? null;
onChange={(e) => setServiceRemoteUrl(e.target.value)} setPickedFile(f, serviceBlobUrlRef, setServicePendingFile);
/> e.target.value = "";
) : ( }}
<Input />
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 ? ( {serviceBlobUrlRef.current || serviceRemoteUrl ? (
<div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-3"> <div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-3">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
@ -2129,26 +2119,16 @@ export default function ContentWebsite({
</div> </div>
<div> <div>
<label className="text-sm font-medium text-slate-700">Logo image</label> <label className="text-sm font-medium text-slate-700">Logo image</label>
{contributorMode ? ( <Input
<Input className="mt-2 cursor-pointer"
className="mt-2" type="file"
type="url" accept="image/jpeg,image/png,image/gif,image/webp"
placeholder="Logo URL from Media Library"
value={partnerRemoteUrl}
onChange={(e) => setPartnerRemoteUrl(e.target.value)}
/>
) : (
<Input
className="mt-2 cursor-pointer"
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
onChange={(e) => { onChange={(e) => {
const f = e.target.files?.[0] ?? null; const f = e.target.files?.[0] ?? null;
setPickedFile(f, partnerBlobUrlRef, setPartnerPendingFile); setPickedFile(f, partnerBlobUrlRef, setPartnerPendingFile);
e.target.value = ""; e.target.value = "";
}} }}
/> />
)}
{partnerBlobUrlRef.current || partnerRemoteUrl ? ( {partnerBlobUrlRef.current || partnerRemoteUrl ? (
<div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-4"> <div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-4">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
@ -2327,26 +2307,16 @@ export default function ContentWebsite({
<label className="text-sm font-medium text-slate-700"> <label className="text-sm font-medium text-slate-700">
Banner image (optional) Banner image (optional)
</label> </label>
{contributorMode ? ( <Input
<Input className="mt-2 cursor-pointer"
className="mt-2" type="file"
type="url" accept="image/jpeg,image/png,image/gif,image/webp"
placeholder="Image URL from Media Library" onChange={(e) => {
value={popupRemoteUrl} const f = e.target.files?.[0] ?? null;
onChange={(e) => setPopupRemoteUrl(e.target.value)} setPickedFile(f, popupBlobUrlRef, setPopupPendingFile);
/> e.target.value = "";
) : ( }}
<Input />
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 ? ( {popupBlobUrlRef.current || popupRemoteUrl ? (
<div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-3"> <div className="mt-3 flex justify-center rounded-lg border bg-slate-50 p-3">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}

View File

@ -60,6 +60,20 @@ export async function uploadMediaLibraryFile(formData: FormData) {
return await httpPostFormDataInterceptor("/media-library/upload", formData); return await httpPostFormDataInterceptor("/media-library/upload", formData);
} }
/** Reads `public_url` from upload API (`data.data.public_url` after interceptor wrap). */
export function parseMediaLibraryUploadPublicUrl(
interceptorResult: unknown,
): string | null {
if (!interceptorResult || typeof interceptorResult !== "object") return null;
const r = interceptorResult as {
error?: boolean;
data?: { data?: { public_url?: string } };
};
if (r.error) return null;
const url = r.data?.data?.public_url;
return typeof url === "string" && url.trim() ? url.trim() : null;
}
export async function registerMediaLibrary(body: { export async function registerMediaLibrary(body: {
public_url: string; public_url: string;
object_key?: string; object_key?: string;