Adjust product state actions and icon font loading

This commit is contained in:
2026-05-10 20:25:43 +07:00
parent d8a0b9b05e
commit 9fb4cd2aa7
6 changed files with 171 additions and 43 deletions

View File

@ -30,6 +30,8 @@ interface ProductRow {
totalStock: number;
}
type ProductState = "UNPUBLISHED" | "DELETED_BY_SELLER" | "DELETED_BY_ADMIN" | string;
function getToken() {
if (typeof window === "undefined") {
return "";
@ -184,6 +186,8 @@ function ProductsPageInner() {
const [unpublishTarget, setUnpublishTarget] = useState<ProductRow | null>(null);
const [deleting, setDeleting] = useState(false);
const [unpublishing, setUnpublishing] = useState(false);
const [publishingId, setPublishingId] = useState<string | null>(null);
const [restoringId, setRestoringId] = useState<string | null>(null);
// Reset to page 1 when tab changes
useEffect(() => {
@ -263,6 +267,7 @@ function ProductsPageInner() {
}
async function handleRestore(productId: string) {
setRestoringId(productId);
try {
const res = await fetch(`/api/products/${productId}?action=restore`, {
method: "PUT",
@ -277,9 +282,37 @@ function ProductsPageInner() {
window.location.reload();
} catch (err) {
alert(err instanceof Error ? err.message : p.restoreError);
} finally {
setRestoringId(null);
}
}
async function handlePublish(productId: string) {
setPublishingId(productId);
try {
const res = await fetch(`/api/products/${productId}?action=publish`, {
method: "PUT",
headers: { "x-auth-token": getToken() },
});
const result = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(result?.responseDesc || p.publishError);
}
window.location.reload();
} catch (err) {
alert(err instanceof Error ? err.message : p.publishError);
}
finally {
setPublishingId(null);
}
}
function getProductState(product: ProductRow): ProductState {
return (product.state || product.status || product.reviewStatus || "").toUpperCase();
}
const internationalCount = rows.filter(
(row) => row.market === "International"
).length;
@ -411,6 +444,12 @@ function ProductsPageInner() {
</tr>
) : (
rows.map((product) => {
const productState = getProductState(product);
const isInactiveInAllTab =
activeTab === "All Product" &&
(productState === "UNPUBLISHED" ||
productState === "DELETED_BY_SELLER" ||
productState === "DELETED_BY_ADMIN");
const stockTone =
product.totalStock === 0
? "red"
@ -441,7 +480,7 @@ function ProductsPageInner() {
fill
sizes="44px"
className={`object-cover ${
stockTone === "red" ? "grayscale" : ""
stockTone === "red" || isInactiveInAllTab ? "grayscale" : ""
}`}
/>
) : (
@ -453,7 +492,7 @@ function ProductsPageInner() {
<div>
<p
className={`text-xs font-bold transition-colors group-hover:text-primary ${
stockTone === "red"
stockTone === "red" || isInactiveInAllTab
? "text-outline line-through"
: "text-on-surface"
}`}
@ -469,7 +508,7 @@ function ProductsPageInner() {
<td className="px-6 py-4 text-center">
<span
className={`text-xs font-bold ${
stockTone === "red"
stockTone === "red" || isInactiveInAllTab
? "text-outline"
: "text-on-surface"
}`}
@ -503,39 +542,64 @@ function ProductsPageInner() {
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[9px] font-black uppercase tracking-tight ${marketClasses(
product.market
)}`}
)} ${isInactiveInAllTab ? "opacity-60 line-through" : ""}`}
>
{product.market}
</span>
</td>
<td className="px-6 py-4 text-center">
<div className="flex items-center justify-center gap-3">
{!isInReviewTab ? (
!isDeletedTab ? (
<Link
href={`/products/${product.id}/edit${activeTab === "Draft" ? "?draft=1" : ""}`}
className="rounded-lg bg-primary px-3 py-1 text-[10px] font-bold text-white transition-colors hover:bg-primary/90"
>
{p.edit}
</Link>
) : null
) : null}
<Link
href={`/products/${product.id}/detail${activeTab === "Draft" ? "?draft=1" : isInReviewTab ? "?review=1" : ""}`}
className="text-[10px] font-bold text-primary transition-colors hover:underline"
>
{p.detail}
</Link>
{isDeletedTab ? (
productState === "DELETED_BY_ADMIN" ? (
<span className="rounded-full bg-error-container px-3 py-1 text-[10px] font-bold text-on-error-container">
{p.deletedByAdmin}
</span>
) : (
<button
type="button"
onClick={() => handleRestore(product.id)}
disabled={restoringId === product.id}
className="rounded-lg bg-tertiary px-3 py-1 text-[10px] font-bold text-white transition-colors hover:bg-tertiary/90 disabled:opacity-60"
>
{restoringId === product.id ? p.publishing : p.restore}
</button>
)
) : productState === "UNPUBLISHED" ? (
<button
type="button"
onClick={() => handlePublish(product.id)}
disabled={publishingId === product.id}
className="rounded-lg bg-tertiary px-3 py-1 text-[10px] font-bold text-white transition-colors hover:bg-tertiary/90 disabled:opacity-60"
>
{publishingId === product.id ? p.publishing : p.publish}
</button>
) : productState === "DELETED_BY_SELLER" ? (
<button
type="button"
onClick={() => handleRestore(product.id)}
className="rounded-lg bg-tertiary px-3 py-1 text-[10px] font-bold text-white transition-colors hover:bg-tertiary/90"
disabled={restoringId === product.id}
className="rounded-lg bg-tertiary px-3 py-1 text-[10px] font-bold text-white transition-colors hover:bg-tertiary/90 disabled:opacity-60"
>
{p.restore}
{restoringId === product.id ? p.publishing : p.restore}
</button>
) : (
<>
{!isInReviewTab ? (
!isDeletedTab ? (
<Link
href={`/products/${product.id}/edit${activeTab === "Draft" ? "?draft=1" : ""}`}
className="rounded-lg bg-primary px-3 py-1 text-[10px] font-bold text-white transition-colors hover:bg-primary/90"
>
{p.edit}
</Link>
) : null
) : null}
<Link
href={`/products/${product.id}/detail${activeTab === "Draft" ? "?draft=1" : isInReviewTab ? "?review=1" : ""}`}
className="text-[10px] font-bold text-primary transition-colors hover:underline"
>
{p.detail}
</Link>
{canUnpublish ? (
<button
type="button"

View File

@ -12,6 +12,9 @@ interface ProductRow {
minPrice: number | null;
maxPrice: number | null;
totalStock: number | null;
state?: string | null;
status?: string | null;
reviewStatus?: string | null;
}
function getToken() {
@ -32,6 +35,10 @@ function marketBadge(market: string | null) {
return "bg-tertiary-fixed text-on-tertiary-fixed";
}
function getProductState(product: ProductRow) {
return (product.state || product.status || product.reviewStatus || "").toUpperCase();
}
function ConfirmModal({
title,
message,
@ -124,6 +131,7 @@ function AdminProductsPageInner() {
const [restoreTarget, setRestoreTarget] = useState<ProductRow | null>(null);
const [processingDelete, setProcessingDelete] = useState(false);
const [processingRestore, setProcessingRestore] = useState(false);
const [publishingId, setPublishingId] = useState<string | null>(null);
const pageSize = 20;
useEffect(() => {
@ -204,6 +212,25 @@ function AdminProductsPageInner() {
}
}
async function handlePublish(productId: string) {
setPublishingId(productId);
try {
const res = await fetch(`/api/products/${productId}?action=publish`, {
method: "PUT",
headers: { "x-auth-token": getToken() },
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data?.responseDesc || "Gagal publish produk");
}
window.location.reload();
} catch (err) {
alert(err instanceof Error ? err.message : "Gagal publish produk");
} finally {
setPublishingId(null);
}
}
return (
<div className="space-y-8">
<div className="flex items-end justify-between">
@ -271,23 +298,36 @@ function AdminProductsPageInner() {
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{rows.map((product) => (
{rows.map((product) => {
const productState = getProductState(product);
const isInactiveInAllTab =
!isDeletedTab &&
(productState === "UNPUBLISHED" ||
productState === "DELETED_BY_SELLER" ||
productState === "DELETED_BY_ADMIN");
return (
<tr key={product.id} className="group hover:bg-surface-container-low transition-colors">
<td className="px-6 py-5">
<div>
<p className="font-extrabold text-on-surface transition-colors group-hover:text-primary">
<p
className={`font-extrabold transition-colors group-hover:text-primary ${
isInactiveInAllTab ? "text-slate-400 line-through" : "text-on-surface"
}`}
>
{product.name}
</p>
<p className="mt-1 text-[10px] font-medium text-slate-400">ID: {product.id.slice(0, 8)}</p>
</div>
</td>
<td className="px-6 py-5">
<span className={`inline-flex rounded-full px-3 py-1 text-[10px] font-black uppercase tracking-wider ${marketBadge(product.market)}`}>
<span className={`inline-flex rounded-full px-3 py-1 text-[10px] font-black uppercase tracking-wider ${marketBadge(product.market)} ${isInactiveInAllTab ? "line-through opacity-60" : ""}`}>
{product.market || "—"}
</span>
</td>
<td className="px-6 py-5">
<span className="font-bold text-on-surface">{formatPrice(product.minPrice, product.maxPrice)}</span>
<span className={isInactiveInAllTab ? "font-bold text-slate-400 line-through" : "font-bold text-on-surface"}>
{formatPrice(product.minPrice, product.maxPrice)}
</span>
</td>
<td className="px-6 py-5 text-center">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-sm font-bold text-on-surface">
@ -296,15 +336,6 @@ function AdminProductsPageInner() {
</td>
<td className="px-6 py-5">
<div className="flex justify-end gap-2">
{!isDeletedTab ? (
<Link
href={`/admin/products/${product.id}`}
className="rounded-xl px-4 py-2 text-[10px] font-black uppercase tracking-widest text-primary transition-colors hover:bg-primary/5"
>
Detail
</Link>
) : null}
{isDeletedTab ? (
<button
type="button"
@ -313,19 +344,44 @@ function AdminProductsPageInner() {
>
Restore
</button>
) : (
) : productState === "UNPUBLISHED" ? (
<button
type="button"
onClick={() => setDeleteTarget(product)}
className="rounded-xl bg-error px-4 py-2 text-[10px] font-black uppercase tracking-widest text-white transition-colors hover:bg-error/90"
onClick={() => handlePublish(product.id)}
disabled={publishingId === product.id}
className="rounded-xl bg-tertiary px-4 py-2 text-[10px] font-black uppercase tracking-widest text-white transition-colors hover:bg-tertiary/90 disabled:opacity-60"
>
Delete
{publishingId === product.id ? "Processing..." : "Publish"}
</button>
) : productState === "DELETED_BY_SELLER" ? (
<button
type="button"
onClick={() => setRestoreTarget(product)}
className="rounded-xl bg-tertiary px-4 py-2 text-[10px] font-black uppercase tracking-widest text-white transition-colors hover:bg-tertiary/90"
>
Restore
</button>
) : (
<>
<Link
href={`/admin/products/${product.id}`}
className="rounded-xl px-4 py-2 text-[10px] font-black uppercase tracking-widest text-primary transition-colors hover:bg-primary/5"
>
Detail
</Link>
<button
type="button"
onClick={() => setDeleteTarget(product)}
className="rounded-xl bg-error px-4 py-2 text-[10px] font-black uppercase tracking-widest text-white transition-colors hover:bg-error/90"
>
Delete
</button>
</>
)}
</div>
</td>
</tr>
))}
)})}
</tbody>
</table>
</div>

View File

@ -41,11 +41,13 @@ export async function PUT(
const isDraft = req.nextUrl.searchParams.get("draft") === "1";
const action = req.nextUrl.searchParams.get("action");
if (action === "unpublish" || action === "restore") {
if (action === "unpublish" || action === "restore" || action === "publish") {
const endpoint =
action === "unpublish"
? `${API_URL}/api/v1.0/seller/product/${productId}/unpublish`
: `${API_URL}/api/v1.0/seller/product/${productId}/restore`;
: action === "restore"
? `${API_URL}/api/v1.0/seller/product/${productId}/restore`
: `${API_URL}/api/v1.0/seller/product/${productId}/publish`;
const res = await fetch(endpoint, {
method: "PUT",

View File

@ -56,7 +56,7 @@ export default function RootLayout({
{/* eslint-disable-next-line @next/next/no-page-custom-font */}
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=optional"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block"
/>
</head>
<body className="min-h-screen bg-background text-on-surface font-body antialiased">

View File

@ -358,8 +358,10 @@ export const en = {
empty: "No products found.",
edit: "Edit",
detail: "Detail",
publish: "Publish",
restore: "Restore",
unpublish: "Unpublish",
deletedByAdmin: "Deleted by admin",
table: {
product: "Product",
price: "Price",
@ -389,6 +391,7 @@ export const en = {
errorGeneric: "Failed to unpublish product",
},
restoreError: "Failed to restore product",
publishError: "Failed to publish product",
tabs: {
allProduct: "All Product",
draft: "Draft",

View File

@ -359,8 +359,10 @@ export const id = {
empty: "Tidak ada produk ditemukan.",
edit: "Edit",
detail: "Detail",
publish: "Publish",
restore: "Restore",
unpublish: "Unpublish",
deletedByAdmin: "Dihapus oleh admin",
table: {
product: "Produk",
price: "Harga",
@ -390,6 +392,7 @@ export const id = {
errorGeneric: "Gagal unpublish produk",
},
restoreError: "Gagal restore produk",
publishError: "Gagal publish produk",
tabs: {
allProduct: "Semua Produk",
draft: "Draft",