Adjust product state actions and icon font loading
This commit is contained in:
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user