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
+
+
+
+
+
+ | Type |
+ Status |
+ Target |
+ Reason |
+ Notes |
+ Author |
+ Actions |
+
+
+
+ {reports.map((report, index) => (
+
+ |
+
+ {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}
diff --git a/src/components/admin/banner-form.tsx b/src/components/admin/banner-form.tsx
new file mode 100644
index 0000000..2566753
--- /dev/null
+++ b/src/components/admin/banner-form.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { useState } from "react";
+
+export default function BannerForm() {
+ const [message, setMessage] = useState("");
+
+ const onClickClear = async () => {
+ await fetch("/api/admin/banner", { method: "DELETE" });
+ };
+
+ const onClickSet = async () => {
+ await fetch("/api/admin/banner", { method: "POST", body: message });
+ };
+
+ return (
+
+
setMessage(e.target.value)} />
+
+
+
+
+
+ );
+}
diff --git a/src/components/admin/banner.tsx b/src/components/admin/banner.tsx
new file mode 100644
index 0000000..3a69344
--- /dev/null
+++ b/src/components/admin/banner.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import useSWR from "swr";
+import { Icon } from "@iconify/react";
+
+interface ApiResponse {
+ message: string;
+}
+
+const fetcher = (url: string) => fetch(url).then((res) => res.json());
+
+export default function AdminBanner() {
+ const { data } = useSWR("/api/admin/banner", fetcher);
+ if (!data || !data.message) return null;
+
+ return (
+
+
+ {data.message}
+
+ );
+}