276 lines
8.9 KiB
TypeScript
276 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useI18n } from "../../components/I18nProvider";
|
|
|
|
type PendingUser = {
|
|
id: string;
|
|
email: string;
|
|
status: string;
|
|
emailVerifiedAt: string | null;
|
|
approvedAt: string | null;
|
|
role: string;
|
|
};
|
|
type PendingListing = {
|
|
id: string;
|
|
status: string;
|
|
createdAt: string;
|
|
owner: { email: string };
|
|
translations: { title: string; slug: string; locale: string }[];
|
|
evChargingAvailable: boolean;
|
|
evChargingOnSite: boolean;
|
|
wheelchairAccessible: boolean;
|
|
};
|
|
|
|
export default function PendingAdminPage() {
|
|
const { t } = useI18n();
|
|
const [pendingUsers, setPendingUsers] = useState<PendingUser[]>([]);
|
|
const [pendingListings, setPendingListings] = useState<PendingListing[]>([]);
|
|
const [role, setRole] = useState<string | null>(null);
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function loadPending() {
|
|
setMessage(null);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch("/api/admin/pending", { cache: "no-store" });
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || "Failed to load");
|
|
return;
|
|
}
|
|
setRole(data.role ?? null);
|
|
setPendingUsers(data.users ?? []);
|
|
setPendingListings(data.listings ?? []);
|
|
} catch (e) {
|
|
setError("Failed to load");
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetch("/api/auth/me", { cache: "no-store" })
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
setRole(data.user?.role ?? null);
|
|
if (!data.user?.role) {
|
|
setError(t("adminRequired"));
|
|
return;
|
|
}
|
|
if (
|
|
["ADMIN", "USER_MODERATOR", "LISTING_MODERATOR"].includes(
|
|
data.user.role,
|
|
)
|
|
) {
|
|
loadPending();
|
|
return;
|
|
}
|
|
setError(t("adminRequired"));
|
|
})
|
|
.catch(() => setError(t("adminRequired")));
|
|
}, [t]);
|
|
|
|
async function approveUser(userId: string, makeAdmin: boolean) {
|
|
setMessage(null);
|
|
setError(null);
|
|
const res = await fetch("/api/admin/users/approve", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ userId, makeAdmin }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || "Failed to approve user");
|
|
} else {
|
|
setMessage(t("userUpdated"));
|
|
loadPending();
|
|
}
|
|
}
|
|
|
|
async function approveListing(
|
|
listingId: string,
|
|
action: "approve" | "reject" | "remove",
|
|
) {
|
|
setMessage(null);
|
|
setError(null);
|
|
const reason =
|
|
action === "reject"
|
|
? window.prompt(`${t("reject")}? (optional)`)
|
|
: action === "remove"
|
|
? window.prompt(`${t("remove")}? (optional)`)
|
|
: null;
|
|
const res = await fetch("/api/admin/listings/approve", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ listingId, action, reason }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || "Failed to update listing");
|
|
} else {
|
|
setMessage(t("approvalsMessage"));
|
|
loadPending();
|
|
}
|
|
}
|
|
|
|
async function rejectUser(userId: string) {
|
|
setMessage(null);
|
|
setError(null);
|
|
const reason = window.prompt(`${t("reject")}? (optional)`);
|
|
const res = await fetch("/api/admin/users/reject", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ userId, reason }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
setError(data.error || "Failed to reject user");
|
|
} else {
|
|
setMessage(t("userUpdated"));
|
|
loadPending();
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="panel" style={{ maxWidth: 960, margin: "40px auto" }}>
|
|
<h1>{t("pendingAdminTitle")}</h1>
|
|
<div style={{ display: "grid", gap: 16 }}>
|
|
<section>
|
|
<h3>{t("pendingUsersTitle")}</h3>
|
|
{pendingUsers.length === 0 ? (
|
|
<p>{t("noPendingUsers")}</p>
|
|
) : (
|
|
<ul
|
|
style={{ display: "grid", gap: 8, padding: 0, listStyle: "none" }}
|
|
>
|
|
{pendingUsers.map((u) => (
|
|
<li
|
|
key={u.id}
|
|
style={{
|
|
border: "1px solid #ddd",
|
|
borderRadius: 8,
|
|
padding: 12,
|
|
}}
|
|
>
|
|
<div>
|
|
<strong>{u.email}</strong> — {t("statusLabel")}: {u.status}{" "}
|
|
— {t("verifiedLabel")}:{" "}
|
|
{u.emailVerifiedAt ? t("yes") : t("no")}
|
|
</div>
|
|
<div style={{ marginTop: 8, display: "flex", gap: 8 }}>
|
|
<button
|
|
className="button"
|
|
onClick={() => approveUser(u.id, false)}
|
|
disabled={role !== "ADMIN" && role !== "USER_MODERATOR"}
|
|
>
|
|
{t("approve")}
|
|
</button>
|
|
<button
|
|
className="button secondary"
|
|
onClick={() => approveUser(u.id, true)}
|
|
disabled={role !== "ADMIN"}
|
|
>
|
|
{t("approveAdmin")}
|
|
</button>
|
|
<button
|
|
className="button secondary"
|
|
onClick={() => rejectUser(u.id)}
|
|
disabled={role !== "ADMIN" && role !== "USER_MODERATOR"}
|
|
>
|
|
{t("reject")}
|
|
</button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
<section>
|
|
<h3>{t("pendingListingsTitle")}</h3>
|
|
{pendingListings.length === 0 ? (
|
|
<p>{t("noPendingListings")}</p>
|
|
) : (
|
|
<ul
|
|
style={{ display: "grid", gap: 8, padding: 0, listStyle: "none" }}
|
|
>
|
|
{pendingListings.map((l) => (
|
|
<li
|
|
key={l.id}
|
|
style={{
|
|
border: "1px solid #ddd",
|
|
borderRadius: 8,
|
|
padding: 12,
|
|
}}
|
|
>
|
|
<div>
|
|
<strong>{l.translations[0]?.title ?? "Listing"}</strong> —
|
|
owner: {l.owner.email}
|
|
</div>
|
|
<div
|
|
style={{
|
|
marginTop: 6,
|
|
display: "flex",
|
|
gap: 6,
|
|
flexWrap: "wrap",
|
|
}}
|
|
>
|
|
{l.evChargingOnSite ? (
|
|
<span className="badge">{t("amenityEvOnSite")}</span>
|
|
) : null}
|
|
{l.evChargingAvailable && !l.evChargingOnSite ? (
|
|
<span className="badge">{t("amenityEvNearby")}</span>
|
|
) : null}
|
|
{l.wheelchairAccessible ? (
|
|
<span className="badge">
|
|
{t("amenityWheelchairAccessible")}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<div style={{ fontSize: 12, color: "#666" }}>
|
|
{t("slugsLabel")}:{" "}
|
|
{l.translations
|
|
.map((t) => `${t.slug} (${t.locale})`)
|
|
.join(", ")}
|
|
</div>
|
|
<div style={{ marginTop: 8, display: "flex", gap: 8 }}>
|
|
<button
|
|
className="button"
|
|
onClick={() => approveListing(l.id, "approve")}
|
|
disabled={
|
|
role !== "ADMIN" && role !== "LISTING_MODERATOR"
|
|
}
|
|
>
|
|
{t("publish")}
|
|
</button>
|
|
<button
|
|
className="button secondary"
|
|
onClick={() => approveListing(l.id, "reject")}
|
|
disabled={
|
|
role !== "ADMIN" && role !== "LISTING_MODERATOR"
|
|
}
|
|
>
|
|
{t("reject")}
|
|
</button>
|
|
<button
|
|
className="button secondary"
|
|
onClick={() => approveListing(l.id, "remove")}
|
|
disabled={
|
|
role !== "ADMIN" && role !== "LISTING_MODERATOR"
|
|
}
|
|
>
|
|
{t("remove")}
|
|
</button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
</div>
|
|
{message ? (
|
|
<p style={{ marginTop: 12, color: "green" }}>{message}</p>
|
|
) : null}
|
|
{error ? <p style={{ marginTop: 12, color: "red" }}>{error}</p> : null}
|
|
</main>
|
|
);
|
|
}
|