Add admin category management
This commit is contained in:
512
src/app/admin/categories/page.tsx
Normal file
512
src/app/admin/categories/page.tsx
Normal file
@ -0,0 +1,512 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface CategoryRow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
interface SubCategoryRow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
subCategoryAttributes: string[];
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
if (typeof window === "undefined") return "";
|
||||
return sessionStorage.getItem("token") || localStorage.getItem("token") || "";
|
||||
}
|
||||
|
||||
function toText(value: unknown) {
|
||||
return typeof value === "string" ? value : typeof value === "number" ? String(value) : "";
|
||||
}
|
||||
|
||||
function parseCategories(payload: unknown): CategoryRow[] {
|
||||
const rows =
|
||||
Array.isArray((payload as { rows?: unknown[] })?.rows) ? (payload as { rows: unknown[] }).rows :
|
||||
Array.isArray((payload as { data?: unknown[] })?.data) ? (payload as { data: unknown[] }).data :
|
||||
Array.isArray(payload) ? payload :
|
||||
[];
|
||||
|
||||
return rows
|
||||
.map((item) => {
|
||||
const row = item as Record<string, unknown>;
|
||||
const id = toText(row.id);
|
||||
const name = toText(row.name);
|
||||
if (!id || !name) return null;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description: toText(row.description) || null,
|
||||
};
|
||||
})
|
||||
.filter((item): item is CategoryRow => Boolean(item));
|
||||
}
|
||||
|
||||
function parseSubCategories(payload: unknown): SubCategoryRow[] {
|
||||
const rows =
|
||||
Array.isArray((payload as { rows?: unknown[] })?.rows) ? (payload as { rows: unknown[] }).rows :
|
||||
Array.isArray((payload as { data?: unknown[] })?.data) ? (payload as { data: unknown[] }).data :
|
||||
Array.isArray(payload) ? payload :
|
||||
[];
|
||||
|
||||
return rows
|
||||
.map((item) => {
|
||||
const row = item as Record<string, unknown>;
|
||||
const id = toText(row.id);
|
||||
const name = toText(row.name);
|
||||
if (!id || !name) return null;
|
||||
|
||||
const attributes = Array.isArray(row.subCategoryAttributes)
|
||||
? row.subCategoryAttributes.map((attr) => toText(attr)).filter(Boolean)
|
||||
: [];
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description: toText(row.description) || null,
|
||||
subCategoryAttributes: attributes,
|
||||
};
|
||||
})
|
||||
.filter((item): item is SubCategoryRow => Boolean(item));
|
||||
}
|
||||
|
||||
export default function AdminCategoriesPage() {
|
||||
const [categories, setCategories] = useState<CategoryRow[]>([]);
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState("");
|
||||
const [subcategories, setSubcategories] = useState<SubCategoryRow[]>([]);
|
||||
const [loadingCategories, setLoadingCategories] = useState(true);
|
||||
const [loadingSubcategories, setLoadingSubcategories] = useState(false);
|
||||
const [categoryError, setCategoryError] = useState("");
|
||||
const [subcategoryError, setSubcategoryError] = useState("");
|
||||
const [categoryName, setCategoryName] = useState("");
|
||||
const [categoryDescription, setCategoryDescription] = useState("");
|
||||
const [subCategoryName, setSubCategoryName] = useState("");
|
||||
const [subCategoryDescription, setSubCategoryDescription] = useState("");
|
||||
const [attributeInput, setAttributeInput] = useState("");
|
||||
const [subCategoryAttributes, setSubCategoryAttributes] = useState<string[]>([]);
|
||||
const [savingCategory, setSavingCategory] = useState(false);
|
||||
const [savingSubcategory, setSavingSubcategory] = useState(false);
|
||||
|
||||
const selectedCategory =
|
||||
categories.find((category) => category.id === selectedCategoryId) || null;
|
||||
|
||||
useEffect(() => {
|
||||
async function loadCategories() {
|
||||
setLoadingCategories(true);
|
||||
setCategoryError("");
|
||||
try {
|
||||
const res = await fetch("/api/admin/categories?page=0&size=100", {
|
||||
headers: { "x-auth-token": getToken() },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(data?.responseDesc || data?.error || "Gagal memuat category");
|
||||
}
|
||||
const rows = parseCategories(data);
|
||||
setCategories(rows);
|
||||
setSelectedCategoryId((current) => current || rows[0]?.id || "");
|
||||
} catch (error) {
|
||||
setCategoryError(error instanceof Error ? error.message : "Gagal memuat category");
|
||||
} finally {
|
||||
setLoadingCategories(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadCategories();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadSubcategories() {
|
||||
if (!selectedCategoryId) {
|
||||
setSubcategories([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingSubcategories(true);
|
||||
setSubcategoryError("");
|
||||
try {
|
||||
const res = await fetch(`/api/admin/categories/${selectedCategoryId}/subcategories?page=0&size=100`, {
|
||||
headers: { "x-auth-token": getToken() },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(data?.responseDesc || data?.error || "Gagal memuat sub-category");
|
||||
}
|
||||
setSubcategories(parseSubCategories(data));
|
||||
} catch (error) {
|
||||
setSubcategoryError(error instanceof Error ? error.message : "Gagal memuat sub-category");
|
||||
} finally {
|
||||
setLoadingSubcategories(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadSubcategories();
|
||||
}, [selectedCategoryId]);
|
||||
|
||||
function addAttribute() {
|
||||
const normalized = attributeInput.trim();
|
||||
if (!normalized) return;
|
||||
setSubCategoryAttributes((current) =>
|
||||
current.includes(normalized) ? current : [...current, normalized]
|
||||
);
|
||||
setAttributeInput("");
|
||||
}
|
||||
|
||||
async function handleCreateCategory() {
|
||||
if (!categoryName.trim()) return;
|
||||
|
||||
setSavingCategory(true);
|
||||
setCategoryError("");
|
||||
try {
|
||||
const res = await fetch("/api/admin/categories", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-auth-token": getToken(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: categoryName.trim(),
|
||||
description: categoryDescription.trim() || null,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(data?.responseDesc || data?.error || "Gagal menambah category");
|
||||
}
|
||||
|
||||
const newRows = parseCategories(data);
|
||||
if (newRows[0]) {
|
||||
setCategories((current) => [newRows[0], ...current]);
|
||||
setSelectedCategoryId(newRows[0].id);
|
||||
} else {
|
||||
const refreshRes = await fetch("/api/admin/categories?page=0&size=100", {
|
||||
headers: { "x-auth-token": getToken() },
|
||||
});
|
||||
const refreshData = await refreshRes.json();
|
||||
const rows = parseCategories(refreshData);
|
||||
setCategories(rows);
|
||||
setSelectedCategoryId(rows[0]?.id || "");
|
||||
}
|
||||
|
||||
setCategoryName("");
|
||||
setCategoryDescription("");
|
||||
} catch (error) {
|
||||
setCategoryError(error instanceof Error ? error.message : "Gagal menambah category");
|
||||
} finally {
|
||||
setSavingCategory(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateSubcategory() {
|
||||
if (!selectedCategoryId || !subCategoryName.trim()) return;
|
||||
|
||||
setSavingSubcategory(true);
|
||||
setSubcategoryError("");
|
||||
try {
|
||||
const res = await fetch(`/api/admin/categories/${selectedCategoryId}/subcategories`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-auth-token": getToken(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: subCategoryName.trim(),
|
||||
description: subCategoryDescription.trim() || null,
|
||||
subCategoryAttributes,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(data?.responseDesc || data?.error || "Gagal menambah sub-category");
|
||||
}
|
||||
|
||||
const refreshRes = await fetch(`/api/admin/categories/${selectedCategoryId}/subcategories?page=0&size=100`, {
|
||||
headers: { "x-auth-token": getToken() },
|
||||
});
|
||||
const refreshData = await refreshRes.json();
|
||||
setSubcategories(parseSubCategories(refreshData));
|
||||
|
||||
setSubCategoryName("");
|
||||
setSubCategoryDescription("");
|
||||
setSubCategoryAttributes([]);
|
||||
setAttributeInput("");
|
||||
} catch (error) {
|
||||
setSubcategoryError(error instanceof Error ? error.message : "Gagal menambah sub-category");
|
||||
} finally {
|
||||
setSavingSubcategory(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="mb-10 flex items-end justify-between gap-6">
|
||||
<div className="max-w-3xl">
|
||||
<p className="mb-3 text-[10px] font-black uppercase tracking-[0.25em] text-primary">
|
||||
Taxonomy Console
|
||||
</p>
|
||||
<h2 className="text-4xl font-extrabold tracking-tight text-on-surface">
|
||||
Category Management
|
||||
</h2>
|
||||
<p className="mt-3 text-base text-slate-500">
|
||||
Kelola category dan sub-category produk untuk admin panel menggunakan endpoint category dari backend.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="rounded-xl bg-surface-container-lowest p-5 shadow-[0px_20px_40px_rgba(25,28,30,0.06)]">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-400">Categories</p>
|
||||
<p className="mt-2 text-3xl font-black text-on-surface">{categories.length}</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-surface-container-lowest p-5 shadow-[0px_20px_40px_rgba(25,28,30,0.06)]">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.15em] text-slate-400">Sub-Categories</p>
|
||||
<p className="mt-2 text-3xl font-black text-on-surface">{subcategories.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 xl:grid-cols-12">
|
||||
<section className="xl:col-span-4">
|
||||
<div className="rounded-2xl border-t-4 border-primary bg-surface-container-low p-6 shadow-[0px_20px_40px_rgba(25,28,30,0.06)]">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-black uppercase tracking-[0.2em] text-primary">
|
||||
Master Categories
|
||||
</h3>
|
||||
<p className="mt-1 text-xs text-slate-400">Daftar category utama</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-primary-fixed px-3 py-1 text-[10px] font-black uppercase tracking-widest text-on-primary-fixed">
|
||||
{categories.length} items
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 space-y-3 rounded-xl bg-white p-4">
|
||||
<input
|
||||
value={categoryName}
|
||||
onChange={(event) => setCategoryName(event.target.value)}
|
||||
placeholder="Nama category"
|
||||
className="w-full rounded-lg border border-surface-container px-4 py-3 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
value={categoryDescription}
|
||||
onChange={(event) => setCategoryDescription(event.target.value)}
|
||||
placeholder="Deskripsi category"
|
||||
rows={3}
|
||||
className="w-full rounded-lg border border-surface-container px-4 py-3 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreateCategory}
|
||||
disabled={savingCategory || !categoryName.trim()}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-xl bg-primary-container px-4 py-3 text-sm font-bold text-white transition hover:bg-primary disabled:opacity-60"
|
||||
>
|
||||
<span className="material-symbols-outlined text-base">add</span>
|
||||
{savingCategory ? "Menyimpan..." : "Add New Category"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{categoryError && (
|
||||
<div className="mb-4 rounded-xl bg-error-container px-4 py-3 text-sm font-semibold text-on-error-container">
|
||||
{categoryError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{loadingCategories ? (
|
||||
<div className="rounded-xl bg-white px-5 py-10 text-center text-slate-400">
|
||||
<span className="material-symbols-outlined animate-spin text-3xl">progress_activity</span>
|
||||
</div>
|
||||
) : categories.length === 0 ? (
|
||||
<div className="rounded-xl bg-white px-5 py-10 text-center text-slate-400">
|
||||
Belum ada category
|
||||
</div>
|
||||
) : (
|
||||
categories.map((category, index) => {
|
||||
const isActive = category.id === selectedCategoryId;
|
||||
return (
|
||||
<button
|
||||
key={category.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedCategoryId(category.id)}
|
||||
className={`group flex w-full items-center justify-between border-l-4 px-5 py-4 text-left transition ${
|
||||
isActive
|
||||
? "border-primary bg-white"
|
||||
: "border-transparent bg-white/50 hover:border-slate-300 hover:bg-white"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<span className={`mb-1 block text-xs font-black uppercase tracking-widest ${isActive ? "text-secondary" : "text-slate-400"}`}>
|
||||
CAT-{String(index + 1).padStart(3, "0")}
|
||||
</span>
|
||||
<h4 className={`text-xl font-black tracking-tight ${isActive ? "text-on-surface" : "text-on-surface/70"}`}>
|
||||
{category.name}
|
||||
</h4>
|
||||
{category.description && (
|
||||
<p className="mt-1 text-xs text-slate-500">{category.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className={`material-symbols-outlined transition ${isActive ? "text-primary" : "text-slate-300 group-hover:text-slate-500"}`}>
|
||||
chevron_right
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="xl:col-span-8">
|
||||
<div className="rounded-2xl bg-surface-container-lowest p-8 shadow-[0px_20px_40px_rgba(25,28,30,0.06)]">
|
||||
<div className="mb-8 flex items-start justify-between gap-6">
|
||||
<div>
|
||||
<h3 className="text-sm font-black uppercase tracking-[0.2em] text-secondary">
|
||||
{selectedCategory ? `Sub-Categories: ${selectedCategory.name}` : "Sub-Categories"}
|
||||
</h3>
|
||||
<p className="mt-2 text-3xl font-black tracking-tight text-on-surface">
|
||||
Taxonomy Architecture
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 rounded-2xl bg-surface-container-low p-5">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<input
|
||||
value={subCategoryName}
|
||||
onChange={(event) => setSubCategoryName(event.target.value)}
|
||||
placeholder="Nama sub-category"
|
||||
disabled={!selectedCategoryId}
|
||||
className="w-full rounded-lg border border-surface-container bg-white px-4 py-3 text-sm focus:border-primary focus:outline-none disabled:opacity-60"
|
||||
/>
|
||||
<input
|
||||
value={subCategoryDescription}
|
||||
onChange={(event) => setSubCategoryDescription(event.target.value)}
|
||||
placeholder="Deskripsi sub-category"
|
||||
disabled={!selectedCategoryId}
|
||||
className="w-full rounded-lg border border-surface-container bg-white px-4 py-3 text-sm focus:border-primary focus:outline-none disabled:opacity-60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex gap-3">
|
||||
<input
|
||||
value={attributeInput}
|
||||
onChange={(event) => setAttributeInput(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
addAttribute();
|
||||
}
|
||||
}}
|
||||
placeholder="Tambah attribute lalu Enter"
|
||||
disabled={!selectedCategoryId}
|
||||
className="flex-1 rounded-lg border border-surface-container bg-white px-4 py-3 text-sm focus:border-primary focus:outline-none disabled:opacity-60"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addAttribute}
|
||||
disabled={!selectedCategoryId || !attributeInput.trim()}
|
||||
className="rounded-xl border-2 border-primary px-4 py-3 text-xs font-black uppercase tracking-widest text-primary transition hover:bg-primary hover:text-white disabled:opacity-60"
|
||||
>
|
||||
Add Attribute
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{subCategoryAttributes.length > 0 && (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{subCategoryAttributes.map((attribute) => (
|
||||
<button
|
||||
key={attribute}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSubCategoryAttributes((current) =>
|
||||
current.filter((item) => item !== attribute)
|
||||
)
|
||||
}
|
||||
className="flex items-center gap-1 rounded-full bg-surface-container-high px-3 py-1 text-[10px] font-black uppercase tracking-wider text-tertiary"
|
||||
>
|
||||
<span className="material-symbols-outlined text-xs">label</span>
|
||||
{attribute}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCreateSubcategory}
|
||||
disabled={savingSubcategory || !selectedCategoryId || !subCategoryName.trim()}
|
||||
className="mt-4 flex items-center gap-2 rounded-xl bg-primary px-5 py-3 text-xs font-black uppercase tracking-widest text-white transition hover:bg-primary-container disabled:opacity-60"
|
||||
>
|
||||
<span className="material-symbols-outlined text-sm">add_circle</span>
|
||||
{savingSubcategory ? "Menyimpan..." : "Add Sub-Category"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{subcategoryError && (
|
||||
<div className="mb-4 rounded-xl bg-error-container px-4 py-3 text-sm font-semibold text-on-error-container">
|
||||
{subcategoryError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!selectedCategoryId ? (
|
||||
<div className="rounded-xl border border-dashed border-surface-container px-6 py-12 text-center text-slate-400">
|
||||
Pilih category di panel kiri untuk melihat sub-category.
|
||||
</div>
|
||||
) : loadingSubcategories ? (
|
||||
<div className="rounded-xl border border-dashed border-surface-container px-6 py-12 text-center text-slate-400">
|
||||
<span className="material-symbols-outlined animate-spin text-3xl">progress_activity</span>
|
||||
</div>
|
||||
) : subcategories.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-surface-container px-6 py-12 text-center text-slate-400">
|
||||
Belum ada sub-category untuk category ini.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{subcategories.map((subcategory) => (
|
||||
<div key={subcategory.id} className="group rounded-xl border-b border-slate-100 pb-5">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-surface-container-low">
|
||||
<span className="material-symbols-outlined text-secondary">category</span>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-lg font-black text-on-surface">
|
||||
{subcategory.name}
|
||||
</h5>
|
||||
<p className="text-xs text-slate-400">
|
||||
{subcategory.description || `ID: ${subcategory.id}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="rounded-full bg-surface-container px-3 py-1 text-[10px] font-black uppercase tracking-widest text-slate-500">
|
||||
{subcategory.subCategoryAttributes.length} attributes
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2 pl-[3.75rem]">
|
||||
{subcategory.subCategoryAttributes.length > 0 ? (
|
||||
subcategory.subCategoryAttributes.map((attribute) => (
|
||||
<span
|
||||
key={`${subcategory.id}-${attribute}`}
|
||||
className="flex items-center gap-1 rounded-full bg-surface-container px-3 py-1 text-[10px] font-black uppercase tracking-wider text-on-tertiary-fixed-variant"
|
||||
>
|
||||
<span className="material-symbols-outlined text-xs">label</span>
|
||||
{attribute}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-slate-300">Belum ada attribute</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -8,6 +8,7 @@ const navItems = [
|
||||
{ href: "/admin/dashboard", icon: "dashboard", label: "Dashboard" },
|
||||
{ href: "/admin/news", icon: "newspaper", label: "News" },
|
||||
{ href: "/admin/places", icon: "map", label: "Places" },
|
||||
{ href: "/admin/categories", icon: "category", label: "Categories" },
|
||||
{ href: "/admin/review", icon: "rate_review", label: "Review" },
|
||||
];
|
||||
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { API_URL, makeHeaders } from "@/lib/api";
|
||||
|
||||
function normalizeBearerToken(rawToken: string) {
|
||||
if (!rawToken) return "";
|
||||
return rawToken.startsWith("Bearer ") ? rawToken : `Bearer ${rawToken}`;
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
context: { params: Promise<{ categoryId: string }> }
|
||||
) {
|
||||
const token = normalizeBearerToken(req.headers.get("x-auth-token") || "");
|
||||
const { categoryId } = await context.params;
|
||||
const { searchParams } = req.nextUrl;
|
||||
const page = parseInt(searchParams.get("page") || "0", 10) + 1;
|
||||
const size = searchParams.get("size") || "100";
|
||||
|
||||
const res = await fetch(
|
||||
`${API_URL}/api/v1.0/sub/${categoryId}/categories?page=${page}&size=${size}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: makeHeaders(token, { includeTenantId: true }),
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
context: { params: Promise<{ categoryId: string }> }
|
||||
) {
|
||||
const token = normalizeBearerToken(req.headers.get("x-auth-token") || "");
|
||||
const { categoryId } = await context.params;
|
||||
const body = await req.json();
|
||||
|
||||
const res = await fetch(`${API_URL}/api/v1.0/sub/${categoryId}/categories`, {
|
||||
method: "POST",
|
||||
headers: makeHeaders(token, { includeTenantId: true }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
}
|
||||
40
src/app/api/admin/categories/route.ts
Normal file
40
src/app/api/admin/categories/route.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { API_URL, makeHeaders } from "@/lib/api";
|
||||
|
||||
function normalizeBearerToken(rawToken: string) {
|
||||
if (!rawToken) return "";
|
||||
return rawToken.startsWith("Bearer ") ? rawToken : `Bearer ${rawToken}`;
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const token = normalizeBearerToken(req.headers.get("x-auth-token") || "");
|
||||
const { searchParams } = req.nextUrl;
|
||||
const page = parseInt(searchParams.get("page") || "0", 10) + 1;
|
||||
const size = searchParams.get("size") || "100";
|
||||
|
||||
const res = await fetch(
|
||||
`${API_URL}/api/v1.0/categories?page=${page}&size=${size}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: makeHeaders(token, { includeTenantId: true }),
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const token = normalizeBearerToken(req.headers.get("x-auth-token") || "");
|
||||
const body = await req.json();
|
||||
|
||||
const res = await fetch(`${API_URL}/api/v1.0/categories`, {
|
||||
method: "POST",
|
||||
headers: makeHeaders(token, { includeTenantId: true }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
}
|
||||
Reference in New Issue
Block a user