mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 13:17:45 +00:00
Compare commits
3 commits
5f45f205f4
...
cdd41c6566
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdd41c6566 | ||
| 28bbaa52f1 | |||
|
|
8df69bcd79 |
11 changed files with 154 additions and 63 deletions
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "miis" ADD COLUMN "likeCount" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "miis_likeCount_idx" ON "miis"("likeCount" DESC);
|
||||||
|
|
||||||
|
UPDATE miis SET like_count = (SELECT COUNT(*) FROM likes WHERE likes."miiId" = miis.id);
|
||||||
|
|
@ -91,13 +91,15 @@ model Mii {
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
likedBy Like[]
|
likeCount Int @default(0)
|
||||||
|
|
||||||
punishmentId Int?
|
punishmentId Int?
|
||||||
punishments MiiPunishment[]
|
punishments MiiPunishment[]
|
||||||
|
likedBy Like[]
|
||||||
|
|
||||||
@@index([tags], type: Gin)
|
@@index([tags], type: Gin)
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
|
@@index([likeCount(sort: Desc)])
|
||||||
@@index([quarantined, createdAt(sort: Desc)])
|
@@index([quarantined, createdAt(sort: Desc)])
|
||||||
@@index([platform, createdAt(sort: Desc)])
|
@@index([platform, createdAt(sort: Desc)])
|
||||||
@@index([userId, createdAt(sort: Desc)])
|
@@index([userId, createdAt(sort: Desc)])
|
||||||
|
|
|
||||||
|
|
@ -5,55 +5,57 @@ import { prisma } from "@/lib/prisma";
|
||||||
import { idSchema } from "@/lib/schemas";
|
import { idSchema } from "@/lib/schemas";
|
||||||
import { RateLimit } from "@/lib/rate-limit";
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
// const session = await auth();
|
const session = await auth();
|
||||||
// if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
|
||||||
// const rateLimit = new RateLimit(request, 100, "/api/mii/like");
|
const rateLimit = new RateLimit(request, 100, "/api/mii/like");
|
||||||
// const check = await rateLimit.handle();
|
const check = await rateLimit.handle();
|
||||||
// if (check) return check;
|
if (check) return check;
|
||||||
|
|
||||||
// const { id: slugId } = await params;
|
const { id: slugId } = await params;
|
||||||
// const parsed = idSchema.safeParse(slugId);
|
const parsed = idSchema.safeParse(slugId);
|
||||||
// if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
|
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
|
||||||
// const miiId = parsed.data;
|
const miiId = parsed.data;
|
||||||
|
|
||||||
// const result = await prisma.$transaction(async (tx) => {
|
const result = await prisma.$transaction(async (tx) => {
|
||||||
// const existingLike = await tx.like.findUnique({
|
const existingLike = await tx.like.findUnique({
|
||||||
// where: {
|
where: {
|
||||||
// userId_miiId: {
|
userId_miiId: {
|
||||||
// userId: Number(session.user?.id),
|
userId: Number(session.user?.id),
|
||||||
// miiId,
|
miiId,
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
// });
|
});
|
||||||
|
|
||||||
// if (existingLike) {
|
if (existingLike) {
|
||||||
// // Remove the like if it exists
|
await tx.like.delete({
|
||||||
// await tx.like.delete({
|
where: {
|
||||||
// where: {
|
userId_miiId: {
|
||||||
// userId_miiId: {
|
userId: Number(session.user?.id),
|
||||||
// userId: Number(session.user?.id),
|
miiId,
|
||||||
// miiId,
|
},
|
||||||
// },
|
},
|
||||||
// },
|
});
|
||||||
// });
|
await tx.mii.update({
|
||||||
// } else {
|
where: { id: miiId },
|
||||||
// // Add a like if it doesn't exist
|
data: { likeCount: { decrement: 1 } },
|
||||||
// await tx.like.create({
|
});
|
||||||
// data: {
|
} else {
|
||||||
// userId: Number(session.user?.id),
|
await tx.like.create({
|
||||||
// miiId,
|
data: {
|
||||||
// },
|
userId: Number(session.user?.id),
|
||||||
// });
|
miiId,
|
||||||
// }
|
},
|
||||||
|
});
|
||||||
// const likeCount = await tx.like.count({
|
await tx.mii.update({
|
||||||
// where: { miiId },
|
where: { id: miiId },
|
||||||
// });
|
data: { likeCount: { increment: 1 } },
|
||||||
|
});
|
||||||
// return { liked: !existingLike, count: likeCount };
|
}
|
||||||
// });
|
|
||||||
|
return { liked: !existingLike };
|
||||||
return NextResponse.json({ success: false });
|
});
|
||||||
|
|
||||||
|
return rateLimit.sendResponse({ success: true, liked: result.liked });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,5 +46,5 @@ export default async function MiiPage({ params }: Props) {
|
||||||
// Check ownership
|
// Check ownership
|
||||||
if (!mii || (Number(session?.user?.id) !== mii.userId && Number(session?.user?.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID))) redirect("/404");
|
if (!mii || (Number(session?.user?.id) !== mii.userId && Number(session?.user?.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID))) redirect("/404");
|
||||||
|
|
||||||
return <EditForm mii={mii} likes={mii._count.likedBy} />;
|
return <EditForm mii={mii} likes={mii.likeCount} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,13 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
return {
|
return {
|
||||||
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL!),
|
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL!),
|
||||||
title: `${mii.name} - TomodachiShare`,
|
title: `${mii.name} - TomodachiShare`,
|
||||||
description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii._count.likedBy} likes.`,
|
description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii.likeCount} likes.`,
|
||||||
keywords: ["mii", "tomodachi life", "nintendo", "tomodachishare", "tomodachi-share", "mii creator", "mii collection", ...mii.tags],
|
keywords: ["mii", "tomodachi life", "nintendo", "tomodachishare", "tomodachi-share", "mii creator", "mii collection", ...mii.tags],
|
||||||
creator: name,
|
creator: name,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: "article",
|
type: "article",
|
||||||
title: `${mii.name} - TomodachiShare`,
|
title: `${mii.name} - TomodachiShare`,
|
||||||
description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii._count.likedBy} likes.`,
|
description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii.likeCount} likes.`,
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: metadataImageUrl,
|
url: metadataImageUrl,
|
||||||
|
|
@ -70,7 +70,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: `${mii.name} - TomodachiShare`,
|
title: `${mii.name} - TomodachiShare`,
|
||||||
description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii._count.likedBy} likes.`,
|
description: `Check out '${mii.name}', a ${mii.platform === MiiPlatform.SWITCH ? "Switch Living the Dream" : "3DS"} Tomodachi Life Mii created by ${mii.name} on TomodachiShare with ${mii.likeCount} likes.`,
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: metadataImageUrl,
|
url: metadataImageUrl,
|
||||||
|
|
@ -306,7 +306,7 @@ export default async function MiiPage({ params }: Props) {
|
||||||
{/* Submission name */}
|
{/* Submission name */}
|
||||||
<h1 className="text-4xl font-extrabold wrap-break-word whitespace-break-spaces text-amber-700 flex-1 min-w-0">{mii.name}</h1>
|
<h1 className="text-4xl font-extrabold wrap-break-word whitespace-break-spaces text-amber-700 flex-1 min-w-0">{mii.name}</h1>
|
||||||
{/* Like button */}
|
{/* Like button */}
|
||||||
<LikeButton likes={mii._count.likedBy ?? 0} miiId={mii.id} isLiked={false} big />
|
<LikeButton likes={mii.likeCount ?? 0} miiId={mii.id} isLiked={false} big />
|
||||||
</div>
|
</div>
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<div id="tags" className="flex flex-wrap gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
|
<div id="tags" className="flex flex-wrap gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export default async function ReportMiiPage({ params }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center w-full">
|
<div className="flex justify-center w-full">
|
||||||
<ReportMiiForm mii={mii} likes={mii._count.likedBy} />
|
<ReportMiiForm mii={mii} likes={mii.likeCount} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export default function AuthorButtons({ mii }: Props) {
|
||||||
<Icon icon="mdi:pencil" />
|
<Icon icon="mdi:pencil" />
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</Link>
|
</Link>
|
||||||
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy ?? 0} inMiiPage />
|
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount ?? 0} inMiiPage />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
import SortSelect from "./sort-select";
|
import SortSelect from "./sort-select";
|
||||||
|
import TimeRangeSelect from "./time-range-select";
|
||||||
import Pagination from "./pagination";
|
import Pagination from "./pagination";
|
||||||
import FilterMenu from "./filter-menu";
|
import FilterMenu from "./filter-menu";
|
||||||
import MiiGrid from "./mii-grid";
|
import MiiGrid from "./mii-grid";
|
||||||
|
|
@ -20,7 +21,7 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
||||||
const parsed = searchSchema.safeParse(searchParams);
|
const parsed = searchSchema.safeParse(searchParams);
|
||||||
if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
|
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 } = parsed.data;
|
const { q: query, sort, tags, exclude, platform, gender, makeup, allowCopying, quarantined, page = 1, limit = 24, timeRange } = parsed.data;
|
||||||
|
|
||||||
// My Likes page
|
// My Likes page
|
||||||
let miiIdsLiked: number[] | undefined = undefined;
|
let miiIdsLiked: number[] | undefined = undefined;
|
||||||
|
|
@ -66,6 +67,14 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
||||||
...(makeup && { makeup: { equals: makeup } }),
|
...(makeup && { makeup: { equals: makeup } }),
|
||||||
// Quarantined
|
// Quarantined
|
||||||
...(!quarantined && !userId && { quarantined: false }),
|
...(!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 = {
|
const select: Prisma.MiiSelect = {
|
||||||
|
|
@ -89,6 +98,7 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
||||||
allowedCopying: true,
|
allowedCopying: true,
|
||||||
quarantined: true,
|
quarantined: true,
|
||||||
in_queue: true,
|
in_queue: true,
|
||||||
|
likeCount: true,
|
||||||
// Mii liked check
|
// Mii liked check
|
||||||
...(session?.user?.id && {
|
...(session?.user?.id && {
|
||||||
likedBy: {
|
likedBy: {
|
||||||
|
|
@ -96,10 +106,6 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
||||||
select: { userId: true },
|
select: { userId: true },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
// Like count
|
|
||||||
_count: {
|
|
||||||
select: { likedBy: true },
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
@ -111,7 +117,7 @@ export default async function MiiList({ searchParams, userId, parentPage }: Prop
|
||||||
let orderBy: Prisma.MiiOrderByWithRelationInput[];
|
let orderBy: Prisma.MiiOrderByWithRelationInput[];
|
||||||
|
|
||||||
if (sort === "likes") {
|
if (sort === "likes") {
|
||||||
orderBy = [{ likedBy: { _count: "desc" } }, { name: "asc" }];
|
orderBy = [{ likeCount: "desc" }, { name: "asc" }];
|
||||||
} else if (sort === "oldest") {
|
} else if (sort === "oldest") {
|
||||||
orderBy = [{ createdAt: "asc" }, { name: "asc" }];
|
orderBy = [{ createdAt: "asc" }, { name: "asc" }];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -143,6 +149,7 @@ 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">
|
<div className="relative flex items-center justify-end gap-2 w-full md:max-w-2/3 max-md:justify-center">
|
||||||
<FilterMenu />
|
<FilterMenu />
|
||||||
<SortSelect />
|
<SortSelect />
|
||||||
|
<TimeRangeSelect />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ export default function MiiGrid({ miis, userId, parentPage }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-auto grid grid-cols-2 items-center">
|
<div className="mt-auto grid grid-cols-2 items-center">
|
||||||
<LikeButton likes={mii._count.likedBy} miiId={mii.id} isLiked={likedIds.has(mii.id)} abbreviate />
|
<LikeButton likes={mii.likeCount} miiId={mii.id} isLiked={likedIds.has(mii.id)} abbreviate />
|
||||||
|
|
||||||
{!userId && (
|
{!userId && (
|
||||||
<Link href={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
|
<Link href={`/profile/${mii.user?.id}`} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
|
@ -87,7 +87,7 @@ export default function MiiGrid({ miis, userId, parentPage }: Props) {
|
||||||
<Link href={`/edit/${mii.id}`} title="Edit Mii" aria-label="Edit Mii" data-tooltip="Edit">
|
<Link href={`/edit/${mii.id}`} title="Edit Mii" aria-label="Edit Mii" data-tooltip="Edit">
|
||||||
<Icon icon="mdi:pencil" />
|
<Icon icon="mdi:pencil" />
|
||||||
</Link>
|
</Link>
|
||||||
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
|
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -105,7 +105,7 @@ export default function MiiGrid({ miis, userId, parentPage }: Props) {
|
||||||
<Icon icon="material-symbols:check-rounded" />
|
<Icon icon="material-symbols:check-rounded" />
|
||||||
</button>
|
</button>
|
||||||
<div className="text-zinc-400 hover:text-red-500 transition-colors p-1 bg-white rounded-md shadow-sm border border-zinc-200 hover:border-red-500 flex items-center justify-center">
|
<div className="text-zinc-400 hover:text-red-500 transition-colors p-1 bg-white rounded-md shadow-sm border border-zinc-200 hover:border-red-500 flex items-center justify-center">
|
||||||
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy} />
|
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii.likeCount} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
71
src/components/mii/list/time-range-select.tsx
Normal file
71
src/components/mii/list/time-range-select.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
"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,6 +72,8 @@ export const searchSchema = z.object({
|
||||||
.max(100, { error: "Limit cannot be more than 100" })
|
.max(100, { error: "Limit cannot be more than 100" })
|
||||||
.optional(),
|
.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(),
|
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
|
export const userNameSchema = z
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue