mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 13:17:45 +00:00
Compare commits
No commits in common. "aa631095faaf262a2be359b0d1790599abca685a" and "e1885fd8fed2d963729d20f5a42ea71fcfd944b6" have entirely different histories.
aa631095fa
...
e1885fd8fe
12 changed files with 138 additions and 121 deletions
|
|
@ -3,3 +3,5 @@ ALTER TABLE "miis" ADD COLUMN "likeCount" INTEGER NOT NULL DEFAULT 0;
|
|||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "miis_likeCount_idx" ON "miis"("likeCount" DESC);
|
||||
|
||||
UPDATE miis SET "likeCount" = (SELECT COUNT(*) FROM likes WHERE likes."miiId" = miis.id);
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `punishmentId` on the `miis` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `notes` on the `punishments` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `reasons` on the `punishments` table. All the data in the column will be lost.
|
||||
- You are about to drop the `mii_punishments` table. If the table is not empty, all the data it contains will be lost.
|
||||
- Added the required column `reason` to the `punishments` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "mii_punishments" DROP CONSTRAINT "mii_punishments_miiId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "mii_punishments" DROP CONSTRAINT "mii_punishments_punishmentId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "miis" DROP COLUMN "punishmentId";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "punishments" RENAME COLUMN "notes" TO "reason";
|
||||
ALTER TABLE "punishments" DROP COLUMN "reasons";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "mii_punishments";
|
||||
|
|
@ -90,9 +90,12 @@ model Mii {
|
|||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
likeCount Int @default(0)
|
||||
likedBy Like[]
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
likeCount Int @default(0)
|
||||
|
||||
punishmentId Int?
|
||||
punishments MiiPunishment[]
|
||||
likedBy Like[]
|
||||
|
||||
@@index([tags], type: Gin)
|
||||
@@index([createdAt])
|
||||
|
|
@ -139,13 +142,28 @@ model Report {
|
|||
@@map("reports")
|
||||
}
|
||||
|
||||
model MiiPunishment {
|
||||
punishmentId Int
|
||||
miiId Int
|
||||
reason String
|
||||
|
||||
punishment Punishment @relation(fields: [punishmentId], references: [id], onDelete: Cascade)
|
||||
mii Mii @relation(fields: [miiId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([punishmentId, miiId])
|
||||
@@map("mii_punishments")
|
||||
}
|
||||
|
||||
model Punishment {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
type PunishmentType
|
||||
returned Boolean @default(false)
|
||||
|
||||
reason String
|
||||
notes String
|
||||
reasons String[]
|
||||
violatingMiis MiiPunishment[]
|
||||
|
||||
expiresAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,15 @@ export default async function AdminPage({ searchParams }: Props) {
|
|||
</div>
|
||||
|
||||
<BannerForm />
|
||||
|
||||
{/* Separator */}
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium my-1">
|
||||
<hr className="grow border-zinc-300" />
|
||||
<span>Control Center</span>
|
||||
<hr className="grow border-zinc-300" />
|
||||
</div>
|
||||
|
||||
<ControlCenter />
|
||||
<RegenerateImagesButton />
|
||||
|
||||
{/* Separator */}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,16 @@ export async function GET(request: NextRequest) {
|
|||
id: true,
|
||||
type: true,
|
||||
returned: true,
|
||||
reason: true,
|
||||
|
||||
notes: true,
|
||||
reasons: true,
|
||||
violatingMiis: {
|
||||
select: {
|
||||
miiId: true,
|
||||
reason: true,
|
||||
},
|
||||
},
|
||||
|
||||
expiresAt: true,
|
||||
createdAt: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,16 @@ const punishSchema = z.object({
|
|||
.number({ error: "Duration (days) must be a number" })
|
||||
.int({ error: "Duration (days) must be an integer" })
|
||||
.positive({ error: "Duration (days) must be valid" }),
|
||||
reason: z.string(),
|
||||
notes: z.string(),
|
||||
reasons: z.array(z.string()).optional(),
|
||||
miiReasons: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number({ error: "Mii ID must be a number" }).int({ error: "Mii ID must be an integer" }).positive({ error: "Mii ID must be valid" }),
|
||||
reason: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
|
|
@ -33,7 +42,7 @@ export async function POST(request: NextRequest) {
|
|||
const parsed = punishSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 });
|
||||
const { type, duration, reason } = parsed.data;
|
||||
const { type, duration, notes, reasons, miiReasons } = parsed.data;
|
||||
|
||||
const expiresAt = type === "TEMP_EXILE" ? dayjs().add(duration, "days").toDate() : null;
|
||||
|
||||
|
|
@ -42,7 +51,14 @@ export async function POST(request: NextRequest) {
|
|||
userId,
|
||||
type: type as PunishmentType,
|
||||
expiresAt,
|
||||
reason,
|
||||
notes,
|
||||
reasons: reasons?.length !== 0 ? reasons : [],
|
||||
violatingMiis: {
|
||||
create: miiReasons?.map((mii) => ({
|
||||
miiId: mii.id,
|
||||
reason: mii.reason,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,17 @@ export async function DELETE(request: NextRequest) {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,17 @@ export default async function ExiledPage() {
|
|||
userId: Number(session?.user.id),
|
||||
returned: false,
|
||||
},
|
||||
include: {
|
||||
violatingMiis: {
|
||||
include: {
|
||||
mii: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!activePunishment) redirect("/");
|
||||
|
|
@ -63,9 +74,36 @@ export default async function ExiledPage() {
|
|||
</p>
|
||||
|
||||
<p className="mt-1">
|
||||
<span className="font-bold">Reason:</span> {activePunishment.reason}
|
||||
<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="grow border-zinc-300" />
|
||||
<span>Violating Items</span>
|
||||
<hr className="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">{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" ? (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import { Icon } from "@iconify/react";
|
||||
import { Prisma, Punishment, PunishmentType } from "@prisma/client";
|
||||
import { Prisma, PunishmentType } from "@prisma/client";
|
||||
|
||||
import ProfilePicture from "../profile-picture";
|
||||
import SubmitButton from "../submit-button";
|
||||
|
|
@ -16,7 +16,11 @@ interface ApiResponse {
|
|||
name: string;
|
||||
image: string;
|
||||
createdAt: string;
|
||||
punishments: Punishment[];
|
||||
punishments: Prisma.PunishmentGetPayload<{
|
||||
include: {
|
||||
violatingMiis: true;
|
||||
};
|
||||
}>[];
|
||||
}
|
||||
|
||||
interface MiiList {
|
||||
|
|
@ -166,7 +170,7 @@ export default function Punishments() {
|
|||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-600">
|
||||
<strong>Reason:</strong> {punishment.reason}
|
||||
<strong>Notes:</strong> {punishment.notes}
|
||||
</p>
|
||||
{punishment.type !== "WARNING" && (
|
||||
<p className="text-sm text-zinc-600">
|
||||
|
|
@ -181,6 +185,24 @@ export default function Punishments() {
|
|||
<strong>Returned:</strong> {JSON.stringify(punishment.returned)}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-zinc-600">
|
||||
<strong>Reasons:</strong>
|
||||
</p>
|
||||
<ul className="ml-8 list-disc text-sm text-zinc-600">
|
||||
{punishment.reasons.map((reason, index) => (
|
||||
<li key={index}>{reason}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-sm text-zinc-600">
|
||||
<strong>Mii Reasons:</strong>
|
||||
</p>
|
||||
<ul className="ml-8 list-disc text-sm text-zinc-600">
|
||||
{punishment.violatingMiis.map((mii) => (
|
||||
<li key={mii.miiId}>
|
||||
{mii.miiId}: {mii.reason}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { auth } from "@/lib/auth";
|
|||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
import SortSelect from "./sort-select";
|
||||
import TimeRangeSelect from "./time-range-select";
|
||||
import Pagination from "./pagination";
|
||||
import FilterMenu from "./filter-menu";
|
||||
import MiiGrid from "./mii-grid";
|
||||
|
|
@ -21,7 +20,7 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
|||
const parsed = searchSchema.safeParse(searchParams);
|
||||
if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
|
||||
|
||||
const { q: query, sort, tags, exclude, platform, gender, makeup, allowCopying, quarantined, page = 1, limit = 24, timeRange } = parsed.data;
|
||||
const { q: query, sort, tags, exclude, platform, gender, makeup, allowCopying, quarantined, page = 1, limit = 24 } = parsed.data;
|
||||
|
||||
// My Likes page
|
||||
let miiIdsLiked: number[] | undefined = undefined;
|
||||
|
|
@ -67,14 +66,6 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
|||
...(makeup && { makeup: { equals: makeup } }),
|
||||
// Quarantined
|
||||
...(!quarantined && !userId && { quarantined: false }),
|
||||
// Time range
|
||||
...(timeRange && {
|
||||
createdAt: {
|
||||
gte: new Date(
|
||||
Date.now() - { day: 86400000, week: 604800000, month: 2592000000, year: 31536000000 }[timeRange],
|
||||
),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const select: Prisma.MiiSelect = {
|
||||
|
|
@ -149,7 +140,6 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
|||
<div className="relative flex items-center justify-end gap-2 w-full md:max-w-2/3 max-md:justify-center">
|
||||
<FilterMenu />
|
||||
<SortSelect />
|
||||
<TimeRangeSelect />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
import { useSelect } from "downshift";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
type TimeRange = "day" | "week" | "month" | "year";
|
||||
|
||||
const items: { value: TimeRange | "all"; label: string }[] = [
|
||||
{ value: "all", label: "all time" },
|
||||
{ value: "day", label: "today" },
|
||||
{ value: "week", label: "this week" },
|
||||
{ value: "month", label: "this month" },
|
||||
{ value: "year", label: "this year" },
|
||||
];
|
||||
|
||||
export default function TimeRangeSelect() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
const currentRange = (searchParams.get("timeRange") as TimeRange) || "all";
|
||||
const currentItem = items.find((i) => i.value === currentRange) || items[0];
|
||||
|
||||
const { isOpen, getToggleButtonProps, getMenuProps, getItemProps, highlightedIndex, selectedItem } = useSelect({
|
||||
items,
|
||||
selectedItem: currentItem,
|
||||
itemToString: (item) => item?.label || "",
|
||||
onSelectedItemChange: ({ selectedItem }) => {
|
||||
if (!selectedItem) return;
|
||||
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("page", "1");
|
||||
|
||||
if (selectedItem.value === "all") {
|
||||
params.delete("timeRange");
|
||||
} else {
|
||||
params.set("timeRange", selectedItem.value);
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative w-fit">
|
||||
<button type="button" {...getToggleButtonProps()} aria-label="Time range dropdown" className="pill input w-full gap-1 justify-between! text-nowrap">
|
||||
<Icon icon="mdi:clock-outline" className="size-5" />
|
||||
{selectedItem?.label || "all time"}
|
||||
<Icon icon="tabler:chevron-down" className="ml-1 size-5" />
|
||||
</button>
|
||||
|
||||
<ul
|
||||
{...getMenuProps()}
|
||||
className={`absolute z-50 w-full bg-orange-200 border-2 border-orange-400 rounded-lg mt-1 shadow-lg max-h-60 overflow-y-auto ${
|
||||
isOpen ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
{isOpen &&
|
||||
items.map((item, index) => (
|
||||
<li key={item.value} {...getItemProps({ item, index })} className={`px-4 py-1 cursor-pointer text-sm ${highlightedIndex === index ? "bg-black/15" : ""}`}>
|
||||
{item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -72,8 +72,6 @@ export const searchSchema = z.object({
|
|||
.max(100, { error: "Limit cannot be more than 100" })
|
||||
.optional(),
|
||||
page: z.coerce.number({ error: "Page must be a number" }).int({ error: "Page must be an integer" }).min(1, { error: "Page must be at least 1" }).optional(),
|
||||
// Time range filter
|
||||
timeRange: z.enum(["day", "week", "month", "year"], { error: "Time range must be either 'day', 'week', 'month', or 'year'" }).optional(),
|
||||
});
|
||||
|
||||
export const userNameSchema = z
|
||||
|
|
|
|||
Loading…
Reference in a new issue