Compare commits

..

3 Commits

Author SHA1 Message Date
d93693dc2d feat(readme): adding alert to the top 2025-04-04 21:12:49 -05:00
db992412d0 feat: clean up code 2025-04-04 21:10:56 -05:00
ed2d6f0ac1 feat: custom modifications 2025-04-04 21:10:50 -05:00
30 changed files with 2382 additions and 1289 deletions

@ -4,6 +4,9 @@
## The MHSF Project
> [!IMPORTANT]
> You are on the `v2` branch. Features may be broken as MHSF is getting rebuilt from the ground up.
A modern, third-party Minehut server list that is completely open-source, built using top of the line web technologies. Not built to take your money or time, find a server in minutes. Completely ad-free.
## Security

@ -38,6 +38,10 @@ const nextConfig = {
protocol: "https",
hostname: "img.clerk.com",
},
{
protocol: "https",
hostname: "avatars.githubusercontent.com"
}
],
},
async redirects() {

@ -1,5 +1,5 @@
{
"name": "mh-stats",
"name": "mhsf",
"version": "1.3.0",
"private": true,
"packageManager": "yarn@1.22.22",
@ -15,7 +15,7 @@
},
"dependencies": {
"@babel/parser": "^7.24.7",
"@biomejs/biome": "^1.8.3",
"@biomejs/biome": "^1.9.4",
"@clerk/elements": "^0.22.2",
"@clerk/nextjs": "^6.9.2",
"@emotion/is-prop-valid": "^1.3.0",
@ -35,9 +35,15 @@
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "1.1.0",
"@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/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@types/request-ip": "^0.0.41",
"@unocss/eslint-plugin": "^0.61.5",
"@unocss/postcss": "^0.61.5",
"@unocss/transformer-directives": "^0.61.5",
@ -80,8 +86,10 @@
"recharts": "^2.15.1",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.0",
"request-ip": "^3.3.0",
"sonner": "^1.7.0",
"stripe-gradient": "^1.0.1",
"superjson": "^2.2.2",
"swapy": "^1.0.5",
"tailwind-merge": "^2.3.0",
"tailwindcss": "^4.0.7",
@ -89,7 +97,8 @@
"tailwindcss-patch": "^4.0.0",
"turbo": "^2.4.0",
"unplugin-tailwindcss-mangle": "^3.0.1",
"vaul": "^1.1.2"
"vaul": "^1.1.2",
"zod": "^3.24.2"
},
"devDependencies": {
"@clerk/themes": "^2.1.19",

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

@ -0,0 +1,203 @@
/*
* 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 type { 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 {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Material } from "@/components/ui/material";
import { Placeholder } from "@/components/ui/placeholder";
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,
EllipsisVertical,
FileQuestion,
Filter,
SortAsc,
Trash,
} from "lucide-react";
import { useQueryState } from "nuqs";
import { use } from "react";
import Markdown from "react-markdown";
import { toast } from "sonner";
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 modIndex = (
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
).findIndex((c) => c.friendlyName === atob(decodeURIComponent(mod)));
if (modIndex === -1)
return (
<div className="w-full h-full flex justify-center items-center absolute top-[0%]">
<Link href={backRoute}>
<ArrowLeft className="absolute left-[10px] top-[10px]" />
</Link>
<Placeholder
title="We couldn't find the file you were looking for."
icon={<FileQuestion />}
/>
</div>
);
const modObj = ((user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ?? [])[
modIndex
];
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>
<div className="flex justify-between items-center">
<Button className="mt-2">
{modObj?.active ? "Disable" : "Enable"}
</Button>
<DropdownMenu>
<DropdownMenuTrigger>
<Button
variant="secondary"
className="flex items-center"
size="sm"
>
<EllipsisVertical size={16} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="gap-2"
onClick={async () => {
const time = Date.now();
const array =
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ??
[];
array.splice(modIndex, 1);
await user?.update({
unsafeMetadata: {
...user.unsafeMetadata,
activatedModifications: array,
},
});
toast.success(`Deleted in ${Date.now() - time}ms`);
}}
>
<Trash size={16} /> Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Separator className="mt-3" />
<Material className="mt-6 grid gap-3">
<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>
<Setting>
<SettingContent className="flex items-center">
<SettingMeta>
<SettingTitle>File name</SettingTitle>
</SettingMeta>
<Link
href={
`"/servers/embedded/sl-modification-frame/file/${modObj.originalFileName}`
}
className="text-blue-600"
>
<code className="flex items-center">
{modObj.originalFileName}.ts
</code>
</Link>
</SettingContent>
</Setting>
</Material>
</span>
</main>
);
}

@ -28,65 +28,70 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { ModificationAction } from "@/components/feat/server-list/modification/modification-action";
import { ModificationCustomModificationRow } from "@/components/feat/server-list/modification/modification-custom-modification-row";
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
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 { useRouter } from "@/lib/useRouter";
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 Markdown from "react-markdown";
export default function ServerListCategoryFrame({
params,
export default async function ServerListCategoryFrame({
params,
}: {
params: Promise<{ category: string }>;
params: Promise<{ category: string }>;
}) {
const { category } = use(params);
const categoryObj = serverModDB.find(
(c) => c.displayTitle === atob(category)
);
const router = useRouter();
const { category } = await params;
const categoryObj = serverModDB.find(
(c) => c.displayTitle === atob(decodeURIComponent(category)),
);
return (
<main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full flex items-center gap-2">
<Link href="/servers/embedded/sl-modification-frame">
<ArrowLeft size={20} />
</Link>
{categoryObj?.displayTitle}
</h1>
<Markdown className="text-wrap pt-2">{categoryObj?.description}</Markdown>
return (
<main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full flex items-center gap-2">
<Link href="/servers/embedded/sl-modification-frame">
<ArrowLeft size={20} />
</Link>
{categoryObj?.displayTitle}
</h1>
<Markdown className="text-wrap pt-2">{categoryObj?.description}</Markdown>
<Material className="mt-10 p-4 grid grid-cols-6 gap-2">
{categoryObj?.entries.map((m) => (
<Material
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
elevation="high"
onClick={() =>
router.push(
`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`
)
}
key={m.name}
>
<div
className={cn(
"w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
)}
style={{ backgroundColor: m.color }}
>
<m.icon 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.name}
</span>
</Material>
))}
</Material>
</main>
);
<Material className="mt-10 p-4 grid grid-cols-6 gap-2">
{categoryObj?.entries.map((m) => (
<Link
key={m.name}
href={`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`}
>
<Material
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
elevation="high"
>
<div
className={cn(
"w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center",
)}
style={{ backgroundColor: m.color }}
>
<m.icon 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.name}
</span>
</Material>
</Link>
))}
<SignedIn>
{categoryObj?.__custom && (
<ModificationCustomModificationRow category={category} />
)}
</SignedIn>
</Material>
</main>
);
}

@ -4,37 +4,29 @@ import { use, useEffect, useRef, useState } from "react";
import { useUser } from "@clerk/nextjs";
import type { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import { Link } from "@/components/util/link";
import { AlertOctagon, ArrowLeft, ExternalLink } from "lucide-react";
import { ArrowLeft, FileQuestion } from "lucide-react";
import Editor from "@monaco-editor/react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import * as ts from "typescript";
import useClipboard from "@/lib/useClipboard";
import { useTheme } from "@/lib/hooks/use-theme";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { languages, Uri } from "monaco-editor";
import {
Drawer,
DrawerContent,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Alert } from "@/components/ui/alert";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { compressToEncodedURIComponent } from "lz-string";
import { Geist_Mono } from "next/font/google";
import type { languages } from "monaco-editor";
import { cn } from "@/lib/utils";
import { debounce } from "lodash";
import { tryCatch } from "@/lib/try-catch";
import { Placeholder } from "@/components/ui/placeholder";
import { CustomErrors } from "@/components/feat/server-list/modification/custom-files/custom-errors";
import { CustomLint } from "@/components/feat/server-list/modification/custom-files/custom-lint";
import { CustomTest } from "@/components/feat/server-list/modification/custom-files/custom-test";
export type MonacoRefType = typeof import(
"monaco-editor/esm/vs/editor/editor.api"
);
const typeDefs = `// Hi :) how'd you get here?
// Here, in return I'll provide you with a random number: ${Math.ceil(Math.random() * 100)}
@ -77,378 +69,269 @@ export namespace Minehut {
`;
export const transpileTypeScript = (code: string) => {
try {
const result = ts.transpileModule(typeDefs + code, {
compilerOptions: {
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
jsx: ts.JsxEmit.ReactJSX,
esModuleInterop: true,
},
});
return result.outputText;
} catch (error) {
console.error("TypeScript transpilation error:", error);
toast.error(`TypeScript error: ${error}`);
return null;
}
try {
const result = ts.transpileModule(typeDefs + code, {
compilerOptions: {
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
jsx: ts.JsxEmit.ReactJSX,
esModuleInterop: true,
},
});
return result.outputText;
} catch (error) {
console.error("TypeScript transpilation error:", error);
toast.error(`TypeScript error: ${error}`);
return null;
}
};
const geistMono = Geist_Mono({ subsets: ["latin"] });
export default function CustomFilePage({
params,
params,
}: {
params: Promise<{ filename: string }>;
params: Promise<{ filename: string }>;
}) {
const { filename } = use(params);
const { user } = useUser();
const monacoRef =
useRef<typeof import("monaco-editor/esm/vs/editor/editor.api")>(null);
const { resolvedTheme } = useTheme();
const [successfullyLinted, setSuccessfullyLinted] = useState(false);
const [syntaxErrors, setSyntaxErrors] = useState<
languages.typescript.Diagnostic[] | null
>(null);
const file = (
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
).findIndex((c) => c.name === filename);
const { filename } = use(params);
const { user } = useUser();
const monacoRef =
useRef<typeof import("monaco-editor/esm/vs/editor/editor.api")>(null);
const { resolvedTheme } = useTheme();
const [successfullyLinted, setSuccessfullyLinted] = useState(false);
const [syntaxErrors, setSyntaxErrors] = useState<
languages.typescript.Diagnostic[] | null
>(null);
const file = (
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
).findIndex((c) => c.name === filename);
if (file === -1) {
return <>Bruh.</>;
}
if (file === -1) {
return (
<div className="w-full h-full flex justify-center items-center absolute top-[0%]">
<Link href="/servers/embedded/sl-modification-frame">
<ArrowLeft className="absolute left-[10px] top-[10px]" />
</Link>
<Placeholder
title="We couldn't find the file you were looking for."
icon={<FileQuestion />}
/>
</div>
);
}
const validateCode = (code: string) => {
if (!monacoRef.current) return;
const fileContents = ((user?.unsafeMetadata
.customFiles as Array<ClerkCustomModification>) ?? [])[file].contents;
const [value, setValue] = useState(fileContents);
const clipboard = useClipboard();
monacoRef.current.languages.typescript
.getTypeScriptWorker()
.then((worker) => {
worker(
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
).then((client) => {
client
.getSemanticDiagnostics(
(
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
).toString()
)
.then((diags) => {
setSyntaxErrors(diags);
});
});
});
};
const saveFile = async () => {
const metadata =
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ??
[];
const index = (
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
).findIndex((c) => c.name === filename);
const fileContents = ((user?.unsafeMetadata
.customFiles as Array<ClerkCustomModification>) ?? [])[file].contents;
const [value, setValue] = useState(fileContents);
const clipboard = useClipboard();
validateCode(value);
metadata[index].contents = value;
const saveFile = async () => {
const metadata =
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ??
[];
const index = (
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
).findIndex((c) => c.name === filename);
await user?.update({
unsafeMetadata: {
...user.unsafeMetadata,
customFiles: metadata,
},
});
};
metadata[index].contents = value;
const debouncedSave = debounce(async () => {
const { error } = await tryCatch(saveFile());
if (error)
toast.error(
"Whoa! We encountered an error while auto-saving. Please copy your code locally to ensure you'll keep your code changes.",
);
}, 300);
await user?.update({
unsafeMetadata: {
customFiles: metadata,
},
});
};
// biome-ignore lint: L
useEffect(() => {
setSuccessfullyLinted(false);
debouncedSave();
}, [value]);
const lintFile = async () => {
toast.info("Transpiling TypeScript...");
const { error, data: transpiledCode } = await tryCatch(
(async () => transpileTypeScript(value))()
);
if (error) {
toast.error("Failed to transpile TypeScript! Error: " + error.message);
return;
}
const startTime = Date.now();
if (transpiledCode === null) {
toast.error("Cannot continue.");
return;
}
console.log("[MHSF Filters] Transpiled TypeScript:", transpiledCode ?? "");
toast.info("Generating function...");
const functionBody = transpiledCode.match(
/function\s+filter\s*\([^)]*\)\s*\{([\s\S]*)\}/
)?.[1];
const { error: filterErr, data: filterFunc } = await tryCatch(
(async () => new Function("data", functionBody as string))()
);
if (filterErr) {
toast.error(`Failed to generate function! Error: ${filterErr.message}`);
return;
}
if (typeof filterFunc === "function") {
toast.success("Linted in " + (Date.now() - startTime) + "ms");
setSuccessfullyLinted(true);
} else {
toast.error("Code doesn't have a 'filter' function. Cannot be tested.");
toast.error(typeof filterFunc);
}
};
return (
<main className="max-w-[800px] p-4">
<div className="w-full justify-between flex items-center gap-2 my-2">
<strong className="flex items-center gap-1">
<Link href="/servers/embedded/sl-modification-frame/files">
<ArrowLeft size={20} />
</Link>
{filename}.ts
</strong>
<span className="flex items-center gap-2">
{syntaxErrors !== null && syntaxErrors.length !== 0 && (
<CustomErrors
filename={filename}
value={value}
monacoRef={monacoRef}
/>
)}
<Tooltip>
<TooltipTrigger>
<CustomLint
successfullyLinted={successfullyLinted}
setSuccessfullyLinted={setSuccessfullyLinted}
syntaxErrors={syntaxErrors}
value={value}
/>
</TooltipTrigger>
<TooltipContent>
{syntaxErrors !== null && syntaxErrors.length !== 0
? `You must have no type errors in the editor to lint, you have ${syntaxErrors.length} error(s).`
: "Check for possible runtime errors."}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<CustomTest
value={value}
successfullyLinted={successfullyLinted}
/>
</TooltipTrigger>
<TooltipContent>
{successfullyLinted
? "Open a full server-list instance with your filter activated in test mode."
: "You must lint before testing."}
</TooltipContent>
</Tooltip>
</span>
</div>
<div>
<Editor
className={cn("h-[calc(100vh-100px)]")}
defaultLanguage="typescript"
value={value}
theme={resolvedTheme === "dark" ? "vs-dark" : "vs"}
onChange={(newValue) => {
setValue(newValue || "");
}}
onMount={(editor, monaco) => {
monacoRef.current = monaco;
// Ensure TypeScript is properly configured
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
noEmit: true,
esModuleInterop: true,
jsx: monaco.languages.typescript.JsxEmit.React,
reactNamespace: "React",
allowJs: true,
typeRoots: ["node_modules/@types"],
});
const debouncedSave = debounce(async () => {
const { error } = await tryCatch(saveFile());
if (error)
toast.error(
"Whoa! We encountered an error while auto-saving. Please copy your code locally to ensure you'll keep your code changes."
);
}, 300);
// Create a virtual TS file for the types
const libUri = "file:///node_modules/@types/mhsf/index.d.ts";
useEffect(() => {
setSuccessfullyLinted(false);
validateCode(value);
debouncedSave();
}, [value]);
// Add typedefs as a library
monaco.languages.typescript.typescriptDefaults.addExtraLib(
typeDefs,
libUri,
);
return (
<main className="max-w-[800px] p-4">
<div className="w-full justify-between flex items-center gap-2 my-2">
<strong className="flex items-center gap-1">
<Link href="/servers/embedded/sl-modification-frame/files">
<ArrowLeft size={20} />
</Link>
{filename}.ts
</strong>
<span className="flex items-center gap-2">
{syntaxErrors !== null && syntaxErrors.length !== 0 && (
<Drawer direction="right">
<DrawerTrigger>
<Button
variant="danger-subtle"
size="square-md"
className="flex items-center justify-center"
>
<AlertOctagon />
</Button>
</DrawerTrigger>
<DrawerContent className="p-4 min-w-[400px] overflow-x-hidden max-h-screen overflow-y-auto">
<DrawerTitle>Type Errors</DrawerTitle>
<div className="p-2">
{syntaxErrors.map((c, i) => (
<Alert
variant={
c.category === 1
? "error"
: c.category === 0
? "warning"
: "info"
}
key={i}
className="gap-1 my-2"
>
{c.messageText.toString()}{" "}
<DropdownMenu>
<DropdownMenuTrigger>
<small className="flex items-center gap-1 cursor-pointer">
(TS{typeof c !== "string" && c.code})
<ExternalLink size={16} />
</small>
</DropdownMenuTrigger>
<DropdownMenuContent>
<Link
noExtraIcons
target="_blank"
href={`https://typescript.tv/errors/#ts${c.code}`}
>
<DropdownMenuItem>typescript.tv</DropdownMenuItem>
</Link>
<Link
noExtraIcons
target="_blank"
href={`https://ts-error-translator.vercel.app/?error=${compressToEncodedURIComponent(c.messageText.toString())}`}
>
<DropdownMenuItem>
ts-error-translator
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
</Alert>
))}
</div>
</DrawerContent>
</Drawer>
)}
<Tooltip>
<TooltipTrigger>
<Button
onClick={lintFile}
disabled={syntaxErrors === null || syntaxErrors.length !== 0}
variant={successfullyLinted ? "success-subtle" : "secondary"}
>
Lint
</Button>
</TooltipTrigger>
<TooltipContent>
{syntaxErrors !== null && syntaxErrors.length !== 0
? "You must have no type errors in the editor to lint, you have " +
syntaxErrors.length +
" error(s)."
: "Check for possible runtime errors."}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
disabled={!successfullyLinted}
onClick={() => {
const t = btoa(value);
// Create a model for the libUri file
if (!monaco.editor.getModel(monaco.Uri.parse(libUri))) {
monaco.editor.createModel(
typeDefs,
"typescript",
monaco.Uri.parse(libUri),
);
}
const newTab = window.open(`/servers?tm=${encodeURIComponent(t)}`)
const interval = setInterval(() => {
newTab?.dispatchEvent(new Event("test-mode.enable"))
}, 500)
toast.info("Waiting for server tab to pick up thread...")
// Make sure the current file is using the correct language
const currentModel = editor.getModel();
if (currentModel) {
monaco.editor.setModelLanguage(currentModel, "typescript");
}
newTab?.addEventListener("test-mode.enabled", () => {
clearInterval(interval);
toast.success("Connected to new tab; continue.")
})
}}
>
Test
</Button>
</TooltipTrigger>
<TooltipContent>
{successfullyLinted
? "Open a full server-list instance with your filter activated in test mode."
: "You must lint before testing."}
</TooltipContent>
</Tooltip>
</span>
</div>
<div>
<Editor
className={cn("h-[calc(100vh-100px)]")}
defaultLanguage="typescript"
value={value}
theme={resolvedTheme === "dark" ? "vs-dark" : "vs"}
onChange={(newValue) => {
setValue(newValue || "");
}}
onMount={(editor, monaco) => {
monacoRef.current = monaco;
// Ensure TypeScript is properly configured
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
noEmit: true,
esModuleInterop: true,
jsx: monaco.languages.typescript.JsxEmit.React,
reactNamespace: "React",
allowJs: true,
typeRoots: ["node_modules/@types"],
});
// Create a virtual TS file for the types
const libUri = "file:///node_modules/@types/mhsf/index.d.ts";
// Add typedefs as a library
monaco.languages.typescript.typescriptDefaults.addExtraLib(
typeDefs,
libUri
);
// Add actions
[
{
id: "manually-save-file",
label: "MHSF: Manually Save File",
run: () => {
saveFile();
toast.success("Manually saved file!");
},
},
{
id: "lint-file",
label: "MHSF: Lint File",
run: lintFile
},
].forEach((e) => editor.addAction(e));
// Create a model for the libUri file
if (!monaco.editor.getModel(monaco.Uri.parse(libUri))) {
monaco.editor.createModel(
typeDefs,
"typescript",
monaco.Uri.parse(libUri)
);
}
// Make sure the current file is using the correct language
const currentModel = editor.getModel();
if (currentModel) {
monaco.editor.setModelLanguage(currentModel, "typescript");
}
const currentUri = monaco.Uri.parse(`file:///${filename}.ts`);
if (!monaco.editor.getModel(currentUri)) {
monaco.editor.createModel(fileContents, "typescript", currentUri);
editor.setModel(monaco.editor.getModel(currentUri));
}
}}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
lineNumbers: "on",
roundedSelection: false,
scrollbar: {
vertical: "visible",
horizontal: "visible",
},
quickSuggestions: true,
suggestOnTriggerCharacters: true,
acceptSuggestionOnEnter: "on",
tabCompletion: "on",
wordBasedSuggestions: "currentDocument",
cursorSmoothCaretAnimation: "on",
parameterHints: {
enabled: true,
},
hover: {
enabled: true,
delay: 300,
sticky: true,
},
}}
/>
</div>
</main>
);
const currentUri = monaco.Uri.parse(`file:///${filename}.ts`);
if (!monaco.editor.getModel(currentUri)) {
monaco.editor.createModel(fileContents, "typescript", currentUri);
editor.setModel(monaco.editor.getModel(currentUri));
}
}}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
lineNumbers: "on",
roundedSelection: false,
scrollbar: {
vertical: "visible",
horizontal: "visible",
},
quickSuggestions: true,
suggestOnTriggerCharacters: true,
acceptSuggestionOnEnter: "on",
tabCompletion: "on",
wordBasedSuggestions: "currentDocument",
cursorSmoothCaretAnimation: "on",
parameterHints: {
enabled: true,
},
hover: {
enabled: true,
delay: 300,
sticky: true,
},
}}
/>
</div>
</main>
);
}
function guidGenerator() {
const S4 = () => {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return (
S4() +
S4() +
"-" +
S4() +
"-" +
S4() +
"-" +
S4() +
"-" +
S4() +
S4() +
S4()
);
export async function findSupportedOperations(
fileValue: string,
): Promise<{ filter: boolean; sort: boolean }> {
const returnValue = { filter: true, sort: true };
const transpiledValue = transpileTypeScript(fileValue);
const functionBody = transpiledValue
?.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
const { error: filterErr, data: filterFunc } = await tryCatch(
(async () =>
new Function(
"server",
`${functionBody}
return filter(server)`,
))(),
);
const { error: sortErr, data: sortFunc } = await tryCatch(
(async () =>
new Function(
"serverA",
"serverB",
`${functionBody}
return sort(serverA, serverB)`,
))(),
);
if (filterErr) returnValue.filter = false;
if (sortErr) returnValue.sort = false;
try {
filterFunc?.({});
} catch (e) {
if (String(e).startsWith("ReferenceError: filter is not defined")) {
returnValue.filter = false;
}
}
try {
sortFunc?.({}, {});
} catch (e) {
if (String(e).startsWith("ReferenceError: sort is not defined")) {
returnValue.sort = false;
}
}
return returnValue;
}

@ -33,97 +33,126 @@
import { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Material } from "@/components/ui/material";
import { Placeholder } from "@/components/ui/placeholder";
import { Link } from "@/components/util/link";
import { useUser } from "@clerk/nextjs";
import {
ArrowLeft,
Braces,
EllipsisVertical,
FileCode,
Pencil,
Trash,
ArrowLeft,
Braces,
EllipsisVertical,
FileCode,
Filter,
Pencil,
SortAsc,
Trash,
} from "lucide-react";
import { use, useEffect, useState } from "react";
import { toast } from "sonner";
import { findSupportedOperations } from "../file/[filename]/page";
export default function ServerListModificationFrame() {
const { user } = useUser();
const files =
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [];
return (
<main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full flex items-center gap-2">
<Link href="/servers/embedded/sl-modification-frame">
<ArrowLeft size={16} />
</Link>
Files
</h1>
<Material className="grid gap-1 mt-4">
{files.length === 0 && (
<Placeholder
icon={<Braces />}
title="We couldn't find any files"
description="Try creating a filter!"
/>
)}
{files.map((c, i) => (
<Link
href={`/servers/embedded/sl-modification-frame/file/${c.name}`}
className="w-full py-1 px-2 rounded-xl flex items-center gap-1 justify-between hover:bg-slate-100 dark:hover:bg-zinc-700/30"
key={c.name}
>
<span className="flex items-center gap-1">
<FileCode size={16} />
{c.name}.ts
</span>
<span>
<DropdownMenu>
<DropdownMenuTrigger>
<Button
variant="tertiary"
className="flex items-center justify-center hover:bg-slate-200 dark:hover:bg-zinc-700/60"
size="square-sm"
>
<EllipsisVertical
size={16}
className="text-muted-foreground"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={async (e) => {
e.stopPropagation();
const startTime = Date.now();
files.splice(i, 1);
await user?.update({
unsafeMetadata: {
customFiles: files,
},
});
toast.success(
"Deleted file in " + (Date.now() - startTime) + "ms"
);
}}
>
<Trash size={16} /> Delete
</DropdownMenuItem>
<DropdownMenuItem className="flex items-center gap-2">
<Pencil size={16} /> Rename
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</span>
</Link>
))}
</Material>
</main>
);
const { user } = useUser();
const files =
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [];
const operations = usePlatforms(files);
return (
<main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full flex items-center gap-2">
<Link href="/servers/embedded/sl-modification-frame">
<ArrowLeft size={16} />
</Link>
Files
</h1>
<Material className="grid gap-1 mt-4">
{files.length === 0 && (
<Placeholder
icon={<Braces />}
title="We couldn't find any files"
description="Try creating a filter!"
/>
)}
{files.map((c, i) => (
<Link
href={`/servers/embedded/sl-modification-frame/file/${c.name}`}
className="w-full py-1 px-2 rounded-xl flex items-center gap-1 justify-between hover:bg-slate-100 dark:hover:bg-zinc-700/30"
key={c.name}
>
<span className="flex items-center gap-1">
<FileCode size={16} />
{operations[i].filter && <Filter size={16} />}
{operations[i].sort && <SortAsc size={16} />}
{c.name}.ts
</span>
<span>
<DropdownMenu>
<DropdownMenuTrigger>
<DropdownMenu></DropdownMenu>
<Button
variant="tertiary"
className="flex items-center justify-center hover:bg-slate-200 dark:hover:bg-zinc-700/60"
size="square-sm"
>
<EllipsisVertical
size={16}
className="text-muted-foreground"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={async (e) => {
e.stopPropagation();
const startTime = Date.now();
files.splice(i, 1);
await user?.update({
unsafeMetadata: {
...user.unsafeMetadata,
customFiles: files,
},
});
toast.success(
"Deleted file in " + (Date.now() - startTime) + "ms",
);
}}
>
<Trash size={16} /> Delete
</DropdownMenuItem>
<DropdownMenuItem className="flex items-center gap-2">
<Pencil size={16} /> Rename
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</span>
</Link>
))}
</Material>
</main>
);
}
function usePlatforms(files: Array<ClerkCustomModification>) {
const [result, setResult] = useState<
Array<{ sort: boolean; filter: boolean }>
>([]);
useEffect(() => {
(async () => {
setResult(
await Promise.all(
files.map(async (c) => await findSupportedOperations(c.contents)),
),
);
})();
}, [files]);
return result;
}

@ -34,68 +34,100 @@ 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 { ArrowRight } from "lucide-react";
import { ArrowRight, Binary } from "lucide-react";
import { Button } from "@/components/ui/button";
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() {
const router = useRouter();
const router = useRouter();
const { user } = useUser();
return (
<main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full">Filters & Sorting</h1>
<div className="flex items-center gap-2 my-2">
<Button size="sm">Active modifications</Button>
<Link href="/servers/embedded/sl-modification-frame/files">
<Button size="sm">Custom files</Button>
</Link>
<Button size="sm">Settings</Button>
</div>
<span className="text-wrap pt-2">
Pick out different filters & sorting systems to customize your server
viewing experience. We frequently add new filters in accordance to new
features, as well.
</span>
<Material className="mt-10 p-4">
{serverModDB.map((c) => (
<span key={c.displayTitle}>
<h2 className="text-lg font-bold pb-3 flex justify-between">
{c.displayTitle}
<Link
href={`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}`}
className="flex gap-2 text-sm font-normal items-center"
>
<ArrowRight size={16} />
View more
</Link>
</h2>
<div className="grid grid-cols-6 gap-2">
{c.entries.map((m) => (
<Material
elevation="high"
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
key={m.name}
onClick={() =>
router.push(
`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}/modification/${btoa(m.name)}`
)
}
>
<div
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
style={{ backgroundColor: m.color }}
>
<m.icon 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.name}
</span>
</Material>
))}
</div>
</span>
))}
</Material>
</main>
);
return (
<main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full">Filters & Sorting</h1>
<div className="flex items-center gap-2 my-2">
<Button size="sm">Active modifications</Button>
<Link href="/servers/embedded/sl-modification-frame/files">
<Button size="sm">Custom files</Button>
</Link>
<Button size="sm">Settings</Button>
</div>
<span className="text-wrap pt-2">
Pick out different filters & sorting systems to customize your server
viewing experience. We frequently add new filters in accordance to new
features, as well.
</span>
<Material className="mt-10 p-4">
{serverModDB.map((c) => (
<div key={c.displayTitle} className="my-4">
<h2 className="text-lg font-bold pb-3 flex justify-between">
{c.displayTitle}
<Link
href={`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}`}
className="flex gap-2 text-sm font-normal items-center"
>
<ArrowRight size={16} />
View more
</Link>
</h2>
<div className="grid grid-cols-6 gap-2">
{c.entries.map((m) => (
<Material
elevation="high"
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
key={m.name}
onClick={() =>
router.push(
`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}/modification/${btoa(m.name)}`,
)
}
>
<div
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
style={{ backgroundColor: m.color }}
>
<m.icon 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.name}
</span>
</Material>
))}
<SignedIn>
{c.__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/${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>
);
}

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

@ -1,11 +1,15 @@
import { BrandingGenericIcon } from "../icons/branding-icons";
import { BrandingGenericIcon, Discord } from "../icons/branding-icons";
import { Link } from "../../util/link";
import { FooterStatus } from "./status";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import Github from "@/components/ui/github";
import Image from "next/image"
export function Footer() {
return (
<footer className="w-full border-t p-[20px] mt-15">
<div className="flex justify-between items-center">
<div className="flex justify-between items-start">
<span className="flex items-center gap-4 text-muted-foreground">
<Link href="Special:Root">
<BrandingGenericIcon className="max-w-[32px] max-h-[32px]" />
@ -37,7 +41,46 @@ export function Footer() {
</li>
</ul>
</span>
<FooterStatus />
<div className="block">
<div className="flex items-center mb-2 justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant="tertiary" size="square-md" className="flex items-center">
<Discord className="w-[1.25em] h-[1.25em]" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<Link href="https://t.mhsf.app/d/m" noExtraIcons>
<DropdownMenuItem className="py-2 flex items-center gap-2">
<Image className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" src="https://avatars.githubusercontent.com/u/16529253?s=200&v=4" alt="Minehut" width={30} height={30} />
<span className="block">
Minehut Discord
<small className="flex">Not officially owned by MHSF, however conversations about MHSF and related take place there.</small>
</span>
</DropdownMenuItem>
</Link>
<Link href="https://t.mhsf.app/d/u" noExtraIcons>
<DropdownMenuItem className="py-2 flex items-center gap-2">
<BrandingGenericIcon className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" width={30} height={30} />
<span className="block">
MHSF Discord
<small className="flex">A read-only server for updates related to MHSF.</small>
</span>
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
<Link href="https://github.com/DeveloLongScript/MHSF" noExtraIcons>
<Button variant="tertiary" size="square-md" className="flex items-center">
<Github className="w-[1.25em] h-[1.25em]" />
</Button>
</Link>
</div>
<FooterStatus />
</div>
</div>
<span className="block mt-4">
<small className="text-muted-foreground text-[0.75rem]">

@ -0,0 +1,153 @@
/*
* 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 {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerTitle,
} from "@/components/ui/drawer";
import { Button } from "@/components/ui/button";
import { Alert } from "@/components/ui/alert";
import { compressToEncodedURIComponent } from "lz-string";
import { AlertOctagon, ExternalLink } from "lucide-react";
import type { languages, Uri } from "monaco-editor";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Link } from "@/components/util/link";
import { type RefObject, useEffect, useState } from "react";
import type { MonacoRefType } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
export type SyntaxErrorInterface = languages.typescript.Diagnostic[] | null;
export function CustomErrors({
value,
monacoRef,
filename
}: {
value: string;
monacoRef: RefObject<MonacoRefType | null>;
filename: string;
}) {
const [syntaxErrors, setSyntaxErrors] = useState<SyntaxErrorInterface>();
const validateCode = () => {
if (!monacoRef.current) return;
monacoRef.current.languages.typescript
.getTypeScriptWorker()
.then((worker) => {
worker(
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri,
).then((client) => {
client
.getSemanticDiagnostics(
(
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
).toString(),
)
.then((diags) => {
setSyntaxErrors(diags);
});
});
});
};
validateCode();
// biome-ignore lint: L
useEffect(validateCode, [value]);
if (syntaxErrors !== null && syntaxErrors !== undefined)
return (
<Drawer direction="right">
<DrawerTrigger>
<Button
variant="danger-subtle"
size="square-md"
className="flex items-center justify-center"
>
<AlertOctagon />
</Button>
</DrawerTrigger>
<DrawerContent className="p-4 min-w-[400px] overflow-x-hidden max-h-screen overflow-y-auto">
<DrawerTitle>Type Errors</DrawerTitle>
<div className="p-2">
{syntaxErrors.map((c, i) => (
<Alert
variant={
c.category === 1
? "error"
: c.category === 0
? "warning"
: "info"
}
/* biome-ignore lint: No. */
key={i}
className="gap-1 my-2"
>
{c.messageText.toString()}{" "}
<DropdownMenu>
<DropdownMenuTrigger>
<small className="flex items-center gap-1 cursor-pointer">
(TS{typeof c !== "string" && c.code})
<ExternalLink size={16} />
</small>
</DropdownMenuTrigger>
<DropdownMenuContent>
<Link
noExtraIcons
target="_blank"
href={`https://typescript.tv/errors/#ts${c.code}`}
>
<DropdownMenuItem>typescript.tv</DropdownMenuItem>
</Link>
<Link
noExtraIcons
target="_blank"
href={`https://ts-error-translator.vercel.app/?error=${compressToEncodedURIComponent(c.messageText.toString())}`}
>
<DropdownMenuItem>ts-error-translator</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
</Alert>
))}
</div>
</DrawerContent>
</Drawer>
);
return null;
}

@ -0,0 +1,91 @@
/*
* 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 { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
import { Button } from "@/components/ui/button";
import { tryCatch } from "@/lib/try-catch";
import { toast } from "sonner";
import type { SyntaxErrorInterface } from "./custom-errors";
export function CustomLint({
successfullyLinted,
setSuccessfullyLinted,
value,
syntaxErrors,
}: {
successfullyLinted: boolean;
setSuccessfullyLinted: (change: boolean) => void;
value: string;
syntaxErrors: SyntaxErrorInterface;
}) {
const lintFile = async () => {
toast.info("Transpiling TypeScript...");
const { error, data: transpiledCode } = await tryCatch(
(async () => transpileTypeScript(value))(),
);
if (error) {
toast.error(`Failed to transpile TypeScript! Error: ${error.message}`);
return;
}
const startTime = Date.now();
if (transpiledCode === null) {
toast.error("Cannot continue.");
return;
}
toast.info("Generating function...");
const functionBody = transpiledCode.match(
/function\s+filter\s*\([^)]*\)\s*\{([\s\S]*)\}/,
)?.[1];
const { error: filterErr, data: filterFunc } = await tryCatch(
(async () => new Function("data", functionBody as string))(),
);
if (filterErr) {
toast.error(`Failed to generate function! Error: ${filterErr.message}`);
return;
}
if (typeof filterFunc === "function") {
toast.success(`Linted in ${Date.now() - startTime}ms`);
setSuccessfullyLinted(true);
} else {
toast.error("Code doesn't have a 'filter' function. Cannot be tested.");
toast.error(typeof filterFunc);
}
};
return (
<Button
onClick={lintFile}
disabled={syntaxErrors === null || syntaxErrors.length !== 0}
variant={successfullyLinted ? "success-subtle" : "secondary"}
>
Lint
</Button>
);
}

@ -0,0 +1,150 @@
/*
* 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 { toast } from "sonner";
import type { ClerkCustomActivatedModification } from "../modification-file-creation-dialog";
import { useUser } from "@clerk/nextjs";
import { Alert } from "@/components/ui/alert";
import { Material } from "@/components/ui/material";
import {
Setting,
SettingContent,
SettingDescription,
SettingMeta,
SettingTitle,
} from "@/components/feat/settings/setting";
import { Input } from "@/components/ui/input";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { DialogTrigger } from "@/components/ui/dialog";
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
import { useRouter } from "@/lib/useRouter";
export function CustomTestSuccess({
filename,
testMode,
value,
}: { filename: string; testMode: "filter" | "sort" | ""; value: string }) {
const { user } = useUser();
const [friendlyName, setFriendlyName] = useState("");
const router = useRouter();
return (
<>
<p className="text-sm my-2">
You can now activate this custom modification. Please note that the
filter and sort versions of your modifications will be different, and
the one used will be selected based on what type you tested on.
</p>
{(
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
).find(
(c) => c.originalFileName === filename && c.testMode === testMode,
) !== undefined && (
<Alert className="mb-2 gap-2" variant="warning">
This modification was already activated! Hitting activate here will
just overwrite the contents and the new friendly name.
</Alert>
)}
<Material>
<Setting>
<SettingContent>
<SettingMeta>
<SettingTitle>Name</SettingTitle>
<SettingDescription>
Set a friendly name for your modification.
</SettingDescription>
</SettingMeta>
<Input
placeholder="My cool mod"
value={friendlyName}
onChange={(c) => setFriendlyName(c.target.value)}
/>
</SettingContent>
</Setting>
</Material>
<DialogTrigger>
<Button
className="w-full my-2"
disabled={friendlyName === "" || friendlyName.replace(" ", "") === ""}
onClick={async () => {
const array =
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ??
[];
const index = array.findIndex(
(c) => c.originalFileName === filename && c.testMode === testMode,
);
const color = `#${Math.floor(Math.random() * 16777215).toString(16)}`;
const transpiledValue = transpileTypeScript(value);
if (transpiledValue === null)
return toast.error("Error transpiling");
if (index !== -1) {
// Original already exists
array[index] = {
originalFileName: filename,
friendlyName,
transpiledContents: transpiledValue,
active: true,
testMode: testMode as "filter" | "sort",
color,
};
} else {
array.push({
originalFileName: filename,
friendlyName,
transpiledContents: transpiledValue,
active: true,
testMode: testMode as "filter" | "sort",
color,
});
}
await user?.update({
unsafeMetadata: {
...user.unsafeMetadata,
activatedModifications: array,
},
});
toast.success("Activated!");
router.push("/servers/embedded/sl-modification-frame");
}}
>
Activate
</Button>
</DialogTrigger>
</>
);
}

@ -0,0 +1,182 @@
/*
* 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 { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
import { Setting, SettingContent, SettingDescription, SettingMeta, SettingTitle } from "@/components/feat/settings/setting";
import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent } from "@/components/ui/drawer";
import { Material } from "@/components/ui/material";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { tryCatch } from "@/lib/try-catch";
import { Check } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { CustomTestSuccess } from "./custom-test-success";
export function CustomTest({value, successfullyLinted}: {value: string, successfullyLinted: boolean}) {
const [open, setOpen] = useState(false);
const [filterEnabled, setFilterEnabled] = useState(true);
const [sortEnabled, setSortEnabled] = useState(true);
const [success, setSuccess] = useState(false);
const [fileName, setFileName] = useState("");
const [testMode, setTestMode] = useState<"filter" | "sort" | "">("");
// biome-ignore lint: values needed (but not shown by linter)
useEffect(() => setSuccess(false), [value]);
// biome-ignore lint: values not needed
useEffect(() => {
setFilterEnabled(true);
setSortEnabled(true);
setTestMode("");
(async () => {
const transpiledValue = transpileTypeScript(value);
const functionBody = transpiledValue
?.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
const { error: filterErr, data: filterFunc } = await tryCatch(
(async () =>
new Function(
"server",
`${functionBody}
return filter(server)`,
))(),
);
const { error: sortErr, data: sortFunc } = await tryCatch(
(async () =>
new Function(
"serverA",
"serverB",
`${functionBody}
return sort(serverA, serverB)`,
))(),
);
if (filterErr) setFilterEnabled(false);
if (sortErr) setSortEnabled(false);
try {
filterFunc?.({});
} catch (e) {
if (String(e).startsWith("ReferenceError: filter is not defined")) {
setFilterEnabled(false);
}
}
try {
sortFunc?.({}, {});
} catch (e) {
if (String(e).startsWith("ReferenceError: sort is not defined")) {
setSortEnabled(false);
}
}
})();
}, [open, value]);
return (
<>
<Button disabled={!successfullyLinted} onClick={() => setOpen(true)}>
Test
</Button>
<Drawer direction="right" open={open} onOpenChange={setOpen}>
<DrawerContent className="p-4 min-w-[400px] overflow-x-hidden max-h-screen overflow-y-auto">
<p className="text-sm mb-2">
You can run an interactive server-list environment with actual
online servers to test your modifications.
</p>
<Material>
<Setting>
<SettingContent>
<SettingMeta>
<SettingTitle>Function to test</SettingTitle>
<SettingDescription>
You can pick to either test a sorting system or a filter.
</SettingDescription>
</SettingMeta>
<Select
value={testMode}
onValueChange={setTestMode as (change: string) => void}
disabled={success}
>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="filter" disabled={!filterEnabled}>
<code>filter</code>
</SelectItem>
<SelectItem value="sort" disabled={!sortEnabled}>
<code>sort</code>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</SettingContent>
</Setting>
</Material>
<Button
className="w-full mt-2 flex items-center gap-2"
disabled={testMode === "" || success}
variant={success ? "success-subtle" : "default"}
onClick={() => {
const t = btoa(value);
const newTab = window.open(
`/servers?tm=${encodeURIComponent(t)}`,
);
const interval = setInterval(() => {
newTab?.dispatchEvent(
new Event(`test-mode.enable.${testMode}`),
);
}, 500);
toast.info("Waiting for server tab to pick up thread...");
newTab?.addEventListener("test-mode.enabled", () => {
clearInterval(interval);
toast.success("Connected to new tab; continue.");
newTab?.addEventListener("test-mode.success", () => {
toast.success("Resolved success from thread!");
setSuccess(true);
});
});
}}
>
{success && <Check size={16} />}Test
</Button>
{success && (
<CustomTestSuccess filename={fileName} testMode={testMode} value={value} />
)}
</DrawerContent>
</Drawer>
</>
);
}

@ -0,0 +1,66 @@
/*
* 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 { useUser } from "@clerk/nextjs";
import { ClerkCustomActivatedModification } from "./modification-file-creation-dialog";
import { Link } from "@/components/util/link";
import { Material } from "@/components/ui/material";
import { Binary } from "lucide-react";
export function ModificationCustomModificationRow({category}: {category: string}) {
const { user } = useUser();
return (
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
).map((m) => (
<Link
href={`/servers/embedded/sl-modification-frame/category/${category}/modification/_custom/${btoa(m.friendlyName)}`}
key={m.friendlyName}
>
<Material
elevation="high"
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
>
<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>
</Link>
));
}

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

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

@ -5,11 +5,11 @@ import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
import type { RouteParams } from "@/pages/api/v1/server/get/[server]";
import { useEffect, useState } from "react";
import {
Setting,
SettingContent,
SettingDescription,
SettingMeta,
SettingTitle,
Setting,
SettingContent,
SettingDescription,
SettingMeta,
SettingTitle,
} from "../../settings/setting";
import { Spinner } from "@/components/ui/spinner";
import { codeToHtml } from "shiki";
@ -22,166 +22,178 @@ import { convert } from "../util";
import { Switch } from "@/components/ui/switch";
export function DebugMenu({
debugOptions,
setOpen,
open,
debugOptions,
setOpen,
open,
}: {
debugOptions: {
serverName: string;
serverId: string;
mhsfData: (MHSFData & RouteParams) | null;
serverData: ServerResponse | null;
onlineServerData: OnlineServer | null;
};
open: boolean;
setOpen: (newState: boolean) => void;
debugOptions: {
serverName: string;
serverId: string;
mhsfData: (MHSFData & RouteParams) | null;
serverData: ServerResponse | null;
onlineServerData: OnlineServer | null;
};
open: boolean;
setOpen: (newState: boolean) => void;
}) {
const [mhsfShikiParsed, setMHSFShikiParsed] = useState("");
const [mhShikiParsed, setMHShikiParsed] = useState("");
const clipboard = useClipboard();
const { resolvedTheme } = useTheme();
const [mhsfShikiParsed, setMHSFShikiParsed] = useState("");
const [mhShikiParsed, setMHShikiParsed] = useState("");
const clipboard = useClipboard();
const { resolvedTheme } = useTheme();
useEffect(() => {
(async () => {
setMHSFShikiParsed(
await codeToHtml(JSON.stringify(debugOptions.mhsfData, null, 2), {
lang: "json",
theme: resolvedTheme === "dark" ? "vitesse-dark" : "vitesse-light",
})
);
setMHShikiParsed(
await codeToHtml(JSON.stringify(debugOptions.serverData, null, 2), {
lang: "json",
theme: resolvedTheme === "dark" ? "vitesse-dark" : "vitesse-light",
})
);
})();
}, [debugOptions]);
useEffect(() => {
(async () => {
setMHSFShikiParsed(
await codeToHtml(JSON.stringify(debugOptions.mhsfData, null, 2), {
lang: "json",
theme: resolvedTheme === "dark" ? "vitesse-dark" : "vitesse-light",
}),
);
setMHShikiParsed(
await codeToHtml(JSON.stringify(debugOptions.serverData, null, 2), {
lang: "json",
theme: resolvedTheme === "dark" ? "vitesse-dark" : "vitesse-light",
}),
);
})();
}, [debugOptions]);
return (
<Drawer onOpenChange={setOpen} open={open} direction="right">
<DrawerContent className="p-4 min-w-[600px] overflow-x-hidden max-h-screen overflow-y-auto">
<DrawerTitle className="text-lg mb-3 flex items-center gap-2">
<Wrench size={24} /> Debug Options
</DrawerTitle>
<span className="m-2 mt-1 text-sm">
This data is only designed for developers; it contains every single
piece of information MHSF knows about the server. Could be useful for
adding new backend options or endpoints.{" "}
<strong>
This only shows up when Debug Mode is enabled. (or when using
Ctrl+Shift+O)
</strong>
</span>
<Material className="mb-2">
<Setting>
<SettingContent>
<SettingMeta>
<SettingTitle>Server name</SettingTitle>
<SettingDescription>
Name of server after being parsed through Minehut API (aka
server.name)
</SettingDescription>
</SettingMeta>
{debugOptions.serverName}
</SettingContent>
</Setting>
</Material>
<Material className="mb-2">
<Setting>
<SettingContent>
<SettingMeta>
<SettingTitle>Server Id</SettingTitle>
<SettingDescription>
Passed usually through query
</SettingDescription>
</SettingMeta>
{debugOptions.serverId}
</SettingContent>
</Setting>
</Material>
<Material className="mb-2">
<strong className="flex items-center gap-2">
{debugOptions.serverData === null && <Spinner />} Parsed Minehut
data
<Button
size="sm"
onClick={() =>
clipboard.writeText(JSON.stringify(debugOptions.serverData))
}
>
Copy (no toast!)
</Button>
</strong>
<span
dangerouslySetInnerHTML={{ __html: mhShikiParsed }}
className="break-all max-w-[100px]"
/>
</Material>
<Material className="mb-2">
<strong className="flex items-center gap-2">
{debugOptions.mhsfData === null && <Spinner />} Parsed MHSF data
<Button
size="sm"
onClick={() =>
clipboard.writeText(JSON.stringify(debugOptions.mhsfData))
}
>
Copy (no toast!)
</Button>
</strong>
{debugOptions.mhsfData !== null && (
<>
<Setting className="py-3">
<SettingContent>
<SettingMeta>
<SettingTitle>See all data</SettingTitle>
<SettingDescription>
WARNING: this data is MASSIVE. (@keyboard yk what else is
massive?)
</SettingDescription>
</SettingMeta>
<DebugShikiParsedDrawer shikiParsed={mhsfShikiParsed}>
<Button>Open data</Button>
</DebugShikiParsedDrawer>
</SettingContent>
</Setting>
<Setting className="py-3">
<SettingContent>
<SettingMeta>
<SettingTitle>Total Statistical Data Count</SettingTitle>
<SettingDescription>
How many times has MHSF grabbed data about this server?
</SettingDescription>
</SettingMeta>
{convert(
debugOptions.mhsfData.achievements.historically.length +
debugOptions.mhsfData.playerData.historically.length +
debugOptions.mhsfData.favoriteData.favoriteHistoricalData
.length
)}
</SettingContent>
</Setting>
return (
<Drawer onOpenChange={setOpen} open={open} direction="right">
<DrawerContent className="p-4 min-w-[600px] overflow-x-hidden max-h-screen overflow-y-auto">
<DrawerTitle className="text-lg mb-3 flex items-center gap-2">
<Wrench size={24} /> Debug Options
</DrawerTitle>
<span className="m-2 mt-1 text-sm">
This data is only designed for developers; it contains every single
piece of information MHSF knows about the server. Could be useful for
adding new backend options or endpoints.{" "}
<strong>
This only shows up when Debug Mode is enabled. (or when using
Ctrl+Shift+O)
</strong>
</span>
<Material className="mb-2">
<Setting>
<SettingContent>
<SettingMeta>
<SettingTitle>Server name</SettingTitle>
<SettingDescription>
Name of server after being parsed through Minehut API (aka
server.name)
</SettingDescription>
</SettingMeta>
{debugOptions.serverName}
</SettingContent>
</Setting>
</Material>
<Material className="mb-2">
<Setting>
<SettingContent>
<SettingMeta>
<SettingTitle>Server Id</SettingTitle>
<SettingDescription>
Passed usually through query
</SettingDescription>
</SettingMeta>
{debugOptions.serverId}
</SettingContent>
</Setting>
</Material>
<Material className="mb-2">
<strong className="flex items-center gap-2">
{debugOptions.serverData === null && <Spinner />} Parsed Minehut
data
<Button
size="sm"
onClick={() =>
clipboard.writeText(JSON.stringify(debugOptions.serverData))
}
>
Copy (no toast!)
</Button>
</strong>
<span
dangerouslySetInnerHTML={{ __html: mhShikiParsed }}
className="break-all max-w-[100px]"
/>
</Material>
<Material className="mb-2">
<strong className="flex items-center gap-2">
{debugOptions.mhsfData === null && <Spinner />} Parsed MHSF data
<Button
size="sm"
onClick={() =>
clipboard.writeText(JSON.stringify(debugOptions.mhsfData))
}
>
Copy (no toast!)
</Button>
</strong>
{debugOptions.mhsfData !== null && (
<>
<Setting className="py-3">
<SettingContent>
<SettingMeta>
<SettingTitle>See all data</SettingTitle>
<SettingDescription>
WARNING: this data is MASSIVE. (@keyboard yk what else is
massive?)
</SettingDescription>
</SettingMeta>
<DebugShikiParsedDrawer shikiParsed={mhsfShikiParsed}>
<Button>Open data</Button>
</DebugShikiParsedDrawer>
</SettingContent>
</Setting>
<Setting className="py-3">
{debugOptions.mhsfData !== undefined && <SettingContent>
<SettingMeta>
<SettingTitle>Total Statistical Data Count</SettingTitle>
<SettingDescription>
How many times has MHSF grabbed data about this server?
</SettingDescription>
</SettingMeta>
{convert(
(
debugOptions.mhsfData.achievements ?? {
historically: { length: 0 },
}
).historically.length +
(
debugOptions.mhsfData.playerData ?? {
historically: { length: 0 },
}
).historically.length +
(
debugOptions.mhsfData.favoriteData ?? {
favoriteHistoricalData: { length: 0 },
}
).favoriteHistoricalData.length,
)}
</SettingContent>}
<Setting className="py-3">
<SettingContent>
<SettingMeta>
<SettingTitle>
Disable image caching on customization images
</SettingTitle>
<SettingDescription>
Enabling this could result in being tracked but{" "}
<strong>very rarely</strong> could render the image
faster. (removes wsrv.nl caching)
</SettingDescription>
</SettingMeta>
<Switch />
</SettingContent>
</Setting>
</>
)}
</Material>
</DrawerContent>
</Drawer>
);
</Setting>
<Setting className="py-3">
<SettingContent>
<SettingMeta>
<SettingTitle>
Disable image caching on customization images
</SettingTitle>
<SettingDescription>
Enabling this could result in being tracked but{" "}
<strong>very rarely</strong> could render the image
faster. (removes wsrv.nl caching)
</SettingDescription>
</SettingMeta>
<Switch />
</SettingContent>
</Setting>
</>
)}
</Material>
</DrawerContent>
</Drawer>
);
}

@ -4,10 +4,10 @@ import type { ServerResponse } from "@/lib/types/mh-server";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
import { cn } from "@/lib/utils";
@ -15,156 +15,176 @@ import { useQueryState } from "nuqs";
import { Badge } from "@/components/ui/badge";
import { convert } from "../util";
import { Material } from "@/components/ui/material";
import { Spinner } from "@/components/ui/spinner";
import { Placeholder } from "@/components/ui/placeholder";
import { CircleSlash } from "lucide-react";
export function StatisticsMainRow({
server,
mhsfData,
server,
mhsfData,
}: {
server: ServerResponse;
mhsfData: ReturnType<typeof useMHSFServer>;
server: ServerResponse;
mhsfData: ReturnType<typeof useMHSFServer>;
}) {
const [statisticType, setStatisticType] = useQueryState("st", {
defaultValue: "playerCount",
});
const [statisticType, setStatisticType] = useQueryState("st", {
defaultValue: "playerCount",
});
return (
<Material
className="relative col-span-2 h-[250px] max-lg:mt-3"
padding="none"
>
<div className="p-4">
<span className="flex gap-4 mb-2">
<strong className="text-lg">Statistics</strong>
<button
type="button"
className={cn(
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
"rounded-xl px-2 flex items-center gap-2",
statisticType === "playerCount" &&
"bg-slate-100 dark:bg-zinc-700/30 font-medium"
)}
onClick={() => setStatisticType("playerCount")}
>
Player Count
<Badge className="px-1">
<code>{convert(server.joins)}</code>
</Badge>
</button>
<button
type="button"
className={cn(
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
"rounded-xl px-2 flex items-center gap-2",
statisticType === "favorites" &&
"bg-slate-100 dark:bg-zinc-700/30 font-medium"
)}
onClick={() => setStatisticType("favorites")}
>
Favorites
<Badge className="px-1">
<code>
{convert(
mhsfData.server?.favoriteData.favoriteNumber as number
)}
</code>
</Badge>
</button>
</span>
<Separator />
</div>
<div className="mt-2">
{!mhsfData.loading && (
<StatisticsChart
data={
statisticType === "playerCount"
? mhsfData.server?.playerData.historically
: mhsfData.server?.favoriteData.favoriteHistoricalData
}
mainDataPoint={statisticType}
/>
)}
</div>
</Material>
);
return (
<Material
className="relative col-span-2 h-[250px] max-lg:mt-3"
padding="none"
>
<div className="p-4">
<span className="flex gap-4 mb-2">
<strong className="text-lg">Statistics</strong>
<button
type="button"
className={cn(
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
"rounded-xl px-2 flex items-center gap-2",
statisticType === "playerCount" &&
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
)}
onClick={() => setStatisticType("playerCount")}
>
Player Count
<Badge className="px-1">
<code>{convert(server.joins)}</code>
</Badge>
</button>
<button
type="button"
className={cn(
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
"rounded-xl px-2 flex items-center gap-2",
statisticType === "favorites" &&
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
)}
onClick={() => setStatisticType("favorites")}
>
Favorites
<Badge className="px-1">
<code>
{convert(
mhsfData.server?.favoriteData.favoriteNumber as number,
)}
</code>
</Badge>
</button>
</span>
<Separator />
</div>
<div className="mt-2">
{!mhsfData.loading ? (
<>
{(statisticType === "playerCount"
? mhsfData.server?.playerData.historically
: mhsfData.server?.favoriteData.favoriteHistoricalData
)?.length !== 0 ? (
<StatisticsChart
data={
statisticType === "playerCount"
? mhsfData.server?.playerData.historically
: mhsfData.server?.favoriteData.favoriteHistoricalData
}
mainDataPoint={statisticType}
/>
) : (
<span className="w-full h-full items-center justify-center flex">
<Placeholder
icon={<CircleSlash />}
title="There is no data to be collected"
description="This server probably never had any data collected in your choosen timespan."
/>
</span>
)}
</>
) : (
<Spinner />
)}
</div>
</Material>
);
}
const chartConfig = {
playerCount: {
label: "Joins",
color: "hsl(var(--chart-1))",
},
favorites: {
label: "Favorites",
color: "hsl(var(--chart-2))",
},
playerCount: {
label: "Joins",
color: "hsl(var(--chart-1))",
},
favorites: {
label: "Favorites",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
export function StatisticsChart({
data,
mainDataPoint,
data,
mainDataPoint,
}: {
data: any;
mainDataPoint: string;
data: any;
mainDataPoint: string;
}) {
console.log(data);
return (
<ChartContainer config={chartConfig} className="max-h-[202px] min-w-full">
<AreaChart
accessibilityLayer
data={data.slice(data.length - 30, data.length)}
margin={{
top: 30,
}}
className="rounded-b-xl"
>
<CartesianGrid vertical={false} horizontal={false} />
<XAxis dataKey="date" tickLine={false} axisLine={false} tick={false} />
<ChartTooltip
content={
<ChartTooltipContent
className="w-[150px]"
nameKey={mainDataPoint}
indicator="line"
labelFormatter={(value) => {
return `${new Date(value).toLocaleDateString("en-US", {
day: "numeric",
month: "short",
})} ${new Date(value).toLocaleTimeString("en-US", {
timeStyle: "short",
})}`;
}}
/>
}
/>
<defs>
<linearGradient
id={`fill${mainDataPoint}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="25%"
stopColor={`var(--color-${mainDataPoint})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${mainDataPoint})`}
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<Area
dataKey={mainDataPoint}
type="natural"
fill={`url(#fill${mainDataPoint})`}
fillOpacity={0.4}
stroke={`var(--color-${mainDataPoint})`}
stackId="a"
/>
</AreaChart>
</ChartContainer>
);
console.log(data);
return (
<ChartContainer config={chartConfig} className="max-h-[202px] min-w-full">
<AreaChart
accessibilityLayer
data={data.slice(data.length - 30, data.length)}
margin={{
top: 30,
}}
className="rounded-b-xl"
>
<CartesianGrid vertical={false} horizontal={false} />
<XAxis dataKey="date" tickLine={false} axisLine={false} tick={false} />
<ChartTooltip
content={
<ChartTooltipContent
className="w-[150px]"
nameKey={mainDataPoint}
indicator="line"
labelFormatter={(value) => {
return `${new Date(value).toLocaleDateString("en-US", {
day: "numeric",
month: "short",
})} ${new Date(value).toLocaleTimeString("en-US", {
timeStyle: "short",
})}`;
}}
/>
}
/>
<defs>
<linearGradient
id={`fill${mainDataPoint}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="25%"
stopColor={`var(--color-${mainDataPoint})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${mainDataPoint})`}
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<Area
dataKey={mainDataPoint}
type="natural"
fill={`url(#fill${mainDataPoint})`}
fillOpacity={0.4}
stroke={`var(--color-${mainDataPoint})`}
stackId="a"
/>
</AreaChart>
</ChartContainer>
);
}

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

@ -51,8 +51,7 @@ type ModDBCategory = {
export const serverModDB: ModDBCategory[] = [
{
displayTitle: "Custom Files",
__custom: true,
displayTitle: "Create Custom Files",
description:
`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.`,
@ -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: []
}
];

@ -112,11 +112,10 @@ export const allTags: Array<{
</>
),
condition: async (c) =>
(c.online === undefined ? c.server?.playerCount : c.online.playerData) ===
(c.online === undefined ? c.server?.playerCount: c.online.playerData.playerCount) ===
0,
htmlDocs: "Nobody is online this server.",
tooltipDesc: "Nobody is online this server.",
role: "gray-subtle",
docsName: "Nobody Online",
__filter: true,

@ -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";
export function useFilters(data: OnlineServer[]) {
const [filteredData, setFilteredData] = useState<OnlineServer[]>(data);
const [t] = useQueryState("tm");
const [testModeEnabled, setTestModeEnabled] = useState(false);
const [testModeStatus, setTestModeStatus] = useState("Haven't connected thread yet (if stuck, select the other tab, and come back)");
const [testModeLoading, setTestModeLoading] = useState(true);
const [filteredData, setFilteredData] = useState<OnlineServer[]>(data);
const [t] = useQueryState("tm");
const [testModeEnabled, setTestModeEnabled] = useState(false);
const [testModeStatus, setTestModeStatus] = useState(
"Haven't connected thread yet (if stuck, select the other tab, and come back)",
);
const [testModeLoading, setTestModeLoading] = useState(true);
useEffect(() => {
if (filteredData.length === 0) setFilteredData(data);
}, [data, filteredData.length]);
useEffect(() => {
if (filteredData.length === 0) setFilteredData(data);
}, [data, filteredData.length]);
useEffect(() => {
if (data.length !== 0)
window.addEventListener("test-mode.enable", (c) => {
window.dispatchEvent(new Event("test-mode.enabled"));
if (!t) {
toast.error("Couldn't enable test mode; no query variable.");
} else {
setTestModeEnabled(true);
const code = atob(t);
(async () => {
setTestModeStatus("Transpiling TypeScript...");
const startTime = Date.now();
const { error, data: transpiledCode } = await tryCatch(
(async () => transpileTypeScript(code))()
);
if (error) {
setTestModeStatus(
"Failed to transpile TypeScript! Error: " + error.message
);
setTestModeLoading(false);
return;
}
if (transpiledCode === null) {
setTestModeStatus("Cannot continue.");
setTestModeLoading(false);
return;
}
console.log(
"[MHSF Filters] Transpiled TypeScript:",
transpiledCode ?? ""
);
setTestModeStatus("Generating function...");
if (
!transpiledCode.includes("export default") &&
!transpiledCode.includes("export")
) {
setTestModeStatus(
"Transpiled code does not contain any export statements."
);
setTestModeLoading(false);
return;
}
const functionBody = transpiledCode
.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
const { error: filterErr, data: filterFunc } = await tryCatch(
(async () =>
new Function(
"server",
`${functionBody}
const testModeInit = (type: "filter" | "sort") => {
window.dispatchEvent(new Event("test-mode.enabled"));
if (!t) {
toast.error("Couldn't enable test mode; no query variable.");
} else {
setTestModeEnabled(true);
const code = atob(t);
(async () => {
setTestModeStatus("Transpiling TypeScript...");
const startTime = Date.now();
const { error, data: transpiledCode } = await tryCatch(
(async () => transpileTypeScript(code))(),
);
if (error) {
setTestModeStatus(
"Failed to transpile TypeScript! Error: " + error.message,
);
setTestModeLoading(false);
return;
}
if (transpiledCode === null) {
setTestModeStatus("Cannot continue.");
setTestModeLoading(false);
return;
}
setTestModeStatus("Generating function...");
if (
!transpiledCode.includes("export default") &&
!transpiledCode.includes("export")
) {
setTestModeStatus(
"Transpiled code does not contain any export statements.",
);
setTestModeLoading(false);
return;
}
const functionBody = transpiledCode
.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
const { error: filterErr, data: filterFunc } = await tryCatch(
(async () =>
type === "filter"
? new Function(
"server",
`${functionBody}
return filter(server)`
))()
);
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 = [];
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]);
return filter(server)`,
)
: new Function(
"serverA",
"serverB",
`${functionBody}
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) => {
window.addEventListener('message', (e) => {
console.log(e);
if (e.data.__key === key) {
callback(e.data)
}

@ -35,6 +35,7 @@ import {
} from "@clerk/nextjs/server";
import { type NextRequest, NextResponse } from "next/server";
import type { ServerResponse } from "./lib/types/mh-server";
import { getBackendProcedure } from "./lib/backend-procedure";
// Thanks for the router matcher API Clerk <3
const isRootRoute = createRouteMatcher(["/"]);
@ -57,22 +58,6 @@ export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
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)) {
const minehut = await fetch(
`https://api.minehut.com/server/${req.url.split("/server/")[1].split("/")[0]}?byName=true`,

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

@ -99,7 +99,7 @@
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@biomejs/biome@^1.8.3":
"@biomejs/biome@^1.9.4":
version "1.9.4"
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.9.4.tgz#89766281cbc3a0aae865a7ff13d6aaffea2842bf"
integrity sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==
@ -3223,11 +3223,43 @@
lodash.merge "^4.6.2"
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":
version "0.23.0"
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
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":
version "4.0.6"
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"
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"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89"
integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==
@ -3503,13 +3535,20 @@
dependencies:
"@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"
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb"
integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==
dependencies:
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":
version "1.20.6"
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"
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:
version "1.0.3"
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"
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:
version "2.2.0"
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"
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:
version "2.1.1"
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"
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:
version "7.2.0"
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"
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"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.4.4.tgz#cec5dbac5850adebdba71fbdf90e6e9a7723c3d6"
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"
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"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==