feat: punishment page
This commit is contained in:
parent
780e147f32
commit
aef188f7c8
12 changed files with 401 additions and 29 deletions
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "punishments" ADD COLUMN "returned" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
@ -136,9 +136,10 @@ model MiiPunishment {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Punishment {
|
model Punishment {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId Int
|
userId Int
|
||||||
type PunishmentType
|
type PunishmentType
|
||||||
|
returned Boolean @default(false)
|
||||||
|
|
||||||
notes String
|
notes String
|
||||||
reasons String[]
|
reasons String[]
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,14 @@ export async function GET(request: NextRequest) {
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
punishments: {
|
punishments: {
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
type: true,
|
type: true,
|
||||||
|
returned: true,
|
||||||
|
|
||||||
notes: true,
|
notes: true,
|
||||||
reasons: true,
|
reasons: true,
|
||||||
violatingMiis: {
|
violatingMiis: {
|
||||||
|
|
@ -33,6 +38,7 @@ export async function GET(request: NextRequest) {
|
||||||
reason: true,
|
reason: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
expiresAt: true,
|
expiresAt: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -67,3 +67,24 @@ export async function POST(request: NextRequest) {
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function DELETE(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 searchParams = request.nextUrl.searchParams;
|
||||||
|
const parsedPunishmentId = idSchema.safeParse(searchParams.get("id"));
|
||||||
|
|
||||||
|
if (!parsedPunishmentId.success) return NextResponse.json({ error: parsedPunishmentId.error.errors[0].message }, { status: 400 });
|
||||||
|
const punishmentId = parsedPunishmentId.data;
|
||||||
|
|
||||||
|
await prisma.punishment.delete({
|
||||||
|
where: {
|
||||||
|
id: punishmentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
}
|
||||||
|
|
|
||||||
48
src/app/api/return/route.ts
Normal file
48
src/app/api/return/route.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
|
const rateLimit = new RateLimit(request, 1);
|
||||||
|
const check = await rateLimit.handle();
|
||||||
|
if (check) return check;
|
||||||
|
|
||||||
|
const activePunishment = await prisma.punishment.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: Number(session.user.id),
|
||||||
|
returned: false,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
violatingMiis: {
|
||||||
|
include: {
|
||||||
|
mii: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activePunishment) return rateLimit.sendResponse({ error: "You have no active punishments!" }, 404);
|
||||||
|
if (activePunishment.type === "PERM_EXILE") return rateLimit.sendResponse({ error: "Your punishment is permanent" }, 403);
|
||||||
|
if (activePunishment.type === "TEMP_EXILE" && activePunishment.expiresAt! > new Date())
|
||||||
|
return rateLimit.sendResponse({ error: "Your punishment has not expired yet." }, 403);
|
||||||
|
|
||||||
|
await prisma.punishment.update({
|
||||||
|
where: {
|
||||||
|
id: activePunishment.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
returned: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return rateLimit.sendResponse({ success: true });
|
||||||
|
}
|
||||||
|
|
@ -39,6 +39,10 @@ body {
|
||||||
@apply hover:bg-orange-400 transition cursor-pointer;
|
@apply hover:bg-orange-400 transition cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
@apply text-zinc-600 !bg-zinc-100 !border-zinc-300 cursor-auto;
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
@apply !bg-orange-200 outline-0 focus:ring-[3px] ring-orange-400/50 transition placeholder:text-black/40;
|
@apply !bg-orange-200 outline-0 focus:ring-[3px] ring-orange-400/50 transition placeholder:text-black/40;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
124
src/app/off-the-island/page.tsx
Normal file
124
src/app/off-the-island/page.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
import ReturnToIsland from "@/components/admin/return-to-island";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Exiled - TomodachiShare",
|
||||||
|
description: "You have been exiled from the TomodachiShare island...",
|
||||||
|
robots: {
|
||||||
|
index: false,
|
||||||
|
follow: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function ExiledPage() {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user) redirect("/");
|
||||||
|
|
||||||
|
const activePunishment = await prisma.punishment.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: Number(session?.user.id),
|
||||||
|
returned: false,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
violatingMiis: {
|
||||||
|
include: {
|
||||||
|
mii: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activePunishment) redirect("/");
|
||||||
|
|
||||||
|
const expiresAt = dayjs(activePunishment.expiresAt);
|
||||||
|
const createdAt = dayjs(activePunishment.createdAt);
|
||||||
|
|
||||||
|
const hasExpired = activePunishment.type === "TEMP_EXILE" && activePunishment.expiresAt! > new Date();
|
||||||
|
const duration = activePunishment.type === "TEMP_EXILE" && Math.ceil(expiresAt.diff(createdAt, "days", true));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-grow flex items-center justify-center">
|
||||||
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-8 max-w-xl w-full flex flex-col">
|
||||||
|
<h2 className="text-4xl font-black mb-2">
|
||||||
|
{activePunishment.type === "PERM_EXILE"
|
||||||
|
? "Exiled permanently"
|
||||||
|
: activePunishment.type === "TEMP_EXILE"
|
||||||
|
? `Exiled for ${duration} ${duration === 1 ? "day" : "days"}`
|
||||||
|
: "Warning"}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
You have been exiled from the TomodachiShare island because you violated the{" "}
|
||||||
|
<Link href={"/terms-of-service"} className="text-blue-500">
|
||||||
|
Terms of Service
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-3">
|
||||||
|
<span className="font-bold">Reviewed:</span> {activePunishment.createdAt.toLocaleDateString("en-GB")} at{" "}
|
||||||
|
{activePunishment.createdAt.toLocaleString("en-GB")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-1">
|
||||||
|
<span className="font-bold">Note:</span> {activePunishment.notes}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-4">
|
||||||
|
<hr className="flex-grow border-zinc-300" />
|
||||||
|
<span>Violating Items</span>
|
||||||
|
<hr className="flex-grow border-zinc-300" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 p-4">
|
||||||
|
{activePunishment.reasons.map((index, reason) => (
|
||||||
|
<div key={index} className="bg-orange-100 rounded-xl border-2 border-orange-400 p-4">
|
||||||
|
<p>
|
||||||
|
<span className="font-bold">Reason:</span> {reason}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{activePunishment.violatingMiis.map((mii) => (
|
||||||
|
<div key={mii.miiId} className="bg-orange-100 rounded-xl border-2 border-orange-400 flex">
|
||||||
|
<Image src={`/mii/${mii.miiId}/image?type=mii`} alt="mii image" width={96} height={96} />
|
||||||
|
<div className="p-4">
|
||||||
|
<p className="text-xl font-bold line-clamp-1" title={"hello"}>
|
||||||
|
{mii.mii.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
<span className="font-bold">Reason:</span> {mii.reason}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-zinc-300 mt-2 mb-4" />
|
||||||
|
|
||||||
|
{activePunishment.type !== "PERM_EXILE" ? (
|
||||||
|
<>
|
||||||
|
<p className="mb-2">Once your punishment ends, you can return by checking the box below.</p>
|
||||||
|
<ReturnToIsland hasExpired={hasExpired} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>Your punishment is permanent, therefore you cannot return.</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
import MiiList from "@/components/mii-list";
|
import MiiList from "@/components/mii-list";
|
||||||
import Skeleton from "@/components/mii-list/skeleton";
|
import Skeleton from "@/components/mii-list/skeleton";
|
||||||
import { Metadata } from "next";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
|
|
@ -39,6 +40,13 @@ export default async function Page({ searchParams }: Props) {
|
||||||
if (session?.user && !session.user.username) {
|
if (session?.user && !session.user.username) {
|
||||||
redirect("/create-username");
|
redirect("/create-username");
|
||||||
}
|
}
|
||||||
|
const activePunishment = await prisma.punishment.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: Number(session?.user.id),
|
||||||
|
returned: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (activePunishment) redirect("/off-the-island");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import Link from "next/link";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
import SubmitForm from "@/components/submit-form";
|
import SubmitForm from "@/components/submit-form";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|
@ -21,6 +23,13 @@ export default async function SubmitPage() {
|
||||||
|
|
||||||
if (!session) redirect("/login");
|
if (!session) redirect("/login");
|
||||||
if (!session.user.username) redirect("/create-username");
|
if (!session.user.username) redirect("/create-username");
|
||||||
|
const activePunishment = await prisma.punishment.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: Number(session?.user.id),
|
||||||
|
returned: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (activePunishment) redirect("/off-the-island");
|
||||||
|
|
||||||
// Check if submissions are disabled
|
// Check if submissions are disabled
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`);
|
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`);
|
||||||
|
|
|
||||||
94
src/components/admin/punishment-deletion-dialog.tsx
Normal file
94
src/components/admin/punishment-deletion-dialog.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import SubmitButton from "../submit-button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
punishmentId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PunishmentDeletionDialog({ punishmentId }: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const response = await fetch(`/api/admin/punish?id=${punishmentId}`, { method: "DELETE" });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setError(data.error);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
// slight delay to trigger animation
|
||||||
|
setTimeout(() => setIsVisible(true), 10);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={() => setIsOpen(true)} className="text-red-500 cursor-pointer hover:text-red-600 text-lg">
|
||||||
|
<Icon icon="material-symbols:close-rounded" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen &&
|
||||||
|
createPortal(
|
||||||
|
<div className="fixed inset-0 w-full h-[calc(100%-var(--header-height))] top-[var(--header-height)] flex items-center justify-center z-40">
|
||||||
|
<div
|
||||||
|
onClick={close}
|
||||||
|
className={`z-40 absolute inset-0 backdrop-brightness-75 backdrop-blur-xs transition-opacity duration-300 ${
|
||||||
|
isVisible ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`z-50 bg-orange-50 border-2 border-amber-500 rounded-2xl shadow-lg p-6 w-full max-w-md transition-discrete duration-300 flex flex-col ${
|
||||||
|
isVisible ? "scale-100 opacity-100" : "scale-75 opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<h2 className="text-xl font-bold">Punishment Deletion</h2>
|
||||||
|
<button onClick={close} className="text-red-400 hover:text-red-500 text-2xl cursor-pointer">
|
||||||
|
<Icon icon="material-symbols:close-rounded" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-zinc-500">Are you sure? This will delete the user‘s punishment and they will be able to come back.</p>
|
||||||
|
|
||||||
|
{error && <span className="text-red-400 font-bold mt-2">Error: {error}</span>}
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 mt-4">
|
||||||
|
<button onClick={close} className="pill button">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<SubmitButton onClick={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/components/admin/return-to-island.tsx
Normal file
53
src/components/admin/return-to-island.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
hasExpired: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReturnToIsland({ hasExpired }: Props) {
|
||||||
|
const [isChecked, setIsChecked] = useState(false);
|
||||||
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
const response = await fetch("/api/return", { method: "DELETE" });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setError(data.error);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect("/");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-center items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="agreement"
|
||||||
|
disabled={hasExpired}
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={(e) => setIsChecked(e.target.checked)}
|
||||||
|
className={`checkbox ${hasExpired && "text-zinc-600 !bg-zinc-100 !border-zinc-300"}`}
|
||||||
|
/>
|
||||||
|
<label htmlFor="agreement" className={`${hasExpired && "text-zinc-500"}`}>
|
||||||
|
I Agree
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-zinc-300 mt-3 mb-4" />
|
||||||
|
|
||||||
|
{error && <span className="text-red-400 font-bold mb-2.5">Error: {error}</span>}
|
||||||
|
<button disabled={!isChecked} onClick={handleClick} className="pill button gap-2 w-fit self-center">
|
||||||
|
<Icon icon="ic:round-home" fontSize={24} />
|
||||||
|
Travel Back
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,10 @@ import Image from "next/image";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { PunishmentType } from "@prisma/client";
|
import { Prisma, PunishmentType } from "@prisma/client";
|
||||||
|
|
||||||
import SubmitButton from "../submit-button";
|
import SubmitButton from "../submit-button";
|
||||||
|
import PunishmentDeletionDialog from "./punishment-deletion-dialog";
|
||||||
|
|
||||||
interface ApiResponse {
|
interface ApiResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|
@ -16,21 +17,11 @@ interface ApiResponse {
|
||||||
username: string;
|
username: string;
|
||||||
image: string;
|
image: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
punishments: {
|
punishments: Prisma.PunishmentGetPayload<{
|
||||||
id: number;
|
include: {
|
||||||
userId: number;
|
violatingMiis: true;
|
||||||
type: string;
|
};
|
||||||
|
}>[];
|
||||||
notes: string;
|
|
||||||
reasons: string[];
|
|
||||||
violatingMiis: {
|
|
||||||
miiId: number;
|
|
||||||
reason: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
expiresAt: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MiiList {
|
interface MiiList {
|
||||||
|
|
@ -172,19 +163,30 @@ export default function Punishments() {
|
||||||
>
|
>
|
||||||
{punishment.type}
|
{punishment.type}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-zinc-600">
|
|
||||||
{new Date(punishment.createdAt).toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })}
|
<div className="flex items-center gap-2">
|
||||||
</span>
|
<span className="text-sm text-zinc-600">
|
||||||
|
{new Date(punishment.createdAt).toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })}
|
||||||
|
</span>
|
||||||
|
<PunishmentDeletionDialog punishmentId={punishment.id} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-zinc-600">
|
<p className="text-sm text-zinc-600">
|
||||||
<strong>Notes:</strong> {punishment.notes}
|
<strong>Notes:</strong> {punishment.notes}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-zinc-600">
|
{punishment.type !== "WARNING" && (
|
||||||
<strong>Expires:</strong>{" "}
|
<p className="text-sm text-zinc-600">
|
||||||
{punishment.expiresAt
|
<strong>Expires:</strong>{" "}
|
||||||
? new Date(punishment.expiresAt).toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })
|
{punishment.expiresAt
|
||||||
: "Never"}
|
? new Date(punishment.expiresAt).toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })
|
||||||
</p>
|
: "Never"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{punishment.type !== "PERM_EXILE" && (
|
||||||
|
<p className="text-sm text-zinc-600">
|
||||||
|
<strong>Returned:</strong> {JSON.stringify(punishment.returned)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<p className="text-sm text-zinc-600">
|
<p className="text-sm text-zinc-600">
|
||||||
<strong>Reasons:</strong>
|
<strong>Reasons:</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue