feat: custom modifications

This commit is contained in:
dvelo 2025-04-04 21:10:50 -05:00
parent bddf5f1528
commit ed2d6f0ac1
21 changed files with 1686 additions and 875 deletions

@ -1,5 +1,5 @@
{ {
"name": "mh-stats", "name": "mhsf",
"version": "1.3.0", "version": "1.3.0",
"private": true, "private": true,
"packageManager": "yarn@1.22.22", "packageManager": "yarn@1.22.22",
@ -35,9 +35,15 @@
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "1.1.0", "@radix-ui/react-switch": "1.1.0",
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3",
"@tanstack/react-query": "^5.69.0",
"@trpc/client": "^11.0.0",
"@trpc/next": "^11.0.0",
"@trpc/react-query": "^11.0.0",
"@trpc/server": "^11.0.0",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/react": "^19.0.8", "@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"@types/request-ip": "^0.0.41",
"@unocss/eslint-plugin": "^0.61.5", "@unocss/eslint-plugin": "^0.61.5",
"@unocss/postcss": "^0.61.5", "@unocss/postcss": "^0.61.5",
"@unocss/transformer-directives": "^0.61.5", "@unocss/transformer-directives": "^0.61.5",
@ -80,8 +86,10 @@
"recharts": "^2.15.1", "recharts": "^2.15.1",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"request-ip": "^3.3.0",
"sonner": "^1.7.0", "sonner": "^1.7.0",
"stripe-gradient": "^1.0.1", "stripe-gradient": "^1.0.1",
"superjson": "^2.2.2",
"swapy": "^1.0.5", "swapy": "^1.0.5",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss": "^4.0.7", "tailwindcss": "^4.0.7",
@ -89,7 +97,8 @@
"tailwindcss-patch": "^4.0.0", "tailwindcss-patch": "^4.0.0",
"turbo": "^2.4.0", "turbo": "^2.4.0",
"unplugin-tailwindcss-mangle": "^3.0.1", "unplugin-tailwindcss-mangle": "^3.0.1",
"vaul": "^1.1.2" "vaul": "^1.1.2",
"zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@clerk/themes": "^2.1.19", "@clerk/themes": "^2.1.19",

@ -74,22 +74,22 @@ export default function RootLayout({
enableSystem enableSystem
disableTransitionOnChange disableTransitionOnChange
> >
<ClerkProvider> <ClerkProvider>
<IsScript> <IsScript>
<NuqsAdapter> <NuqsAdapter>
<FontBoundary> <FontBoundary>
<TooltipProvider> <TooltipProvider>
<Toaster richColors position="top-center" /> <Toaster richColors position="top-center" />
<ClerkProvider> <ClerkProvider>
<NavBar /> <NavBar />
<div className="pt-16 min-h-screen">{children}</div> <div className="pt-16 min-h-screen">{children}</div>
<Footer /> <Footer />
</ClerkProvider> </ClerkProvider>
</TooltipProvider> </TooltipProvider>
</FontBoundary> </FontBoundary>
</NuqsAdapter> </NuqsAdapter>
</IsScript> </IsScript>
</ClerkProvider> </ClerkProvider>
</ThemeProvider> </ThemeProvider>
</html> </html>
); );

@ -0,0 +1,122 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2025 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { ModificationAction } from "@/components/feat/server-list/modification/modification-action";
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import {
Setting,
SettingContent,
SettingDescription,
SettingMeta,
SettingTitle,
} from "@/components/feat/settings/setting";
import { Button } from "@/components/ui/button";
import { Material } from "@/components/ui/material";
import { Separator } from "@/components/ui/separator";
import { Link } from "@/components/util/link";
import { serverModDB } from "@/config/sl-mod-db";
import { useUser } from "@clerk/nextjs";
import { ArrowLeft, Filter, SortAsc } from "lucide-react";
import { useQueryState } from "nuqs";
import { use } from "react";
import Markdown from "react-markdown";
export default function ModificationPage({
params,
}: {
params: Promise<{ category: string; "custom-mod": string }>;
}) {
const { category, "custom-mod": mod } = use(params);
const { user } = useUser();
const [backRoute] = useQueryState("b", {
defaultValue: "/servers/embedded/sl-modification-frame",
});
console.log(mod);
const modObj = (
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
).find((c) => c.friendlyName === atob(decodeURIComponent(mod)));
if (modObj === undefined)
return <>We couldn't find the modification you were looking for.</>;
return (
<main className="max-w-[800px] p-4">
<div
className="h-[150px] w-full rounded-xl p-2"
style={{ backgroundColor: modObj?.color }}
>
<Link href={backRoute}>
<ArrowLeft />
</Link>
</div>
<span className="p-4">
<h1 className="text-xl font-bold w-full">{modObj?.friendlyName}</h1>
<Markdown className="text-wrap pt-2">
This is a custom modification. Enable it! (or not) It's your own! (are
you proud?)
</Markdown>
<Button className="mt-2">
{modObj?.active ? "Disable" : "Enable"}
</Button>
<Separator className="mt-3" />
<Material className="mt-6">
<Setting>
<SettingContent className="flex items-center">
<SettingMeta>
<SettingTitle>Type</SettingTitle>
<SettingDescription>
What type of modification is this?
</SettingDescription>
</SettingMeta>
<div className="flex items-center">
{modObj.testMode === "sort" ? (
<div className="flex items-center gap-1">
<SortAsc size={16} />
Sort
</div>
) : (
<div className="flex items-center gap-1">
<Filter size={16} /> Filter
</div>
)}
</div>
</SettingContent>
</Setting>
</Material>
</span>
</main>
);
}

@ -31,62 +31,95 @@
"use client"; "use client";
import { ModificationAction } from "@/components/feat/server-list/modification/modification-action"; import { ModificationAction } from "@/components/feat/server-list/modification/modification-action";
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import { Material } from "@/components/ui/material"; import { Material } from "@/components/ui/material";
import { Separator } from "@/components/ui/separator";
import { Link } from "@/components/util/link"; import { Link } from "@/components/util/link";
import { serverModDB } from "@/config/sl-mod-db"; import { serverModDB } from "@/config/sl-mod-db";
import { useRouter } from "@/lib/useRouter"; import { useRouter } from "@/lib/useRouter";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ArrowLeft } from "lucide-react"; import { SignedIn, useUser } from "@clerk/nextjs";
import { ArrowLeft, Binary } from "lucide-react";
import { use } from "react"; import { use } from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
export default function ServerListCategoryFrame({ export default function ServerListCategoryFrame({
params, params,
}: { }: {
params: Promise<{ category: string }>; params: Promise<{ category: string }>;
}) { }) {
const { category } = use(params); const { user } = useUser();
const categoryObj = serverModDB.find( const { category } = use(params);
(c) => c.displayTitle === atob(category) const categoryObj = serverModDB.find(
); (c) => c.displayTitle === atob(category),
const router = useRouter(); );
const router = useRouter();
return ( return (
<main className="max-w-[800px] p-4"> <main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full flex items-center gap-2"> <h1 className="text-xl font-bold w-full flex items-center gap-2">
<Link href="/servers/embedded/sl-modification-frame"> <Link href="/servers/embedded/sl-modification-frame">
<ArrowLeft size={20} /> <ArrowLeft size={20} />
</Link> </Link>
{categoryObj?.displayTitle} {categoryObj?.displayTitle}
</h1> </h1>
<Markdown className="text-wrap pt-2">{categoryObj?.description}</Markdown> <Markdown className="text-wrap pt-2">{categoryObj?.description}</Markdown>
<Material className="mt-10 p-4 grid grid-cols-6 gap-2"> <Material className="mt-10 p-4 grid grid-cols-6 gap-2">
{categoryObj?.entries.map((m) => ( {categoryObj?.entries.map((m) => (
<Material <Material
className="p-2 hover:drop-shadow-card-hover cursor-pointer" className="p-2 hover:drop-shadow-card-hover cursor-pointer"
elevation="high" elevation="high"
onClick={() => onClick={() =>
router.push( router.push(
`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}` `/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`,
) )
} }
key={m.name} key={m.name}
> >
<div <div
className={cn( className={cn(
"w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center" "w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center",
)} )}
style={{ backgroundColor: m.color }} style={{ backgroundColor: m.color }}
> >
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" /> <m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
</div> </div>
<span className="text-sm text-center w-full flex items-center justify-center"> <span className="text-sm text-center w-full flex items-center justify-center">
{m.name} {m.name}
</span> </span>
</Material> </Material>
))} ))}
</Material> <SignedIn>
</main> {categoryObj?.__custom &&
); (
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ??
[]
).map((m) => (
<Material
elevation="high"
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
key={m.friendlyName}
onClick={() =>
router.push(
`/servers/embedded/sl-modification-frame/category/${category}/modification/_custom/${btoa(m.friendlyName)}`,
)
}
>
<div
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
style={{ backgroundColor: m.color }}
>
<Binary className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
</div>
<span className="text-sm text-center w-full flex items-center justify-center">
{m.friendlyName}
</span>
</Material>
))}
</SignedIn>
</Material>
</main>
);
} }

@ -47,15 +47,21 @@ import {
Braces, Braces,
EllipsisVertical, EllipsisVertical,
FileCode, FileCode,
Filter,
Pencil, Pencil,
SortAsc,
Trash, Trash,
} from "lucide-react"; } from "lucide-react";
import { use } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { findSupportedOperations } from "../file/[filename]/page";
export default function ServerListModificationFrame() { export default function ServerListModificationFrame() {
const { user } = useUser(); const { user } = useUser();
const files = const files =
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []; (user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [];
const operations = use((async () => await Promise.all(files.map(async (c) => await findSupportedOperations(c.contents))))())
console.log(operations)
return ( return (
<main className="max-w-[800px] p-4"> <main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full flex items-center gap-2"> <h1 className="text-xl font-bold w-full flex items-center gap-2">
@ -80,6 +86,8 @@ export default function ServerListModificationFrame() {
> >
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<FileCode size={16} /> <FileCode size={16} />
{operations[i].filter && <Filter size={16}/>}
{operations[i].sort && <SortAsc size={16}/>}
{c.name}.ts {c.name}.ts
</span> </span>
<span> <span>
@ -105,6 +113,7 @@ export default function ServerListModificationFrame() {
files.splice(i, 1); files.splice(i, 1);
await user?.update({ await user?.update({
unsafeMetadata: { unsafeMetadata: {
...user.unsafeMetadata,
customFiles: files, customFiles: files,
}, },
}); });

@ -34,68 +34,100 @@ import { Material } from "@/components/ui/material";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Link } from "@/components/util/link"; import { Link } from "@/components/util/link";
import { serverModDB } from "@/config/sl-mod-db"; import { serverModDB } from "@/config/sl-mod-db";
import { ArrowRight } from "lucide-react"; import { ArrowRight, Binary } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useRouter } from "@/lib/useRouter"; import { useRouter } from "@/lib/useRouter";
import { SignedIn, useUser } from "@clerk/nextjs";
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
export default function ServerListModificationFrame() { export default function ServerListModificationFrame() {
const router = useRouter(); const router = useRouter();
const { user } = useUser();
return ( return (
<main className="max-w-[800px] p-4"> <main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full">Filters & Sorting</h1> <h1 className="text-xl font-bold w-full">Filters & Sorting</h1>
<div className="flex items-center gap-2 my-2"> <div className="flex items-center gap-2 my-2">
<Button size="sm">Active modifications</Button> <Button size="sm">Active modifications</Button>
<Link href="/servers/embedded/sl-modification-frame/files"> <Link href="/servers/embedded/sl-modification-frame/files">
<Button size="sm">Custom files</Button> <Button size="sm">Custom files</Button>
</Link> </Link>
<Button size="sm">Settings</Button> <Button size="sm">Settings</Button>
</div> </div>
<span className="text-wrap pt-2"> <span className="text-wrap pt-2">
Pick out different filters & sorting systems to customize your server Pick out different filters & sorting systems to customize your server
viewing experience. We frequently add new filters in accordance to new viewing experience. We frequently add new filters in accordance to new
features, as well. features, as well.
</span> </span>
<Material className="mt-10 p-4"> <Material className="mt-10 p-4">
{serverModDB.map((c) => ( {serverModDB.map((c) => (
<span key={c.displayTitle}> <div key={c.displayTitle} className="my-4">
<h2 className="text-lg font-bold pb-3 flex justify-between"> <h2 className="text-lg font-bold pb-3 flex justify-between">
{c.displayTitle} {c.displayTitle}
<Link <Link
href={`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}`} href={`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}`}
className="flex gap-2 text-sm font-normal items-center" className="flex gap-2 text-sm font-normal items-center"
> >
<ArrowRight size={16} /> <ArrowRight size={16} />
View more View more
</Link> </Link>
</h2> </h2>
<div className="grid grid-cols-6 gap-2"> <div className="grid grid-cols-6 gap-2">
{c.entries.map((m) => ( {c.entries.map((m) => (
<Material <Material
elevation="high" elevation="high"
className="p-2 hover:drop-shadow-card-hover cursor-pointer" className="p-2 hover:drop-shadow-card-hover cursor-pointer"
key={m.name} key={m.name}
onClick={() => onClick={() =>
router.push( router.push(
`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}/modification/${btoa(m.name)}` `/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}/modification/${btoa(m.name)}`,
) )
} }
> >
<div <div
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center" className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
style={{ backgroundColor: m.color }} style={{ backgroundColor: m.color }}
> >
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" /> <m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
</div> </div>
<span className="text-sm text-center w-full flex items-center justify-center"> <span className="text-sm text-center w-full flex items-center justify-center">
{m.name} {m.name}
</span> </span>
</Material> </Material>
))} ))}
</div> <SignedIn>
</span> {c.__custom &&
))} (
</Material> (user?.unsafeMetadata
</main> .activatedModifications as ClerkCustomActivatedModification[]) ??
); []
).map((m) => (
<Material
elevation="high"
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
key={m.friendlyName}
onClick={() =>
router.push(
`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}/modification/custom/${btoa(m.friendlyName)}`,
)
}
>
<div
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
style={{ backgroundColor: m.color }}
>
<Binary className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
</div>
<span className="text-sm text-center w-full flex items-center justify-center">
{m.friendlyName}
</span>
</Material>
))}
</SignedIn>
</div>
</div>
))}
</Material>
</main>
);
} }

@ -0,0 +1,49 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2025 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { createContext } from "@/server/context";
import { appRouter } from "@/server/router/_app";
import { getAuth } from "@clerk/nextjs/server";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
// I still have no clue why this works.
import type { NextRequest } from "../../../../../../../node_modules/@clerk/nextjs/node_modules/next/dist/server/web/spec-extension/request";
const handler = (request: NextRequest) => {
const {userId} = getAuth(request);
return fetchRequestHandler({
endpoint: "/api/trpc",
req: request,
router: appRouter,
createContext: createContext(userId),
});
};
export { handler as GET, handler as POST };

@ -236,7 +236,7 @@
.loading-shimmer { .loading-shimmer {
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
animation-duration: 2.5s; animation-duration: 2s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-name: loading-shimmer; animation-name: loading-shimmer;
background: var(--color-muted-foreground) background: var(--color-muted-foreground)

@ -1,6 +1,7 @@
import { BrandingGenericIcon } from "../icons/branding-icons"; import { BrandingGenericIcon } from "../icons/branding-icons";
import { Link } from "../../util/link"; import { Link } from "../../util/link";
import { FooterStatus } from "./status"; import { FooterStatus } from "./status";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
export function Footer() { export function Footer() {
return ( return (
@ -35,6 +36,29 @@ export function Footer() {
Contact Contact
</Link> </Link>
</li> </li>
<li className="text-sm">
<DropdownMenu
>
<DropdownMenuTrigger>
<a className="text-muted-foreground hover:text-shadcn-primary transition-colors cursor-pointer">Discord</a>
</DropdownMenuTrigger>
<DropdownMenuContent>
<Link href="https://t.mhsf.app/d/m" noExtraIcons>
<DropdownMenuItem className="block py-2">
Minehut Discord
<small className="flex">Not officially owned by MHSF, however conversations about MHSF and related take place there.</small>
</DropdownMenuItem>
</Link>
<Link href="https://t.mhsf.app/d/u" noExtraIcons>
<DropdownMenuItem className="block py-2">
MHSF Discord
<small className="flex">A read-only server for updates related to MHSF.</small>
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
</li>
</ul> </ul>
</span> </span>
<FooterStatus /> <FooterStatus />

@ -63,6 +63,15 @@ export type ClerkCustomModification = {
testId?: string; testId?: string;
}; };
export type ClerkCustomActivatedModification = {
originalFileName: string;
friendlyName: string;
transpiledContents: string;
active: boolean;
testMode: "filter" | "sort";
color: string;
}
export function ModificationFileCreationDialog({ export function ModificationFileCreationDialog({
children, children,
type, type,
@ -98,10 +107,11 @@ export function ModificationFileCreationDialog({
<DialogTrigger> <DialogTrigger>
<Button <Button
className="w-full" className="w-full"
onClick={(e) => { onClick={async (e) => {
if (!isSignedIn) return toast.error("Please login."); if (!isSignedIn) return toast.error("Please login.");
user?.update({ await user?.update({
unsafeMetadata: { unsafeMetadata: {
...user.unsafeMetadata,
customFiles: [ customFiles: [
...((user.unsafeMetadata ...((user.unsafeMetadata
.customFiles as Array<ClerkCustomModification>) ?? []), .customFiles as Array<ClerkCustomModification>) ?? []),

@ -1,5 +1,10 @@
import { AnimatedText } from "@/components/ui/animated-text"; import { AnimatedText } from "@/components/ui/animated-text";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { useQueryState } from "nuqs"; import { useQueryState } from "nuqs";
export function ServerTestModeSelector({ export function ServerTestModeSelector({
@ -18,16 +23,27 @@ export function ServerTestModeSelector({
<div className="pl-5 flex items-center gap-1"> <div className="pl-5 flex items-center gap-1">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<span className="relative flex size-2.5 pt-[1px] items-center cursor-pointer"> <span className="relative flex size-2.5 pt-[1px] items-center cursor-pointer">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-orange-400 opacity-75" /> <span
<span className="relative inline-flex size-2.5 rounded-full bg-orange-500" /> className={cn(
</span></TooltipTrigger> "absolute inline-flex h-full w-full animate-ping rounded-full opacity-75",
<TooltipContent>Test mode was enabled.</TooltipContent> testModeLoading ? "bg-orange-500" : "bg-green-400"
)}
/>
<span
className={cn(
"relative inline-flex size-2.5 rounded-full",
testModeLoading ? "bg-orange-500" : "bg-green-500"
)}
/>
</span>
</TooltipTrigger>
<TooltipContent>Test mode was enabled.</TooltipContent>
</Tooltip> </Tooltip>
<AnimatedText <AnimatedText
className="text-muted-foreground top-[2.5px] left-[6px] min-w-[70vw]" className="text-muted-foreground top-[2.5px] left-[6px] min-w-[70vw]"
text={testModeStatus} text={testModeStatus}
glimmer glimmer={testModeLoading}
/> />
</div> </div>
); );

@ -38,14 +38,12 @@ import { MultisessionAppSupport } from "@clerk/nextjs/internal";
export const ClerkProvider = ({ children }: { children: React.ReactNode }) => { export const ClerkProvider = ({ children }: { children: React.ReactNode }) => {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
if (resolvedTheme === "dark") { if (resolvedTheme === "dark") {
console.log(resolvedTheme);
return ( return (
<ImportedClerkProvider appearance={{ baseTheme: dark }}> <ImportedClerkProvider appearance={{ baseTheme: dark }}>
<MultisessionAppSupport>{children}</MultisessionAppSupport> <MultisessionAppSupport>{children}</MultisessionAppSupport>
</ImportedClerkProvider> </ImportedClerkProvider>
); );
} }
console.log("a");
return ( return (
<ImportedClerkProvider> <ImportedClerkProvider>

@ -51,8 +51,7 @@ type ModDBCategory = {
export const serverModDB: ModDBCategory[] = [ export const serverModDB: ModDBCategory[] = [
{ {
displayTitle: "Custom Files", displayTitle: "Create Custom Files",
__custom: true,
description: description:
`Create custom TypeScript-based filter or sorting systems, completely from the comfort of your own browser. `Create custom TypeScript-based filter or sorting systems, completely from the comfort of your own browser.
Types used are *builtin* and you can see live type definitions and IntelliSense in the editor.`, Types used are *builtin* and you can see live type definitions and IntelliSense in the editor.`,
@ -73,4 +72,11 @@ export const serverModDB: ModDBCategory[] = [
}, },
], ],
}, },
{
displayTitle: "Custom Files",
description: "These are all of your activated modifications made in the editor.",
__custom: true,
// Entries are already pre-loaded.
entries: []
}
]; ];

@ -0,0 +1,92 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2025 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { clerkClient as ClerkClient, getAuth } from "@clerk/nextjs/server";
import { Db, MongoClient } from "mongodb";
import { NextApiRequest } from "next";
import { NextRequest, NextResponse } from "next/server";
import requestIp from 'request-ip'
type BackendProcedureValue = {
status: "BANNED" | "OK" | "BLOCKED",
allowed: boolean,
mongoClient?: MongoClient,
defaultDatabase?: Db
}
export async function getBackendProcedure(request: NextApiRequest): Promise<BackendProcedureValue> {
const mongoClient = new MongoClient(process.env.MONGO_DB as string);
const {userId} = getAuth(request)
await mongoClient.connect();
const defaultDatabase = mongoClient.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const clerkClient = await ClerkClient();
if (userId !== null) {
// User exists
const user = await clerkClient.users.getUser(userId);
const userBannedMetadata = user.publicMetadata.banned;
if (userBannedMetadata !== undefined) {
// User is banned
await mongoClient.close()
return {
status: "BANNED",
allowed: false
}
}
}
const detectedIp = requestIp.getClientIp(request);
if (detectedIp !== null) {
const collection = defaultDatabase.collection("blocked-ips");
console.log(await collection.findOne({ ip: detectedIp }), detectedIp)
if (await collection.findOne({ ip: detectedIp }) !== null) {
await mongoClient.close()
return { status: "BLOCKED", allowed: false }
}
}
await mongoClient.close()
return {
status: "OK",
allowed: true,
}
}
function convert(request: Headers) {
const headersObject: Record<string, string> = {};
for (const [key, value] of request) {
headersObject[key] = value;
}
return headersObject;
}

@ -36,111 +36,135 @@ import { tryCatch } from "../try-catch";
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page"; import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
export function useFilters(data: OnlineServer[]) { export function useFilters(data: OnlineServer[]) {
const [filteredData, setFilteredData] = useState<OnlineServer[]>(data); const [filteredData, setFilteredData] = useState<OnlineServer[]>(data);
const [t] = useQueryState("tm"); const [t] = useQueryState("tm");
const [testModeEnabled, setTestModeEnabled] = useState(false); const [testModeEnabled, setTestModeEnabled] = useState(false);
const [testModeStatus, setTestModeStatus] = useState("Haven't connected thread yet (if stuck, select the other tab, and come back)"); const [testModeStatus, setTestModeStatus] = useState(
const [testModeLoading, setTestModeLoading] = useState(true); "Haven't connected thread yet (if stuck, select the other tab, and come back)",
);
const [testModeLoading, setTestModeLoading] = useState(true);
useEffect(() => { useEffect(() => {
if (filteredData.length === 0) setFilteredData(data); if (filteredData.length === 0) setFilteredData(data);
}, [data, filteredData.length]); }, [data, filteredData.length]);
useEffect(() => { const testModeInit = (type: "filter" | "sort") => {
if (data.length !== 0) window.dispatchEvent(new Event("test-mode.enabled"));
window.addEventListener("test-mode.enable", (c) => { if (!t) {
window.dispatchEvent(new Event("test-mode.enabled")); toast.error("Couldn't enable test mode; no query variable.");
if (!t) { } else {
toast.error("Couldn't enable test mode; no query variable."); setTestModeEnabled(true);
} else { const code = atob(t);
setTestModeEnabled(true); (async () => {
const code = atob(t); setTestModeStatus("Transpiling TypeScript...");
(async () => { const startTime = Date.now();
setTestModeStatus("Transpiling TypeScript..."); const { error, data: transpiledCode } = await tryCatch(
const startTime = Date.now(); (async () => transpileTypeScript(code))(),
const { error, data: transpiledCode } = await tryCatch( );
(async () => transpileTypeScript(code))() if (error) {
); setTestModeStatus(
if (error) { "Failed to transpile TypeScript! Error: " + error.message,
setTestModeStatus( );
"Failed to transpile TypeScript! Error: " + error.message setTestModeLoading(false);
); return;
setTestModeLoading(false); }
return; if (transpiledCode === null) {
} setTestModeStatus("Cannot continue.");
if (transpiledCode === null) { setTestModeLoading(false);
setTestModeStatus("Cannot continue."); return;
setTestModeLoading(false); }
return; setTestModeStatus("Generating function...");
} if (
console.log( !transpiledCode.includes("export default") &&
"[MHSF Filters] Transpiled TypeScript:", !transpiledCode.includes("export")
transpiledCode ?? "" ) {
); setTestModeStatus(
setTestModeStatus("Generating function..."); "Transpiled code does not contain any export statements.",
if ( );
!transpiledCode.includes("export default") && setTestModeLoading(false);
!transpiledCode.includes("export") return;
) { }
setTestModeStatus( const functionBody = transpiledCode
"Transpiled code does not contain any export statements." .replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
); .replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
setTestModeLoading(false); const { error: filterErr, data: filterFunc } = await tryCatch(
return; (async () =>
} type === "filter"
const functionBody = transpiledCode ? new Function(
.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon "server",
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon `${functionBody}
const { error: filterErr, data: filterFunc } = await tryCatch(
(async () =>
new Function(
"server",
`${functionBody}
return filter(server)` return filter(server)`,
))() )
); : new Function(
if (filterErr) { "serverA",
setTestModeStatus( "serverB",
`Failed to generate function! Error: ${filterErr.message}` `${functionBody}
);
setTestModeLoading(false);
return;
}
if (typeof filterFunc === "function") {
setTestModeStatus(
"Compiled in " + (Date.now() - startTime) + "ms"
);
toast.promise(
async () => {
let newServers = [];
newServers = data.filter((c) => filterFunc(c));
setTestModeStatus(
"Server count " + data.length + " -> " + newServers.length
);
setFilteredData(() => [...newServers]);
setTestModeLoading(false);
},
{
loading: "Manipulating data...",
success: "Manipulated data; test mode finished!",
error: (e) =>
`Error while manipulating data; go back to your editor and run again. ${e}`,
}
);
} else {
setTestModeStatus(
"Code doesn't have a 'filter' function. Cannot be tested."
);
setTestModeLoading(false);
}
})();
}
});
}, [t, data]);
console.log(filteredData, testModeStatus); return sort(serverA, serverB)`,
))(),
);
if (filterErr) {
setTestModeStatus(
`Failed to generate function! Error: ${filterErr.message}`,
);
setTestModeLoading(false);
return;
}
if (typeof filterFunc === "function") {
setTestModeStatus("Compiled in " + (Date.now() - startTime) + "ms");
toast.promise(
async () => {
let newServers = [];
if (type === "filter") {
newServers = data.filter((c) => filterFunc(c));
setTestModeStatus(
"Server count " + data.length + " -> " + newServers.length,
);
if (newServers.length === 0)
setTestModeStatus(
"No servers were specified in the criteria; showing all servers instead",
);
setFilteredData(() => [...newServers]);
}
if (type === "sort") {
newServers = data.sort((a, b) => filterFunc(a, b));
setTestModeStatus("Sorted " + newServers.length + " servers.");
console.log(newServers, data.sort((a, b) => filterFunc(a, b)))
console.log(filterFunc)
setFilteredData(() => [...newServers]);
}
return { filteredData, testModeEnabled, testModeLoading, testModeStatus }; setTestModeLoading(false);
window.dispatchEvent(new Event("test-mode.success"));
},
{
loading: "Manipulating data...",
success: "Manipulated data; test mode finished!",
error: (e) =>
`Error while manipulating data; go back to your editor and run again. ${e}`,
},
);
} else {
setTestModeStatus(
"Code doesn't have a 'filter' function. Cannot be tested.",
);
setTestModeLoading(false);
}
})();
}
};
useEffect(() => {
if (data.length !== 0) {
window.addEventListener("test-mode.enable.filter", () =>
testModeInit("filter"),
);
window.addEventListener("test-mode.enable.sort", () =>
testModeInit("sort"),
);
}
}, [t, data]);
return { filteredData, testModeEnabled, testModeLoading, testModeStatus };
} }

@ -53,7 +53,6 @@ export function useIframeCommunication(bottomIframe?: RefObject<HTMLIFrameElemen
}, },
handle: (key: string, callback: (object: any) => void) => { handle: (key: string, callback: (object: any) => void) => {
window.addEventListener('message', (e) => { window.addEventListener('message', (e) => {
console.log(e);
if (e.data.__key === key) { if (e.data.__key === key) {
callback(e.data) callback(e.data)
} }

@ -35,6 +35,7 @@ import {
} from "@clerk/nextjs/server"; } from "@clerk/nextjs/server";
import { type NextRequest, NextResponse } from "next/server"; import { type NextRequest, NextResponse } from "next/server";
import type { ServerResponse } from "./lib/types/mh-server"; import type { ServerResponse } from "./lib/types/mh-server";
import { getBackendProcedure } from "./lib/backend-procedure";
// Thanks for the router matcher API Clerk <3 // Thanks for the router matcher API Clerk <3
const isRootRoute = createRouteMatcher(["/"]); const isRootRoute = createRouteMatcher(["/"]);
@ -57,22 +58,6 @@ export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
return NextResponse.redirect(new URL("/home", req.url)); return NextResponse.redirect(new URL("/home", req.url));
} }
} }
// If user is banned, disable all API routes
if (authRes.userId !== null) {
// User exists
const user = await client.users.getUser(authRes.userId);
const userBannedMetadata = user.publicMetadata.banned;
if (userBannedMetadata !== undefined) {
// User is banned
if (apiRoute(req)) {
return NextResponse.json({
banned:
"You were banned. (and I'm not telling you why) Why are you trying to use the API. Huh? Tell me. Now. You're not funny.",
});
}
}
}
if (isOldServerRoute(req)) { if (isOldServerRoute(req)) {
const minehut = await fetch( const minehut = await fetch(
`https://api.minehut.com/server/${req.url.split("/server/")[1].split("/")[0]}?byName=true`, `https://api.minehut.com/server/${req.url.split("/server/")[1].split("/")[0]}?byName=true`,

@ -28,328 +28,337 @@
* OTHER DEALINGS IN THE SOFTWARE. * OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { getBackendProcedure } from "@/lib/backend-procedure";
import type { MHSFData } from "@/lib/types/data"; import type { MHSFData } from "@/lib/types/data";
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
export type RouteParams = { export type RouteParams = {
actions: { actions: {
favorite: string; favorite: string;
customize: string; customize: string;
own: string; own: string;
report: string; report: string;
history: { history: {
dailyData: string; dailyData: string;
monthlyData: string; monthlyData: string;
relativeData: string; relativeData: string;
historicalData: string; historicalData: string;
}; };
}; };
}; };
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<{ server: (MHSFData & RouteParams) | null }> res: NextApiResponse<
{ server: (MHSFData & RouteParams) | null } | { error: string }
>,
) { ) {
const { const backendProcedure = await getBackendProcedure(req);
server,
maxFavoriteEntries,
favoriteTimespanStart,
favoriteTimespanEnd,
maxPlayerEntries,
playerTimespanStart,
playerTimespanEnd,
maxAchievementEntries,
achievementTimespanStart,
achievementTimespanEnd,
} = req.query;
if (!server) return res.status(400).send({ server: null });
const serverData = await findServerData(server as string); if (backendProcedure.status !== "OK")
if (!serverData.exists) return res.status(404).send({ server: null }); return res.status(403).json({
error: `Backend procedure marked request as '${backendProcedure.status}' instead of required 'OK'`,
});
const {
server,
maxFavoriteEntries,
favoriteTimespanStart,
favoriteTimespanEnd,
maxPlayerEntries,
playerTimespanStart,
playerTimespanEnd,
maxAchievementEntries,
achievementTimespanStart,
achievementTimespanEnd,
} = req.query;
if (!server) return res.status(400).send({ server: null });
const mongo = new MongoClient(process.env.MONGO_DB as string); const serverData = await findServerData(server as string);
if (!serverData.exists) return res.status(404).send({ server: null });
try { const mongo = new MongoClient(process.env.MONGO_DB as string);
await mongo.connect();
const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const userId = req.cookies.userId;
// Run queries in parallel try {
const [favoriteData, customizationData, playerData, achievements] = await mongo.connect();
await Promise.all([ const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
findFavoriteData(serverData.name, userId, db, { const userId = req.cookies.userId;
maxFavoriteEntries,
favoriteTimespanStart,
favoriteTimespanEnd,
}),
findCustomizationData(serverData.name, userId, db),
findPlayerData(serverData.name, db, {
maxPlayerEntries,
playerTimespanStart,
playerTimespanEnd,
}),
findAchievements(serverData.name, db, {
maxAchievementEntries,
achievementTimespanStart,
achievementTimespanEnd,
}),
]);
res.send({ // Run queries in parallel
server: { const [favoriteData, customizationData, playerData, achievements] =
favoriteData, await Promise.all([
customizationData, findFavoriteData(serverData.name, userId, db, {
playerData, maxFavoriteEntries,
achievements, favoriteTimespanStart,
actions: { favoriteTimespanEnd,
history: { }),
dailyData: `/api/v1/server/get/${server}/history/daily-data`, findCustomizationData(serverData.name, userId, db),
monthlyData: `/api/v1/server/get/${server}/history/monthly-data`, findPlayerData(serverData.name, db, {
relativeData: `/api/v1/server/get/${server}/history/relative-data`, maxPlayerEntries,
historicalData: `/api/v1/server/get/${server}/history/historical-data`, playerTimespanStart,
}, playerTimespanEnd,
favorite: `/api/v1/server/get/${server}/favorite-server`, }),
customize: `/api/v1/server/get/${server}/customize`, findAchievements(serverData.name, db, {
own: `/api/v1/server/get/${server}/own-server`, maxAchievementEntries,
report: `/api/v1/server/get/${server}/report-server`, achievementTimespanStart,
}, achievementTimespanEnd,
}, }),
}); ]);
} catch (error) {
console.error("Error processing request:", error); res.send({
res.status(500).send({ server: null }); server: {
} finally { favoriteData,
await mongo.close(); customizationData,
} playerData,
achievements,
actions: {
history: {
dailyData: `/api/v1/server/get/${server}/history/daily-data`,
monthlyData: `/api/v1/server/get/${server}/history/monthly-data`,
relativeData: `/api/v1/server/get/${server}/history/relative-data`,
historicalData: `/api/v1/server/get/${server}/history/historical-data`,
},
favorite: `/api/v1/server/get/${server}/favorite-server`,
customize: `/api/v1/server/get/${server}/customize`,
own: `/api/v1/server/get/${server}/own-server`,
report: `/api/v1/server/get/${server}/report-server`,
},
},
});
} catch (error) {
console.error("Error processing request:", error);
res.status(500).send({ server: null });
} finally {
await mongo.close();
}
} }
async function findCustomizationData( async function findCustomizationData(
serverName: string, serverName: string,
userId: string | undefined, userId: string | undefined,
db: any db: any,
): Promise<{ ): Promise<{
description: string | undefined; description: string | undefined;
banner: string | undefined; banner: string | undefined;
discord: string | undefined; discord: string | undefined;
colorScheme: string | undefined; colorScheme: string | undefined;
userProfilePicture: string | undefined; userProfilePicture: string | undefined;
isOwned: boolean; isOwned: boolean;
isOwnedByUser: boolean; isOwnedByUser: boolean;
}> { }> {
// Run queries in parallel // Run queries in parallel
const [customizationData, ownedServerData] = await Promise.all([ const [customizationData, ownedServerData] = await Promise.all([
db.collection("customization").findOne({ server: serverName }), db.collection("customization").findOne({ server: serverName }),
userId userId
? db.collection("owned-servers").findOne({ server: serverName }) ? db.collection("owned-servers").findOne({ server: serverName })
: null, : null,
]); ]);
if (customizationData) { if (customizationData) {
return { return {
...(customizationData as any), ...(customizationData as any),
isOwned: true, isOwned: true,
isOwnedByUser: ownedServerData?.author === userId, isOwnedByUser: ownedServerData?.author === userId,
}; };
} }
return { return {
isOwned: false, isOwned: false,
isOwnedByUser: false, isOwnedByUser: false,
description: undefined, description: undefined,
banner: undefined, banner: undefined,
discord: undefined, discord: undefined,
colorScheme: undefined, colorScheme: undefined,
userProfilePicture: undefined, userProfilePicture: undefined,
}; };
} }
async function findFavoriteData( async function findFavoriteData(
serverName: string, serverName: string,
userId: string | undefined, userId: string | undefined,
db: any, db: any,
query: { query: {
maxFavoriteEntries?: string | string[]; maxFavoriteEntries?: string | string[];
favoriteTimespanStart?: string | string[]; favoriteTimespanStart?: string | string[];
favoriteTimespanEnd?: string | string[]; favoriteTimespanEnd?: string | string[];
} },
) { ) {
// Run queries in parallel // Run queries in parallel
const [userFavorites, metaData, historyData] = await Promise.all([ const [userFavorites, metaData, historyData] = await Promise.all([
userId ? db.collection("favorites").findOne({ user: userId }) : null, userId ? db.collection("favorites").findOne({ user: userId }) : null,
db.collection("meta").findOne({ server: serverName }), db.collection("meta").findOne({ server: serverName }),
fetchHistoryData(db, serverName, query), fetchHistoryData(db, serverName, query),
]); ]);
// Process user favorites // Process user favorites
const favoritedByAccount = const favoritedByAccount =
userId && userFavorites userId && userFavorites
? userFavorites.favorites.includes(serverName) ? userFavorites.favorites.includes(serverName)
: null; : null;
// Process favorite count // Process favorite count
const favoriteNumber = metaData?.favorites || 0; const favoriteNumber = metaData?.favorites || 0;
return { return {
favoritedByAccount, favoritedByAccount,
favoriteNumber, favoriteNumber,
favoriteHistoricalData: historyData, favoriteHistoricalData: historyData,
}; };
} }
async function fetchHistoryData( async function fetchHistoryData(
db: any, db: any,
serverName: string, serverName: string,
query: { query: {
maxFavoriteEntries?: string | string[]; maxFavoriteEntries?: string | string[];
favoriteTimespanStart?: string | string[]; favoriteTimespanStart?: string | string[];
favoriteTimespanEnd?: string | string[]; favoriteTimespanEnd?: string | string[];
} },
) { ) {
// Build query filter // Build query filter
const filter: any = { server: serverName }; const filter: any = { server: serverName };
// Add date range filter if provided // Add date range filter if provided
if (query.favoriteTimespanStart && query.favoriteTimespanEnd) { if (query.favoriteTimespanStart && query.favoriteTimespanEnd) {
filter.date = { filter.date = {
$gte: new Date(Number(query.favoriteTimespanStart)), $gte: new Date(Number(query.favoriteTimespanStart)),
$lte: new Date(Number(query.favoriteTimespanEnd)), $lte: new Date(Number(query.favoriteTimespanEnd)),
}; };
} }
// Determine limit // Determine limit
const limit = query.maxFavoriteEntries ? Number(query.maxFavoriteEntries) : 0; const limit = query.maxFavoriteEntries ? Number(query.maxFavoriteEntries) : 0;
// Use projection to only fetch needed fields // Use projection to only fetch needed fields
const projection = { favorites: 1, date: 1, _id: 0 }; const projection = { favorites: 1, date: 1, _id: 0 };
// Execute optimized query // Execute optimized query
const cursor = db.collection("history").find(filter).project(projection); const cursor = db.collection("history").find(filter).project(projection);
// Apply limit if specified // Apply limit if specified
if (limit > 0) { if (limit > 0) {
cursor.limit(limit); cursor.limit(limit);
} }
return await cursor.toArray(); return await cursor.toArray();
} }
async function findServerData( async function findServerData(
server: string server: string,
): Promise<{ exists: boolean; name: string }> { ): Promise<{ exists: boolean; name: string }> {
try { try {
const response = await fetch("https://api.minehut.com/server/" + server); const response = await fetch("https://api.minehut.com/server/" + server);
// Check if the response is ok before parsing JSON // Check if the response is ok before parsing JSON
if (!response.ok) { if (!response.ok) {
return { exists: false, name: "" }; return { exists: false, name: "" };
} }
const serverJSON = await response.json(); const serverJSON = await response.json();
if (!serverJSON.server) return { exists: false, name: "" }; if (!serverJSON.server) return { exists: false, name: "" };
return { exists: true, name: serverJSON.server.name }; return { exists: true, name: serverJSON.server.name };
} catch (error) { } catch (error) {
console.error("Error fetching server data:", error); console.error("Error fetching server data:", error);
return { exists: false, name: "" }; return { exists: false, name: "" };
} }
} }
async function findPlayerData( async function findPlayerData(
serverName: string, serverName: string,
db: any, db: any,
query: { query: {
maxPlayerEntries?: string | string[]; maxPlayerEntries?: string | string[];
playerTimespanStart?: string | string[]; playerTimespanStart?: string | string[];
playerTimespanEnd?: string | string[]; playerTimespanEnd?: string | string[];
} },
) { ) {
// Get historical player data // Get historical player data
const historyCollection = db.collection("history"); const historyCollection = db.collection("history");
// Build query filter // Build query filter
const filter: any = { server: serverName }; const filter: any = { server: serverName };
// Add date range filter if provided // Add date range filter if provided
if (query.playerTimespanStart && query.playerTimespanEnd) { if (query.playerTimespanStart && query.playerTimespanEnd) {
filter.date = { filter.date = {
$gte: new Date(Number(query.playerTimespanStart)), $gte: new Date(Number(query.playerTimespanStart)),
$lte: new Date(Number(query.playerTimespanEnd)), $lte: new Date(Number(query.playerTimespanEnd)),
}; };
} }
// Use projection to only fetch needed fields // Use projection to only fetch needed fields
const projection = { player_count: 1, date: 1, _id: 0 }; const projection = { player_count: 1, date: 1, _id: 0 };
// Get max player count in a single query // Get max player count in a single query
const [maxResult, playerHistory] = await Promise.all([ const [maxResult, playerHistory] = await Promise.all([
historyCollection historyCollection
.find({ server: serverName }) .find({ server: serverName })
.sort({ player_count: -1 }) .sort({ player_count: -1 })
.limit(1) .limit(1)
.project({ player_count: 1 }) .project({ player_count: 1 })
.toArray(), .toArray(),
historyCollection.find(filter).project(projection).toArray(), historyCollection.find(filter).project(projection).toArray(),
]); ]);
// Apply limit if specified // Apply limit if specified
let historically = playerHistory; let historically = playerHistory;
if (query.maxPlayerEntries) { if (query.maxPlayerEntries) {
historically = historically.slice(0, Number(query.maxPlayerEntries)); historically = historically.slice(0, Number(query.maxPlayerEntries));
} }
// Format the data to match the expected structure // Format the data to match the expected structure
const formattedHistory = historically.map( const formattedHistory = historically.map(
(item: { date: string; player_count?: number }) => ({ (item: { date: string; player_count?: number }) => ({
date: item.date, date: item.date,
playerCount: item.player_count || 0, playerCount: item.player_count || 0,
}) }),
); );
const max = maxResult.length > 0 ? maxResult[0].player_count : 0; const max = maxResult.length > 0 ? maxResult[0].player_count : 0;
return { historically: formattedHistory, max }; return { historically: formattedHistory, max };
} }
async function findAchievements( async function findAchievements(
serverName: string, serverName: string,
db: any, db: any,
query: { query: {
maxAchievementEntries?: string | string[]; maxAchievementEntries?: string | string[];
achievementTimespanStart?: string | string[]; achievementTimespanStart?: string | string[];
achievementTimespanEnd?: string | string[]; achievementTimespanEnd?: string | string[];
} },
) { ) {
// Get achievements data // Get achievements data
const achievementsCollection = db.collection("achievements"); const achievementsCollection = db.collection("achievements");
// Build query filter // Build query filter
const filter: any = { name: serverName }; const filter: any = { name: serverName };
// Add date range filter if provided // Add date range filter if provided
if (query.achievementTimespanStart && query.achievementTimespanEnd) { if (query.achievementTimespanStart && query.achievementTimespanEnd) {
// Assuming there's a timestamp or date field in the achievements collection // Assuming there's a timestamp or date field in the achievements collection
// If it's stored in _id, we might need a different approach // If it's stored in _id, we might need a different approach
filter.timestamp = { filter.timestamp = {
$gte: new Date(Number(query.achievementTimespanStart)), $gte: new Date(Number(query.achievementTimespanStart)),
$lte: new Date(Number(query.achievementTimespanEnd)), $lte: new Date(Number(query.achievementTimespanEnd)),
}; };
} }
// Get historical achievements // Get historical achievements
let historically = await achievementsCollection.find(filter).toArray(); let historically = await achievementsCollection.find(filter).toArray();
// Apply limit if specified // Apply limit if specified
if (query.maxAchievementEntries) { if (query.maxAchievementEntries) {
historically = historically.slice(0, Number(query.maxAchievementEntries)); historically = historically.slice(0, Number(query.maxAchievementEntries));
} }
const currently: any[] = []; const currently: any[] = [];
for (const a of historically) for (const a of historically)
a.achievements.forEach((item: any, interval: number) => a.achievements.forEach((item: any, interval: number) =>
currently.push({ interval, ...item }) currently.push({ interval, ...item }),
); );
return { historically, currently }; return { historically, currently };
} }

@ -33,11 +33,18 @@ import { clerkClient, getAuth } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
import { OnlineServer } from "@/lib/types/mh-server"; import { OnlineServer } from "@/lib/types/mh-server";
import { waitUntil } from "@vercel/functions"; import { waitUntil } from "@vercel/functions";
import { getBackendProcedure } from "@/lib/backend-procedure";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse
) { ) {
const backendProcedure = await getBackendProcedure(req);
if (backendProcedure.status !== "OK")
return res.status(403).json({
error: `Backend procedure marked request as '${backendProcedure.status}' instead of required 'OK'`,
});
const { userId } = getAuth(req); const { userId } = getAuth(req);
const { server } = req.query; const { server } = req.query;

@ -3223,11 +3223,43 @@
lodash.merge "^4.6.2" lodash.merge "^4.6.2"
postcss-selector-parser "6.0.10" postcss-selector-parser "6.0.10"
"@tanstack/query-core@5.69.0":
version "5.69.0"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.69.0.tgz#c434505987ade936dc53e6e27aa1406b0295516f"
integrity sha512-Kn410jq6vs1P8Nm+ZsRj9H+U3C0kjuEkYLxbiCyn3MDEiYor1j2DGVULqAz62SLZtUZ/e9Xt6xMXiJ3NJ65WyQ==
"@tanstack/react-query@^5.69.0":
version "5.69.0"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.69.0.tgz#8d58e800854cc11d0aa2c39569f53ae32ba442a9"
integrity sha512-Ift3IUNQqTcaFa1AiIQ7WCb/PPy8aexZdq9pZWLXhfLcLxH0+PZqJ2xFImxCpdDZrFRZhLJrh76geevS5xjRhA==
dependencies:
"@tanstack/query-core" "5.69.0"
"@tootallnate/quickjs-emscripten@^0.23.0": "@tootallnate/quickjs-emscripten@^0.23.0":
version "0.23.0" version "0.23.0"
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
"@trpc/client@^11.0.0":
version "11.0.0"
resolved "https://registry.yarnpkg.com/@trpc/client/-/client-11.0.0.tgz#3020392edf87abc046594cee0acd379f2c6289d9"
integrity sha512-U2THlxsdr4ykAX5lpTU8k5WRADPQ+68Ex2gfUht3MlCxGK7njBmNSSzjpQSWNt7tMI/xsYrddFiRlmEPrh+Cbg==
"@trpc/next@^11.0.0":
version "11.0.0"
resolved "https://registry.yarnpkg.com/@trpc/next/-/next-11.0.0.tgz#d83e1fac595629c3bc02ba5bbe2595508b7c55ee"
integrity sha512-HpowgsF0jfXG30jEBVK8v90ltbEZiQZq/x0rsjScfZuedkAfapqZvrsrkzv6Pkemz7sxaxJcZB3HEqXxWfkGoA==
"@trpc/react-query@^11.0.0":
version "11.0.0"
resolved "https://registry.yarnpkg.com/@trpc/react-query/-/react-query-11.0.0.tgz#6fb849baf715fb33d2eb6a417d7e75fc00307ae6"
integrity sha512-HeE9bBLA6nqC2xk5wlNZIPQ5vmyli3tgNNab8fTE489+ksNMKxaIx66pZKsMJIorDcP1wS0rWNV+GroU0iR98g==
"@trpc/server@^11.0.0":
version "11.0.0"
resolved "https://registry.yarnpkg.com/@trpc/server/-/server-11.0.0.tgz#5a49758fa3c052a83314c328155d68027164f077"
integrity sha512-xY9q/b/wR/tWGYTm5xmRjivkYD2EZZXmOKmHuNJRYZuLbieeNUsdfQRjJC409WB1pjKWInomhHwuA8bahZJ4lQ==
"@types/acorn@^4.0.0": "@types/acorn@^4.0.0":
version "4.0.6" version "4.0.6"
resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22"
@ -3491,7 +3523,7 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
"@types/react-dom@^19", "@types/react-dom@^19.0.3": "@types/react-dom@19.0.4", "@types/react-dom@^19", "@types/react-dom@^19.0.3":
version "19.0.4" version "19.0.4"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89"
integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg== integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==
@ -3503,13 +3535,20 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@^19", "@types/react@^19.0.8": "@types/react@*", "@types/react@19.0.10", "@types/react@^19", "@types/react@^19.0.8":
version "19.0.10" version "19.0.10"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb"
integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g== integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==
dependencies: dependencies:
csstype "^3.0.2" csstype "^3.0.2"
"@types/request-ip@^0.0.41":
version "0.0.41"
resolved "https://registry.yarnpkg.com/@types/request-ip/-/request-ip-0.0.41.tgz#c22a3244df2573402989346062851b06b7a5ac4e"
integrity sha512-Qzz0PM2nSZej4lsLzzNfADIORZhhxO7PED0fXpg4FjXiHuJ/lMyUg+YFF5q8x9HPZH3Gl6N+NOM8QZjItNgGKg==
dependencies:
"@types/node" "*"
"@types/resolve@^1.17.1": "@types/resolve@^1.17.1":
version "1.20.6" version "1.20.6"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8"
@ -4764,6 +4803,13 @@ cookie@~0.7.2:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
copy-anything@^3.0.2:
version "3.0.5"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0"
integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==
dependencies:
is-what "^4.1.8"
core-util-is@^1.0.3: core-util-is@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
@ -7637,6 +7683,11 @@ is-weakset@^2.0.3:
call-bound "^1.0.3" call-bound "^1.0.3"
get-intrinsic "^1.2.6" get-intrinsic "^1.2.6"
is-what@^4.1.8:
version "4.1.16"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f"
integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==
is-wsl@^2.2.0: is-wsl@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
@ -10877,6 +10928,11 @@ repeat-string@^1.6.1:
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
request-ip@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/request-ip/-/request-ip-3.3.0.tgz#863451e8fec03847d44f223e30a5d63e369fa611"
integrity sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==
require-directory@^2.1.1: require-directory@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@ -11677,6 +11733,13 @@ sucrase@^3.32.0, sucrase@^3.35.0:
pirates "^4.0.1" pirates "^4.0.1"
ts-interface-checker "^0.1.9" ts-interface-checker "^0.1.9"
superjson@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.2.tgz#9d52bf0bf6b5751a3c3472f1292e714782ba3173"
integrity sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==
dependencies:
copy-anything "^3.0.2"
supports-color@^7.1.0: supports-color@^7.1.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@ -12042,7 +12105,7 @@ turbo-windows-arm64@2.4.4:
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.4.4.tgz#e00c26e3d7fd9a82af90018ad3137f14e5221630" resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.4.4.tgz#e00c26e3d7fd9a82af90018ad3137f14e5221630"
integrity sha512-403sqp9t5sx6YGEC32IfZTVWkRAixOQomGYB8kEc6ZD+//LirSxzeCHCnM8EmSXw7l57U1G+Fb0kxgTcKPU/Lg== integrity sha512-403sqp9t5sx6YGEC32IfZTVWkRAixOQomGYB8kEc6ZD+//LirSxzeCHCnM8EmSXw7l57U1G+Fb0kxgTcKPU/Lg==
turbo@^2.4.0, turbo@^2.4.2: turbo@^2.4.0, turbo@^2.4.4:
version "2.4.4" version "2.4.4"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.4.4.tgz#cec5dbac5850adebdba71fbdf90e6e9a7723c3d6" resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.4.4.tgz#cec5dbac5850adebdba71fbdf90e6e9a7723c3d6"
integrity sha512-N9FDOVaY3yz0YCOhYIgOGYad7+m2ptvinXygw27WPLQvcZDl3+0Sa77KGVlLSiuPDChOUEnTKE9VJwLSi9BPGQ== integrity sha512-N9FDOVaY3yz0YCOhYIgOGYad7+m2ptvinXygw27WPLQvcZDl3+0Sa77KGVlLSiuPDChOUEnTKE9VJwLSi9BPGQ==
@ -12867,7 +12930,7 @@ zod@3.23.8:
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
zod@^3.20.6, zod@^3.21.4, zod@^3.23.8, zod@^3.24.1: zod@^3.20.6, zod@^3.21.4, zod@^3.24.1, zod@^3.24.2:
version "3.24.2" version "3.24.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3" resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ== integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==