feat: redesign reports in admin panel
This commit is contained in:
parent
5339fdd95e
commit
0c7be71b2c
5 changed files with 176 additions and 111 deletions
|
|
@ -1,16 +1,11 @@
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import Link from "next/link";
|
|
||||||
import { redirect } from "next/navigation";
|
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 { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
import BannerForm from "@/components/admin/banner-form";
|
import BannerForm from "@/components/admin/banner-form";
|
||||||
import ControlCenter from "@/components/admin/control-center";
|
import ControlCenter from "@/components/admin/control-center";
|
||||||
|
import Reports from "@/components/admin/reports";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Admin - TomodachiShare",
|
title: "Admin - TomodachiShare",
|
||||||
|
|
@ -26,21 +21,6 @@ export default async function AdminPage() {
|
||||||
|
|
||||||
if (!session || Number(session.user.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID)) redirect("/404");
|
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 (
|
return (
|
||||||
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-4">
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -73,83 +53,7 @@ export default async function AdminPage() {
|
||||||
<hr className="flex-grow border-zinc-300" />
|
<hr className="flex-grow border-zinc-300" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 w-full overflow-x-scroll">
|
<Reports />
|
||||||
<table className="w-full text-sm table-fixed rounded-xl overflow-hidden min-w-5xl">
|
|
||||||
<thead className="bg-orange-200 rounded">
|
|
||||||
<tr className=" border-b-2 border-orange-300 *:px-4 *:py-2 *:font-semibold *:text-left">
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Target</th>
|
|
||||||
<th>Reason</th>
|
|
||||||
<th>Notes</th>
|
|
||||||
<th>Reporter</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{reports.map((report, index) => (
|
|
||||||
<tr key={index} className="*:px-4 *:py-2">
|
|
||||||
<td>
|
|
||||||
<span
|
|
||||||
className={`text-xs font-semibold px-2 py-1 rounded-full border ${
|
|
||||||
report.reportType == "USER" ? "bg-red-200 text-red-800" : "bg-cyan-200 text-cyan-800"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{report.reportType}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span
|
|
||||||
className={`text-xs font-semibold px-2 py-1 rounded-full border ${
|
|
||||||
report.status == "OPEN"
|
|
||||||
? "bg-orange-200 text-orange-800"
|
|
||||||
: report.status == "RESOLVED"
|
|
||||||
? "bg-green-200 text-green-800"
|
|
||||||
: "bg-zinc-200 text-zinc-800"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{report.status}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="font-bold text-blue-500">
|
|
||||||
<Link href={report.reportType === "MII" ? `/mii/${report.targetId}` : `/profile/${report.targetId}`}>{report.targetId}</Link>
|
|
||||||
</td>
|
|
||||||
<td>{report.reason}</td>
|
|
||||||
<td className="italic">{report.reasonNotes}</td>
|
|
||||||
<td className="font-bold text-blue-500">
|
|
||||||
<Link href={`/profile/${report.authorId}`}>{report.authorId}</Link>
|
|
||||||
</td>
|
|
||||||
<td className="flex items-center text-2xl *:flex">
|
|
||||||
<form action={updateStatus}>
|
|
||||||
<input type="hidden" name="id" value={report.id} />
|
|
||||||
<input type="hidden" name="status" value={"OPEN"} />
|
|
||||||
|
|
||||||
<button type="submit" data-tooltip="Mark as OPEN" className="cursor-pointer text-orange-300">
|
|
||||||
<Icon icon="mdi:alert-circle" />
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form action={updateStatus}>
|
|
||||||
<input type="hidden" name="id" value={report.id} />
|
|
||||||
<input type="hidden" name="status" value={"RESOLVED"} />
|
|
||||||
|
|
||||||
<button type="submit" data-tooltip="Mark as RESOLVED" className="cursor-pointer text-green-400">
|
|
||||||
<Icon icon="mdi:check-circle" />
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form action={updateStatus}>
|
|
||||||
<input type="hidden" name="id" value={report.id} />
|
|
||||||
<input type="hidden" name="status" value={"DISMISSED"} />
|
|
||||||
|
|
||||||
<button type="submit" data-tooltip="Mark as DISMISSED" className="cursor-pointer text-zinc-400">
|
|
||||||
<Icon icon="mdi:close-circle" />
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -195,8 +195,17 @@ export default async function MiiPage({ params }: Props) {
|
||||||
By: <span className="font-bold">@{mii.user.username}</span>
|
By: <span className="font-bold">@{mii.user.username}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<h4 className="text-sm">
|
<h4 className="text-sm">
|
||||||
Created: {mii.createdAt.toLocaleDateString("en-GB", { month: "long", day: "2-digit", year: "numeric" })} at{" "}
|
Created:{" "}
|
||||||
{mii.createdAt.toLocaleTimeString("en-GB", { timeZone: "UTC" })} UTC
|
{mii.createdAt.toLocaleString("en-GB", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
timeZone: "UTC",
|
||||||
|
})}{" "}
|
||||||
|
UTC
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,8 @@ export default function BannerForm() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 p-2 flex flex-col gap-2">
|
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 p-2 flex gap-2">
|
||||||
<input type="text" className="pill input w-full" placeholder="Enter banner text" value={message} onChange={(e) => setMessage(e.target.value)} />
|
<input type="text" className="pill input w-full" placeholder="Enter banner text" value={message} onChange={(e) => setMessage(e.target.value)} />
|
||||||
<div className="flex gap-2 self-end">
|
|
||||||
<button type="button" className="pill button" onClick={onClickClear}>
|
<button type="button" className="pill button" onClick={onClickClear}>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -24,6 +23,5 @@ export default function BannerForm() {
|
||||||
Set
|
Set
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@ export default function ControlCenter() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 p-2 flex flex-col gap-2">
|
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 p-2 flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-2">
|
||||||
<label htmlFor="submit">Submissions</label>
|
|
||||||
<input
|
<input
|
||||||
name="submit"
|
name="submit"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
@ -32,6 +31,7 @@ export default function ControlCenter() {
|
||||||
checked={canSubmit}
|
checked={canSubmit}
|
||||||
onChange={(e) => setCanSubmit(e.target.checked)}
|
onChange={(e) => setCanSubmit(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
|
<label htmlFor="submit">Enable Submissions</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 self-end">
|
<div className="flex gap-2 self-end">
|
||||||
|
|
|
||||||
154
src/components/admin/reports.tsx
Normal file
154
src/components/admin/reports.tsx
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { ReportStatus } from "@prisma/client";
|
||||||
|
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export default async function Reports() {
|
||||||
|
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 (
|
||||||
|
<div className="bg-orange-100 rounded-xl border-2 border-orange-400">
|
||||||
|
<div className="grid grid-cols-2 gap-2 p-2 max-lg:grid-cols-1">
|
||||||
|
{reports.map((report) => (
|
||||||
|
<div key={report.id} className="p-4 bg-orange-50 border border-orange-200 rounded-md">
|
||||||
|
<div className="w-full overflow-x-scroll">
|
||||||
|
<div className="flex gap-1 w-max">
|
||||||
|
<span
|
||||||
|
className={`text-xs font-semibold px-2 py-1 rounded-full border ${
|
||||||
|
report.reportType == "USER" ? "bg-red-200 text-red-800 border-orange-400" : "bg-cyan-200 text-cyan-800 border-cyan-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{report.reportType}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`text-xs font-semibold px-2 py-1 rounded-full border ${
|
||||||
|
report.status == "OPEN"
|
||||||
|
? "bg-orange-200 text-orange-800 border-orange-400"
|
||||||
|
: report.status == "RESOLVED"
|
||||||
|
? "bg-green-200 text-green-800 border-green-400"
|
||||||
|
: "bg-zinc-200 text-zinc-800 border-zinc-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{report.status}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="ml-2 flex items-center gap-1 text-sm text-zinc-500">
|
||||||
|
<Icon icon="lucide:calendar" className="text-base" />
|
||||||
|
{report.createdAt.toLocaleString("en-GB", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
timeZone: "UTC",
|
||||||
|
})}{" "}
|
||||||
|
UTC
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 text-xs text-zinc-600 mt-4 max-sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<p>Target ID</p>
|
||||||
|
<Link
|
||||||
|
href={report.reportType === "MII" ? `/mii/${report.targetId}` : `/profile/${report.targetId}`}
|
||||||
|
className="text-blue-600 text-sm"
|
||||||
|
>
|
||||||
|
{report.targetId}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Creator ID</p>
|
||||||
|
<Link href={`/profile/${report.creatorId}`} className="text-blue-600 text-sm">
|
||||||
|
{report.creatorId}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Reporter</p>
|
||||||
|
<Link href={`/profile/${report.authorId}`} className="text-blue-600 text-sm">
|
||||||
|
{report.authorId}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Reason</p>
|
||||||
|
<p className="font-medium text-black text-sm">{report.reason}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 border border-orange-200 bg-orange-100/50 rounded-md p-2">
|
||||||
|
<p className="text-zinc-600 text-xs">Notes</p>
|
||||||
|
<p>{report.reasonNotes}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex gap-4">
|
||||||
|
<form action={updateStatus}>
|
||||||
|
<input type="hidden" name="id" value={report.id} />
|
||||||
|
<input type="hidden" name="status" value={"OPEN"} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="cursor-pointer text-orange-400 flex items-center gap-1 p-1.5 rounded-lg transition-colors hover:bg-orange-400/15"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:alert-circle" className="text-xl" />
|
||||||
|
<span className="text-sm">Open</span>{" "}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form action={updateStatus}>
|
||||||
|
<input type="hidden" name="id" value={report.id} />
|
||||||
|
<input type="hidden" name="status" value={"RESOLVED"} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="cursor-pointer text-green-500 flex items-center gap-1 p-1.5 rounded-lg transition-colors hover:bg-green-500/15"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:check-circle" className="text-xl" />
|
||||||
|
<span className="text-sm">Resolve</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form action={updateStatus}>
|
||||||
|
<input type="hidden" name="id" value={report.id} />
|
||||||
|
<input type="hidden" name="status" value={"DISMISSED"} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="cursor-pointer text-zinc-400 flex items-center gap-1 p-1.5 rounded-lg transition-colors hover:bg-zinc-400/15"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:close-circle" className="text-xl" />
|
||||||
|
<span className="text-sm">Dismiss</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{reports.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
<p className="text-lg font-medium">No reports to display</p>
|
||||||
|
<p className="text-sm">Reports will appear here when users submit them</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue