qudoco-fe/components/main/content-website-approver.tsx

251 lines
7.3 KiB
TypeScript
Raw Normal View History

2026-02-27 08:52:08 +00:00
"use client";
import { useCallback, useEffect, useState } from "react";
2026-02-27 08:52:08 +00:00
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent } from "@/components/ui/card";
import { Filter, Loader2 } from "lucide-react";
import {
approveCmsContentSubmission,
listCmsContentSubmissions,
rejectCmsContentSubmission,
} from "@/service/cms-content-submissions";
import { apiPayload } from "@/service/cms-landing";
import { formatDate } from "@/utils/global";
import Swal from "sweetalert2";
const DOMAIN_LABEL: Record<string, string> = {
hero: "Hero",
about: "About Us",
product: "Product",
service: "Service",
partner: "Partner",
popup: "Pop Up",
};
type Row = {
id: string;
domain: string;
title: string;
status: string;
submitter_name: string;
submitted_by_id: number;
payload: string;
created_at: string;
};
2026-02-27 08:52:08 +00:00
export default function ApproverContentWebsite() {
const [rows, setRows] = useState<Row[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [actingId, setActingId] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
try {
const res = await listCmsContentSubmissions({
status: "pending",
page: 1,
limit: 100,
});
const raw = apiPayload<Row[]>(res);
setRows(Array.isArray(raw) ? raw : []);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
load();
}, [load]);
const filtered = rows.filter((r) => {
const q = search.trim().toLowerCase();
if (!q) return true;
return (
r.title.toLowerCase().includes(q) ||
(r.submitter_name ?? "").toLowerCase().includes(q) ||
(DOMAIN_LABEL[r.domain] ?? r.domain).toLowerCase().includes(q)
);
});
2026-02-27 08:52:08 +00:00
async function onApprove(id: string) {
const ok = await Swal.fire({
icon: "question",
title: "Terapkan perubahan ke website?",
showCancelButton: true,
});
if (!ok.isConfirmed) return;
setActingId(id);
try {
const res = await approveCmsContentSubmission(id);
if ((res as { error?: boolean })?.error) {
await Swal.fire({
icon: "error",
title: "Gagal",
text: String((res as { message?: unknown })?.message ?? ""),
});
return;
}
await Swal.fire({
icon: "success",
title: "Disetujui",
timer: 1600,
showConfirmButton: false,
});
await load();
} finally {
setActingId(null);
}
}
async function onReject(id: string) {
const note = await Swal.fire({
icon: "warning",
title: "Tolak pengajuan?",
input: "textarea",
inputPlaceholder: "Catatan (opsional)",
showCancelButton: true,
});
if (!note.isConfirmed) return;
setActingId(id);
try {
const res = await rejectCmsContentSubmission(
id,
typeof note.value === "string" ? note.value : "",
);
if ((res as { error?: boolean })?.error) {
await Swal.fire({
icon: "error",
title: "Gagal",
text: String((res as { message?: unknown })?.message ?? ""),
});
return;
}
await Swal.fire({
icon: "success",
title: "Ditolak",
timer: 1400,
showConfirmButton: false,
});
await load();
} finally {
setActingId(null);
}
}
2026-02-27 08:52:08 +00:00
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-slate-800">Content Website</h1>
<p className="text-slate-500">
Tinjau pengajuan perubahan dari kontributor dan terapkan ke konten live.
2026-02-27 08:52:08 +00:00
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<Input
placeholder="Cari judul, domain, atau nama pengaju…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="max-w-md"
/>
<Button
type="button"
variant="outline"
className="gap-2 shrink-0"
onClick={() => load()}
disabled={loading}
>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Filter className="h-4 w-4" />
)}
Refresh
2026-02-27 08:52:08 +00:00
</Button>
</div>
<Card className="rounded-2xl border shadow-sm overflow-hidden">
<CardContent className="p-0">
{loading ? (
<div className="flex justify-center py-16">
<Loader2 className="h-10 w-10 animate-spin text-[#966314]" />
</div>
) : (
<Table>
<TableHeader>
<TableRow className="bg-slate-50">
<TableHead>Judul</TableHead>
<TableHead>Bagian</TableHead>
<TableHead>Pengaju</TableHead>
<TableHead>Tanggal</TableHead>
<TableHead className="text-right">Aksi</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-slate-500 py-10">
Tidak ada pengajuan tertunda.
</TableCell>
</TableRow>
) : (
filtered.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium max-w-xs">
{item.title}
</TableCell>
<TableCell>
<Badge variant="outline">
{DOMAIN_LABEL[item.domain] ?? item.domain}
</Badge>
</TableCell>
<TableCell className="text-slate-600">
{item.submitter_name || `User #${item.submitted_by_id}`}
</TableCell>
<TableCell className="text-slate-600">
{formatDate(item.created_at)}
</TableCell>
<TableCell className="text-right space-x-2">
<Button
type="button"
size="sm"
className="bg-green-600 hover:bg-green-700"
disabled={actingId === item.id}
onClick={() => onApprove(item.id)}
>
Setujui
</Button>
<Button
type="button"
size="sm"
variant="outline"
className="text-red-600 border-red-200"
disabled={actingId === item.id}
onClick={() => onReject(item.id)}
>
Tolak
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
)}
</CardContent>
</Card>
2026-02-27 08:52:08 +00:00
</div>
);
}