feat(admin): ban appeals, photos polish, economy adjust, staff smoke
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
fe46bd0544
commit
3ad3f9512c
36 files changed
+1185
-293
No files matched your search
+106
-52
@@ -1,35 +1,62 @@
|
||||
import { Camera } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { deletePhoto } from "@/actions/admin-photos";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminPhotos() {
|
||||
export default async function AdminPhotos({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string>>;
|
||||
}) {
|
||||
const { session, permissions } = await getAdminContext();
|
||||
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
const t = await getTranslations("pages.admin.photos");
|
||||
const parsed = parseListParams(new URLSearchParams(await searchParams));
|
||||
|
||||
const total = await prisma.cameraWeb.count().catch(() => 0);
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage || 24);
|
||||
|
||||
let photos: Awaited<ReturnType<typeof prisma.cameraWeb.findMany>> = [];
|
||||
try {
|
||||
photos = await prisma.cameraWeb.findMany({
|
||||
orderBy: { timestamp: "desc" },
|
||||
take: 60,
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
});
|
||||
} catch {
|
||||
photos = [];
|
||||
}
|
||||
|
||||
const userIds = [...new Set(photos.map((p) => p.userId))];
|
||||
const users =
|
||||
userIds.length > 0
|
||||
? await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
.catch(() => [])
|
||||
: [];
|
||||
const usernameById = new Map(users.map((u) => [u.id, u.username]));
|
||||
|
||||
const uniqueRooms = new Set(photos.map((p) => p.roomId)).size;
|
||||
const uniqueUsers = new Set(photos.map((p) => p.userId)).size;
|
||||
const canEdit = canAccess(permissions, PERMS.PAGES_EDIT, session.user.rank);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<AdminPageShell icon={Camera} title={t("title")} subtitle={t("subtitle")}>
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label={t("photosShown")} value={photos.length} icon="📸" />
|
||||
<StatusCard
|
||||
@@ -44,68 +71,95 @@ export default async function AdminPhotos() {
|
||||
hint={t("distinctPhotographers")}
|
||||
icon="👤"
|
||||
/>
|
||||
<StatusCard label={t("total")} value={total} icon="🗂" />
|
||||
</div>
|
||||
|
||||
<section className="mt-6">
|
||||
<section className="mt-2">
|
||||
<h2 className="admin-section-title">{t("recentPhotos")}</h2>
|
||||
{photos.length === 0 ? (
|
||||
<div className="admin-empty">{t("noPhotos")}</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{photos.map((p) => (
|
||||
<div
|
||||
key={String(p.id)}
|
||||
className="admin-card"
|
||||
style={{ padding: 0, overflow: "hidden" }}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={p.url}
|
||||
alt={t("photoAlt", { id: String(p.id) })}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
background: "#f1f5f9",
|
||||
}}
|
||||
/>
|
||||
<div style={{ padding: "0.85rem 1.05rem 1.05rem" }}>
|
||||
<div
|
||||
{photos.map((p) => {
|
||||
const username = usernameById.get(p.userId) ?? `#${p.userId}`;
|
||||
return (
|
||||
<div
|
||||
key={String(p.id)}
|
||||
className="admin-card"
|
||||
style={{ padding: 0, overflow: "hidden" }}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={p.url}
|
||||
alt={t("photoAlt", { id: String(p.id) })}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
gap: "0.5rem",
|
||||
alignItems: "baseline",
|
||||
width: "100%",
|
||||
height: 200,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
background: "#f1f5f9",
|
||||
}}
|
||||
>
|
||||
<strong>#{String(p.id)}</strong>
|
||||
<span className="text-sm theme-text-muted dark:theme-text-muted">
|
||||
{t("colRoom")} {p.roomId}
|
||||
</span>
|
||||
/>
|
||||
<div style={{ padding: "0.85rem 1.05rem 1.05rem" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
gap: "0.5rem",
|
||||
alignItems: "baseline",
|
||||
}}
|
||||
>
|
||||
<strong>#{String(p.id)}</strong>
|
||||
<Link
|
||||
href={`/admin/rooms/edit/${p.roomId}`}
|
||||
className="text-sm theme-text-muted dark:theme-text-muted hover:underline"
|
||||
>
|
||||
{t("colRoom")} {p.roomId}
|
||||
</Link>
|
||||
</div>
|
||||
<p
|
||||
className="text-xs theme-text-muted dark:theme-text-muted"
|
||||
style={{ margin: "0.3rem 0 0" }}
|
||||
>
|
||||
<Link
|
||||
href={`/admin/users/show/${p.userId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{t("byUser", { username })}
|
||||
</Link>{" "}
|
||||
·{" "}
|
||||
{new Date(p.timestamp * 1000)
|
||||
.toISOString()
|
||||
.slice(0, 16)
|
||||
.replace("T", " ")}
|
||||
</p>
|
||||
{canEdit ? (
|
||||
<form
|
||||
action={deletePhoto}
|
||||
style={{ marginTop: "0.75rem" }}
|
||||
>
|
||||
<input type="hidden" name="id" value={String(p.id)} />
|
||||
<Button type="submit" variant="destructive">
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</form>
|
||||
) : null}
|
||||
</div>
|
||||
<p
|
||||
className="text-xs theme-text-muted dark:theme-text-muted"
|
||||
style={{ margin: "0.3rem 0 0" }}
|
||||
>
|
||||
{t("colUser")} {p.userId} ·{" "}
|
||||
{new Date(p.timestamp * 1000)
|
||||
.toISOString()
|
||||
.slice(0, 16)
|
||||
.replace("T", " ")}
|
||||
</p>
|
||||
<form action={deletePhoto} style={{ marginTop: "0.75rem" }}>
|
||||
<input type="hidden" name="id" value={String(p.id)} />
|
||||
<Button type="submit" variant="destructive">
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{pagination.lastPage > 1 ? (
|
||||
<p className="text-sm text-muted-foreground mt-4">
|
||||
{t("pageOf", {
|
||||
page: pagination.page,
|
||||
last: pagination.lastPage,
|
||||
total,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
</main>
|
||||
</AdminPageShell>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user