Files
InaTrading-Portal/src/app/admin/places/page.tsx
2026-04-24 05:19:05 +07:00

284 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
interface PlaceRow {
id: string;
name: string;
description: string | null;
type: string | null;
city: string | null;
province: string | null;
country: string | null;
status: string | null;
image1: string | null;
contact: string | null;
address: string | null;
}
function getToken() {
if (typeof window === "undefined") return "";
return sessionStorage.getItem("token") || localStorage.getItem("token") || "";
}
function statusBadge(status: string | null) {
switch (status) {
case "APPROVED":
return "bg-tertiary-fixed text-on-tertiary-fixed-variant";
case "PENDING":
return "bg-primary-fixed text-on-primary-fixed-variant";
case "REJECTED":
return "bg-error-container text-on-error-container";
default:
return "bg-slate-200 text-slate-600";
}
}
export default function AdminPlacesPage() {
const [rows, setRows] = useState<PlaceRow[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [page, setPage] = useState(0);
const [totalItem, setTotalItem] = useState(0);
const [totalPage, setTotalPage] = useState(1);
const pageSize = 20;
useEffect(() => {
async function load() {
setLoading(true);
setError("");
try {
const token = getToken();
const res = await fetch(`/api/admin/places?page=${page}&size=${pageSize}`, {
headers: { "x-auth-token": token },
});
const data = await res.json();
setRows(Array.isArray(data?.rows) ? data.rows : []);
setTotalItem(data?.totalItem ?? 0);
setTotalPage(data?.totalPage ?? 1);
} catch {
setError("Gagal memuat data lokasi");
} finally {
setLoading(false);
}
}
load();
}, [page]);
const startEntry = page * pageSize + 1;
const endEntry = Math.min((page + 1) * pageSize, totalItem);
return (
<>
{/* Header */}
<section className="flex justify-between items-end mb-12">
<div>
<nav className="flex items-center space-x-2 text-[10px] font-bold uppercase tracking-widest text-primary mb-3">
<span>Curator Dashboard</span>
<span className="material-symbols-outlined text-[10px]">chevron_right</span>
<span className="text-slate-400">Global Places</span>
</nav>
<h2 className="text-4xl font-extrabold tracking-tight text-on-surface leading-none">Places Intelligence</h2>
<p className="mt-4 text-secondary max-w-xl text-base leading-relaxed">
Manage locations, tourist destinations, and business spots across Ina Trading&apos;s global network.
</p>
</div>
<Link
href="/admin/places/new"
className="bg-gradient-to-r from-primary to-primary-container text-white px-8 py-3.5 rounded-lg font-bold flex items-center space-x-3 shadow-lg shadow-primary/20 hover:scale-[1.02] transition-transform"
>
<span className="material-symbols-outlined text-lg">add</span>
<span className="tracking-tight">Add Place</span>
</Link>
</section>
{/* Stats */}
<section className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
<div className="bg-surface-container-lowest p-8 shadow-[0px_20px_40px_rgba(25,28,30,0.06)] rounded-xl border-l-4 border-primary">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400 block mb-2">Total Locations</span>
<div className="flex items-baseline space-x-2">
<span className="text-4xl font-extrabold tracking-tighter">{totalItem.toLocaleString()}</span>
<span className="text-tertiary text-xs font-bold">Entries</span>
</div>
</div>
<div className="bg-surface-container-lowest p-8 shadow-[0px_20px_40px_rgba(25,28,30,0.06)] rounded-xl">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400 block mb-2">Approved</span>
<div className="flex items-baseline space-x-2">
<span className="text-4xl font-extrabold tracking-tighter">
{rows.filter((r) => r.status === "APPROVED").length}
</span>
<span className="text-tertiary text-xs font-bold">Active</span>
</div>
</div>
<div className="bg-surface-container-lowest p-8 shadow-[0px_20px_40px_rgba(25,28,30,0.06)] rounded-xl">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400 block mb-2">Showing Page</span>
<div className="flex items-baseline space-x-2">
<span className="text-4xl font-extrabold tracking-tighter">{page + 1}</span>
<span className="text-slate-400 text-xs font-medium">/ {totalPage}</span>
</div>
</div>
</section>
{/* Table */}
<div className="bg-surface-container-lowest shadow-[0px_20px_40px_rgba(25,28,30,0.06)] rounded-2xl overflow-hidden">
<div className="p-8 border-b border-surface-container">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold tracking-tight">Location Registry</h3>
<div className="flex items-center space-x-4">
<button className="flex items-center space-x-2 text-xs font-bold uppercase tracking-widest text-secondary hover:text-primary transition-colors">
<span className="material-symbols-outlined text-sm">filter_list</span>
<span>Filter</span>
</button>
</div>
</div>
</div>
{loading ? (
<div className="p-16 text-center text-slate-400">
<span className="material-symbols-outlined text-4xl mb-4 block animate-spin">progress_activity</span>
<p className="text-sm font-medium">Memuat data...</p>
</div>
) : error ? (
<div className="p-16 text-center text-error">
<span className="material-symbols-outlined text-4xl mb-4 block">error</span>
<p className="text-sm font-medium">{error}</p>
</div>
) : rows.length === 0 ? (
<div className="p-16 text-center text-slate-400">
<span className="material-symbols-outlined text-4xl mb-4 block">map</span>
<p className="text-sm font-medium">Belum ada lokasi</p>
</div>
) : (
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-surface-container-low/50">
{["Place", "Type", "Location", "Status", "Actions"].map((h) => (
<th
key={h}
className={`px-8 py-5 text-[10px] font-black uppercase tracking-[0.15em] text-slate-500 ${h === "Actions" ? "text-right" : ""}`}
>
{h}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-surface-container">
{rows.map((item) => (
<tr key={item.id} className="hover:bg-surface-container-low transition-colors group">
{/* Place */}
<td className="px-8 py-6">
<div className="flex items-center space-x-4">
{item.image1 ? (
/* eslint-disable-next-line @next/next/no-img-element */
<img
src={item.image1}
alt={item.name}
className="w-12 h-12 rounded-lg object-cover flex-shrink-0 bg-slate-100"
/>
) : (
<div className="w-12 h-12 rounded-lg bg-slate-100 flex items-center justify-center flex-shrink-0">
<span className="material-symbols-outlined text-slate-300 text-xl">place</span>
</div>
)}
<div className="min-w-0">
<p className="font-bold text-on-surface tracking-tight leading-snug truncate max-w-xs">{item.name}</p>
{item.description && (
<p className="text-[10px] text-slate-400 mt-0.5 truncate max-w-xs">{item.description}</p>
)}
</div>
</div>
</td>
{/* Type */}
<td className="px-8 py-6">
{item.type ? (
<span className="px-3 py-1 rounded-full bg-secondary-fixed text-on-secondary-fixed-variant text-[10px] font-black uppercase tracking-widest">
{item.type}
</span>
) : (
<span className="text-slate-300 text-xs"></span>
)}
</td>
{/* Location */}
<td className="px-8 py-6">
<div className="flex flex-col">
<span className="text-sm font-semibold text-on-surface">
{[item.city, item.province].filter(Boolean).join(", ") || "—"}
</span>
{item.country && (
<span className="text-[10px] text-slate-400 mt-0.5">{item.country}</span>
)}
</div>
</td>
{/* Status */}
<td className="px-8 py-6">
<span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${statusBadge(item.status)}`}>
{item.status || "—"}
</span>
</td>
{/* Actions */}
<td className="px-8 py-6 text-right">
<Link
href={`/admin/places/${item.id}/edit`}
onClick={() => sessionStorage.setItem("editPlaceCache", JSON.stringify(item))}
className="inline-flex items-center gap-1.5 bg-surface-container px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest text-on-surface hover:bg-primary hover:text-white transition-all"
>
<span className="material-symbols-outlined text-sm">edit</span>
Edit
</Link>
</td>
</tr>
))}
</tbody>
</table>
)}
{/* Pagination */}
{!loading && rows.length > 0 && (
<div className="p-6 bg-surface-container-low/30 flex items-center justify-between">
<span className="text-xs text-slate-500">
Showing{" "}
<span className="font-bold text-on-surface">{startEntry}{endEntry}</span>{" "}
of{" "}
<span className="font-bold text-on-surface">{totalItem.toLocaleString()}</span> entries
</span>
<div className="flex items-center space-x-2">
<button
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
className="w-8 h-8 flex items-center justify-center rounded border border-surface-container hover:bg-white transition-colors text-slate-400 disabled:opacity-40"
>
<span className="material-symbols-outlined text-sm">chevron_left</span>
</button>
{Array.from({ length: Math.min(totalPage, 5) }, (_, i) => i).map((i) => (
<button
key={i}
onClick={() => setPage(i)}
className={`w-8 h-8 flex items-center justify-center rounded text-xs font-bold transition-colors ${
i === page
? "bg-primary text-white"
: "border border-surface-container hover:bg-white text-on-surface"
}`}
>
{i + 1}
</button>
))}
<button
onClick={() => setPage((p) => Math.min(totalPage - 1, p + 1))}
disabled={page >= totalPage - 1}
className="w-8 h-8 flex items-center justify-center rounded border border-surface-container hover:bg-white transition-colors text-slate-400 disabled:opacity-40"
>
<span className="material-symbols-outlined text-sm">chevron_right</span>
</button>
</div>
</div>
)}
</div>
</>
);
}