From a8c83b9cb6d5c46943abc571370d42272c2851db Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Fri, 2 May 2025 22:05:17 +0100 Subject: [PATCH] feat: banner and report viewer in admin panel --- src/app/admin/page.tsx | 123 ++++++++++++++++++++++++++- src/app/api/admin/banner/route.ts | 30 +++++++ src/app/api/report/route.ts | 27 +----- src/app/layout.tsx | 2 + src/components/admin/banner-form.tsx | 29 +++++++ src/components/admin/banner.tsx | 22 +++++ 6 files changed, 206 insertions(+), 27 deletions(-) create mode 100644 src/app/api/admin/banner/route.ts create mode 100644 src/components/admin/banner-form.tsx create mode 100644 src/components/admin/banner.tsx diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 42d2da8..853e5c8 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,11 +1,45 @@ -import { auth } from "@/lib/auth"; +import { Metadata } from "next"; +import Link from "next/link"; import { redirect } from "next/navigation"; +import { revalidatePath } from "next/cache"; + +import { Icon } from "@iconify/react"; +import { ReportStatus } from "@prisma/client"; + +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +import BannerForm from "@/components/admin/banner-form"; + +export const metadata: Metadata = { + title: "Admin - TomodachiShare", + description: "TomodachiShare admin panel", + robots: { + index: false, + follow: false, + }, +}; export default async function AdminPage() { const session = await auth(); if (!session || Number(session.user.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID)) redirect("/404"); + const reports = await prisma.report.findMany(); + + const updateStatus = async (formData: FormData) => { + "use server"; + const id = Number(formData.get("id")); + const status = formData.get("status") as ReportStatus; + + await prisma.report.update({ + where: { id }, + data: { status }, + }); + + revalidatePath("/admin"); + }; + return (
@@ -13,12 +47,99 @@ export default async function AdminPage() {

View reports, set banners, etc.

+ {/* Separator */} +
+
+ Banners +
+
+ + + {/* Separator */}

Reports
+ +
+ + + + + + + + + + + + + + {reports.map((report, index) => ( + + + + + + + + + + ))} + +
TypeStatusTargetReasonNotesAuthorActions
+ + {report.reportType} + + + + {report.status} + + + {report.targetId} + {report.reason}{report.reasonNotes} + {report.authorId} + +
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
); } diff --git a/src/app/api/admin/banner/route.ts b/src/app/api/admin/banner/route.ts new file mode 100644 index 0000000..179b812 --- /dev/null +++ b/src/app/api/admin/banner/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; + +let bannerText: string | null = null; + +export async function GET() { + return NextResponse.json({ success: true, message: bannerText }); +} + +export async function POST(request: NextRequest) { + const session = await auth(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + if (Number(session.user.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID)) return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + + const body = await request.text(); + bannerText = body; + + return NextResponse.json({ success: true }); +} + +export async function DELETE() { + const session = await auth(); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + if (Number(session.user.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID)) return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + + bannerText = null; + return NextResponse.json({ success: true }); +} diff --git a/src/app/api/report/route.ts b/src/app/api/report/route.ts index 472cd12..c1442e4 100644 --- a/src/app/api/report/route.ts +++ b/src/app/api/report/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { ReportReason, ReportStatus, ReportType } from "@prisma/client"; +import { ReportReason, ReportType } from "@prisma/client"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; @@ -15,10 +15,6 @@ const reportSchema = z.object({ notes: z.string().trim().max(256).optional(), }); -const getReportSchema = z.object({ - status: z.enum(["open", "resolved", "dismissed"], { message: "Status must be either 'open', 'resolved', or 'dismissed'" }).default("open"), -}); - export async function POST(request: NextRequest) { const session = await auth(); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -70,24 +66,3 @@ export async function POST(request: NextRequest) { return rateLimit.sendResponse({ success: true }); } - -export async function GET(request: NextRequest) { - const session = await auth(); - if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - - // Check if user is an admin - if (Number(session.user.id) != Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID)) - return NextResponse.json({ error: "You're not an admin" }, { status: 403 }); - - const parsed = getReportSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams)); - if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 }); - const { status } = parsed.data; - - const reports = await prisma.report.findMany({ - where: { - status: (status.toUpperCase() as ReportStatus) ?? "OPEN", - }, - }); - - return NextResponse.json({ success: true, reports }); -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c29e588..fa60714 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,6 +7,7 @@ import "./globals.css"; import Providers from "./provider"; import Header from "@/components/header"; import Footer from "@/components/footer"; +import AdminBanner from "@/components/admin/banner"; const lexend = Lexend({ subsets: ["latin"], @@ -35,6 +36,7 @@ export default function RootLayout({
+
{children}