Compare commits

..

No commits in common. "d93693dc2d84fe2119e210595eaaf9ad4a243c7f" and "bddf5f15287da9f6c358e3d5caf721d9cbfb65cc" have entirely different histories.

30 changed files with 1289 additions and 2382 deletions

@ -4,9 +4,6 @@
## The MHSF Project ## 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. 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 ## Security

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

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

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

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

@ -4,29 +4,37 @@ import { use, useEffect, useRef, useState } from "react";
import { useUser } from "@clerk/nextjs"; import { useUser } from "@clerk/nextjs";
import type { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog"; import type { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import { Link } from "@/components/util/link"; import { Link } from "@/components/util/link";
import { ArrowLeft, FileQuestion } from "lucide-react"; import { AlertOctagon, ArrowLeft, ExternalLink } from "lucide-react";
import Editor from "@monaco-editor/react"; import Editor from "@monaco-editor/react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner"; import { toast } from "sonner";
import * as ts from "typescript"; import * as ts from "typescript";
import useClipboard from "@/lib/useClipboard"; import useClipboard from "@/lib/useClipboard";
import { useTheme } from "@/lib/hooks/use-theme"; import { useTheme } from "@/lib/hooks/use-theme";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import type { languages } from "monaco-editor"; 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 { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { debounce } from "lodash"; import { debounce } from "lodash";
import { tryCatch } from "@/lib/try-catch"; 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? const typeDefs = `// Hi :) how'd you get here?
// Here, in return I'll provide you with a random number: ${Math.ceil(Math.random() * 100)} // Here, in return I'll provide you with a random number: ${Math.ceil(Math.random() * 100)}
@ -69,269 +77,378 @@ export namespace Minehut {
`; `;
export const transpileTypeScript = (code: string) => { export const transpileTypeScript = (code: string) => {
try { try {
const result = ts.transpileModule(typeDefs + code, { const result = ts.transpileModule(typeDefs + code, {
compilerOptions: { compilerOptions: {
module: ts.ModuleKind.ESNext, module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext, target: ts.ScriptTarget.ESNext,
jsx: ts.JsxEmit.ReactJSX, jsx: ts.JsxEmit.ReactJSX,
esModuleInterop: true, esModuleInterop: true,
}, },
}); });
return result.outputText; return result.outputText;
} catch (error) { } catch (error) {
console.error("TypeScript transpilation error:", error); console.error("TypeScript transpilation error:", error);
toast.error(`TypeScript error: ${error}`); toast.error(`TypeScript error: ${error}`);
return null; return null;
} }
}; };
const geistMono = Geist_Mono({ subsets: ["latin"] });
export default function CustomFilePage({ export default function CustomFilePage({
params, params,
}: { }: {
params: Promise<{ filename: string }>; params: Promise<{ filename: string }>;
}) { }) {
const { filename } = use(params); const { filename } = use(params);
const { user } = useUser(); const { user } = useUser();
const monacoRef = const monacoRef =
useRef<typeof import("monaco-editor/esm/vs/editor/editor.api")>(null); useRef<typeof import("monaco-editor/esm/vs/editor/editor.api")>(null);
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const [successfullyLinted, setSuccessfullyLinted] = useState(false); const [successfullyLinted, setSuccessfullyLinted] = useState(false);
const [syntaxErrors, setSyntaxErrors] = useState< const [syntaxErrors, setSyntaxErrors] = useState<
languages.typescript.Diagnostic[] | null languages.typescript.Diagnostic[] | null
>(null); >(null);
const file = ( const file = (
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [] (user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
).findIndex((c) => c.name === filename); ).findIndex((c) => c.name === filename);
if (file === -1) { if (file === -1) {
return ( return <>Bruh.</>;
<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 fileContents = ((user?.unsafeMetadata const validateCode = (code: string) => {
.customFiles as Array<ClerkCustomModification>) ?? [])[file].contents; if (!monacoRef.current) return;
const [value, setValue] = useState(fileContents);
const clipboard = useClipboard();
const saveFile = async () => { monacoRef.current.languages.typescript
const metadata = .getTypeScriptWorker()
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? .then((worker) => {
[]; worker(
const index = ( monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [] ).then((client) => {
).findIndex((c) => c.name === filename); client
.getSemanticDiagnostics(
(
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
).toString()
)
.then((diags) => {
setSyntaxErrors(diags);
});
});
});
};
metadata[index].contents = value; const fileContents = ((user?.unsafeMetadata
.customFiles as Array<ClerkCustomModification>) ?? [])[file].contents;
const [value, setValue] = useState(fileContents);
const clipboard = useClipboard();
validateCode(value);
await user?.update({ const saveFile = async () => {
unsafeMetadata: { const metadata =
...user.unsafeMetadata, (user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ??
customFiles: metadata, [];
}, const index = (
}); (user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
}; ).findIndex((c) => c.name === filename);
const debouncedSave = debounce(async () => { metadata[index].contents = value;
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);
// biome-ignore lint: L await user?.update({
useEffect(() => { unsafeMetadata: {
setSuccessfullyLinted(false); customFiles: metadata,
debouncedSave(); },
}, [value]); });
};
return ( const lintFile = async () => {
<main className="max-w-[800px] p-4"> toast.info("Transpiling TypeScript...");
<div className="w-full justify-between flex items-center gap-2 my-2"> const { error, data: transpiledCode } = await tryCatch(
<strong className="flex items-center gap-1"> (async () => transpileTypeScript(value))()
<Link href="/servers/embedded/sl-modification-frame/files"> );
<ArrowLeft size={20} /> if (error) {
</Link> toast.error("Failed to transpile TypeScript! Error: " + error.message);
{filename}.ts return;
</strong> }
<span className="flex items-center gap-2"> const startTime = Date.now();
{syntaxErrors !== null && syntaxErrors.length !== 0 && ( if (transpiledCode === null) {
<CustomErrors toast.error("Cannot continue.");
filename={filename} return;
value={value} }
monacoRef={monacoRef} console.log("[MHSF Filters] Transpiled TypeScript:", transpiledCode ?? "");
/> toast.info("Generating function...");
)} const functionBody = transpiledCode.match(
<Tooltip> /function\s+filter\s*\([^)]*\)\s*\{([\s\S]*)\}/
<TooltipTrigger> )?.[1];
<CustomLint const { error: filterErr, data: filterFunc } = await tryCatch(
successfullyLinted={successfullyLinted} (async () => new Function("data", functionBody as string))()
setSuccessfullyLinted={setSuccessfullyLinted} );
syntaxErrors={syntaxErrors} if (filterErr) {
value={value} toast.error(`Failed to generate function! Error: ${filterErr.message}`);
/> return;
</TooltipTrigger> }
<TooltipContent> if (typeof filterFunc === "function") {
{syntaxErrors !== null && syntaxErrors.length !== 0 toast.success("Linted in " + (Date.now() - startTime) + "ms");
? `You must have no type errors in the editor to lint, you have ${syntaxErrors.length} error(s).` setSuccessfullyLinted(true);
: "Check for possible runtime errors."} } else {
</TooltipContent> toast.error("Code doesn't have a 'filter' function. Cannot be tested.");
</Tooltip> toast.error(typeof filterFunc);
<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"],
});
// Create a virtual TS file for the types const debouncedSave = debounce(async () => {
const libUri = "file:///node_modules/@types/mhsf/index.d.ts"; 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);
// Add typedefs as a library useEffect(() => {
monaco.languages.typescript.typescriptDefaults.addExtraLib( setSuccessfullyLinted(false);
typeDefs, validateCode(value);
libUri, debouncedSave();
); }, [value]);
// Create a model for the libUri file return (
if (!monaco.editor.getModel(monaco.Uri.parse(libUri))) { <main className="max-w-[800px] p-4">
monaco.editor.createModel( <div className="w-full justify-between flex items-center gap-2 my-2">
typeDefs, <strong className="flex items-center gap-1">
"typescript", <Link href="/servers/embedded/sl-modification-frame/files">
monaco.Uri.parse(libUri), <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);
// Make sure the current file is using the correct language const newTab = window.open(`/servers?tm=${encodeURIComponent(t)}`)
const currentModel = editor.getModel(); const interval = setInterval(() => {
if (currentModel) { newTab?.dispatchEvent(new Event("test-mode.enable"))
monaco.editor.setModelLanguage(currentModel, "typescript"); }, 500)
} toast.info("Waiting for server tab to pick up thread...")
const currentUri = monaco.Uri.parse(`file:///${filename}.ts`); newTab?.addEventListener("test-mode.enabled", () => {
if (!monaco.editor.getModel(currentUri)) { clearInterval(interval);
monaco.editor.createModel(fileContents, "typescript", currentUri); toast.success("Connected to new tab; continue.")
editor.setModel(monaco.editor.getModel(currentUri)); })
} }}
}} >
options={{ Test
minimap: { enabled: false }, </Button>
scrollBeyondLastLine: false, </TooltipTrigger>
fontSize: 14, <TooltipContent>
lineNumbers: "on", {successfullyLinted
roundedSelection: false, ? "Open a full server-list instance with your filter activated in test mode."
scrollbar: { : "You must lint before testing."}
vertical: "visible", </TooltipContent>
horizontal: "visible", </Tooltip>
}, </span>
quickSuggestions: true, </div>
suggestOnTriggerCharacters: true, <div>
acceptSuggestionOnEnter: "on", <Editor
tabCompletion: "on", className={cn("h-[calc(100vh-100px)]")}
wordBasedSuggestions: "currentDocument", defaultLanguage="typescript"
cursorSmoothCaretAnimation: "on", value={value}
parameterHints: { theme={resolvedTheme === "dark" ? "vs-dark" : "vs"}
enabled: true, onChange={(newValue) => {
}, setValue(newValue || "");
hover: { }}
enabled: true, onMount={(editor, monaco) => {
delay: 300, monacoRef.current = monaco;
sticky: true, // Ensure TypeScript is properly configured
}, monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
}} target: monaco.languages.typescript.ScriptTarget.Latest,
/> allowNonTsExtensions: true,
</div> moduleResolution:
</main> 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>
);
} }
export async function findSupportedOperations( function guidGenerator() {
fileValue: string, const S4 = () => {
): Promise<{ filter: boolean; sort: boolean }> { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
const returnValue = { filter: true, sort: true }; };
const transpiledValue = transpileTypeScript(fileValue); return (
const functionBody = transpiledValue S4() +
?.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon S4() +
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon "-" +
const { error: filterErr, data: filterFunc } = await tryCatch( S4() +
(async () => "-" +
new Function( S4() +
"server", "-" +
`${functionBody} S4() +
return filter(server)`, "-" +
))(), S4() +
); S4() +
const { error: sortErr, data: sortFunc } = await tryCatch( S4()
(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,126 +33,97 @@
import { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog"; import { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Material } from "@/components/ui/material"; import { Material } from "@/components/ui/material";
import { Placeholder } from "@/components/ui/placeholder"; import { Placeholder } from "@/components/ui/placeholder";
import { Link } from "@/components/util/link"; import { Link } from "@/components/util/link";
import { useUser } from "@clerk/nextjs"; import { useUser } from "@clerk/nextjs";
import { import {
ArrowLeft, ArrowLeft,
Braces, Braces,
EllipsisVertical, EllipsisVertical,
FileCode, FileCode,
Filter, Pencil,
Pencil, Trash,
SortAsc,
Trash,
} from "lucide-react"; } from "lucide-react";
import { use, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { findSupportedOperations } from "../file/[filename]/page";
export default function ServerListModificationFrame() { export default function ServerListModificationFrame() {
const { user } = useUser(); const { user } = useUser();
const files = const files =
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []; (user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [];
const operations = usePlatforms(files); return (
<main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full flex items-center gap-2">
return ( <Link href="/servers/embedded/sl-modification-frame">
<main className="max-w-[800px] p-4"> <ArrowLeft size={16} />
<h1 className="text-xl font-bold w-full flex items-center gap-2"> </Link>
<Link href="/servers/embedded/sl-modification-frame"> Files
<ArrowLeft size={16} /> </h1>
</Link> <Material className="grid gap-1 mt-4">
Files {files.length === 0 && (
</h1> <Placeholder
<Material className="grid gap-1 mt-4"> icon={<Braces />}
{files.length === 0 && ( title="We couldn't find any files"
<Placeholder description="Try creating a filter!"
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}`}
{files.map((c, i) => ( 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"
<Link key={c.name}
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" <span className="flex items-center gap-1">
key={c.name} <FileCode size={16} />
> {c.name}.ts
<span className="flex items-center gap-1"> </span>
<FileCode size={16} /> <span>
{operations[i].filter && <Filter size={16} />} <DropdownMenu>
{operations[i].sort && <SortAsc size={16} />} <DropdownMenuTrigger>
{c.name}.ts <Button
</span> variant="tertiary"
<span> className="flex items-center justify-center hover:bg-slate-200 dark:hover:bg-zinc-700/60"
<DropdownMenu> size="square-sm"
<DropdownMenuTrigger> >
<DropdownMenu></DropdownMenu> <EllipsisVertical
<Button size={16}
variant="tertiary" className="text-muted-foreground"
className="flex items-center justify-center hover:bg-slate-200 dark:hover:bg-zinc-700/60" />
size="square-sm" </Button>
> </DropdownMenuTrigger>
<EllipsisVertical <DropdownMenuContent>
size={16} <DropdownMenuItem
className="text-muted-foreground" className="flex items-center gap-2"
/> onClick={async (e) => {
</Button> e.stopPropagation();
</DropdownMenuTrigger> const startTime = Date.now();
<DropdownMenuContent> files.splice(i, 1);
<DropdownMenuItem await user?.update({
className="flex items-center gap-2" unsafeMetadata: {
onClick={async (e) => { customFiles: files,
e.stopPropagation(); },
const startTime = Date.now(); });
files.splice(i, 1); toast.success(
await user?.update({ "Deleted file in " + (Date.now() - startTime) + "ms"
unsafeMetadata: { );
...user.unsafeMetadata, }}
customFiles: files, >
}, <Trash size={16} /> Delete
}); </DropdownMenuItem>
toast.success( <DropdownMenuItem className="flex items-center gap-2">
"Deleted file in " + (Date.now() - startTime) + "ms", <Pencil size={16} /> Rename
); </DropdownMenuItem>
}} </DropdownMenuContent>
> </DropdownMenu>
<Trash size={16} /> Delete </span>
</DropdownMenuItem> </Link>
<DropdownMenuItem className="flex items-center gap-2"> ))}
<Pencil size={16} /> Rename </Material>
</DropdownMenuItem> </main>
</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,100 +34,68 @@ import { Material } from "@/components/ui/material";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Link } from "@/components/util/link"; import { Link } from "@/components/util/link";
import { serverModDB } from "@/config/sl-mod-db"; import { serverModDB } from "@/config/sl-mod-db";
import { ArrowRight, Binary } from "lucide-react"; import { ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useRouter } from "@/lib/useRouter"; import { useRouter } from "@/lib/useRouter";
import { SignedIn, useUser } from "@clerk/nextjs";
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
export default function ServerListModificationFrame() { export default function ServerListModificationFrame() {
const router = useRouter(); const router = useRouter();
const { user } = useUser();
return ( return (
<main className="max-w-[800px] p-4"> <main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full">Filters & Sorting</h1> <h1 className="text-xl font-bold w-full">Filters & Sorting</h1>
<div className="flex items-center gap-2 my-2"> <div className="flex items-center gap-2 my-2">
<Button size="sm">Active modifications</Button> <Button size="sm">Active modifications</Button>
<Link href="/servers/embedded/sl-modification-frame/files"> <Link href="/servers/embedded/sl-modification-frame/files">
<Button size="sm">Custom files</Button> <Button size="sm">Custom files</Button>
</Link> </Link>
<Button size="sm">Settings</Button> <Button size="sm">Settings</Button>
</div> </div>
<span className="text-wrap pt-2"> <span className="text-wrap pt-2">
Pick out different filters & sorting systems to customize your server Pick out different filters & sorting systems to customize your server
viewing experience. We frequently add new filters in accordance to new viewing experience. We frequently add new filters in accordance to new
features, as well. features, as well.
</span> </span>
<Material className="mt-10 p-4"> <Material className="mt-10 p-4">
{serverModDB.map((c) => ( {serverModDB.map((c) => (
<div key={c.displayTitle} className="my-4"> <span key={c.displayTitle}>
<h2 className="text-lg font-bold pb-3 flex justify-between"> <h2 className="text-lg font-bold pb-3 flex justify-between">
{c.displayTitle} {c.displayTitle}
<Link <Link
href={`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}`} href={`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}`}
className="flex gap-2 text-sm font-normal items-center" className="flex gap-2 text-sm font-normal items-center"
> >
<ArrowRight size={16} /> <ArrowRight size={16} />
View more View more
</Link> </Link>
</h2> </h2>
<div className="grid grid-cols-6 gap-2"> <div className="grid grid-cols-6 gap-2">
{c.entries.map((m) => ( {c.entries.map((m) => (
<Material <Material
elevation="high" elevation="high"
className="p-2 hover:drop-shadow-card-hover cursor-pointer" className="p-2 hover:drop-shadow-card-hover cursor-pointer"
key={m.name} key={m.name}
onClick={() => onClick={() =>
router.push( router.push(
`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}/modification/${btoa(m.name)}`, `/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}/modification/${btoa(m.name)}`
) )
} }
> >
<div <div
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center" className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
style={{ backgroundColor: m.color }} style={{ backgroundColor: m.color }}
> >
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" /> <m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
</div> </div>
<span className="text-sm text-center w-full flex items-center justify-center"> <span className="text-sm text-center w-full flex items-center justify-center">
{m.name} {m.name}
</span> </span>
</Material> </Material>
))} ))}
<SignedIn> </div>
{c.__custom && </span>
( ))}
(user?.unsafeMetadata </Material>
.activatedModifications as ClerkCustomActivatedModification[]) ?? </main>
[] );
).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 { .loading-shimmer {
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
animation-duration: 2s; animation-duration: 2.5s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-name: loading-shimmer; animation-name: loading-shimmer;
background: var(--color-muted-foreground) background: var(--color-muted-foreground)

@ -1,15 +1,11 @@
import { BrandingGenericIcon, Discord } from "../icons/branding-icons"; import { BrandingGenericIcon } from "../icons/branding-icons";
import { Link } from "../../util/link"; import { Link } from "../../util/link";
import { FooterStatus } from "./status"; import { FooterStatus } from "./status";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import Github from "@/components/ui/github";
import Image from "next/image"
export function Footer() { export function Footer() {
return ( return (
<footer className="w-full border-t p-[20px] mt-15"> <footer className="w-full border-t p-[20px] mt-15">
<div className="flex justify-between items-start"> <div className="flex justify-between items-center">
<span className="flex items-center gap-4 text-muted-foreground"> <span className="flex items-center gap-4 text-muted-foreground">
<Link href="Special:Root"> <Link href="Special:Root">
<BrandingGenericIcon className="max-w-[32px] max-h-[32px]" /> <BrandingGenericIcon className="max-w-[32px] max-h-[32px]" />
@ -41,46 +37,7 @@ export function Footer() {
</li> </li>
</ul> </ul>
</span> </span>
<div className="block"> <FooterStatus />
<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> </div>
<span className="block mt-4"> <span className="block mt-4">
<small className="text-muted-foreground text-[0.75rem]"> <small className="text-muted-foreground text-[0.75rem]">

@ -1,153 +0,0 @@
/*
* 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;
}

@ -1,91 +0,0 @@
/*
* 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>
);
}

@ -1,150 +0,0 @@
/*
* 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>
</>
);
}

@ -1,182 +0,0 @@
/*
* 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>
</>
);
}

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

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

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

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

@ -51,7 +51,8 @@ type ModDBCategory = {
export const serverModDB: ModDBCategory[] = [ export const serverModDB: ModDBCategory[] = [
{ {
displayTitle: "Create Custom Files", displayTitle: "Custom Files",
__custom: true,
description: description:
`Create custom TypeScript-based filter or sorting systems, completely from the comfort of your own browser. `Create custom TypeScript-based filter or sorting systems, completely from the comfort of your own browser.
Types used are *builtin* and you can see live type definitions and IntelliSense in the editor.`, Types used are *builtin* and you can see live type definitions and IntelliSense in the editor.`,
@ -72,11 +73,4 @@ 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,10 +112,11 @@ export const allTags: Array<{
</> </>
), ),
condition: async (c) => condition: async (c) =>
(c.online === undefined ? c.server?.playerCount: c.online.playerData.playerCount) === (c.online === undefined ? c.server?.playerCount : c.online.playerData) ===
0, 0,
htmlDocs: "Nobody is online this server.", htmlDocs: "Nobody is online this server.",
tooltipDesc: "Nobody is online this server.", tooltipDesc: "Nobody is online this server.",
role: "gray-subtle", role: "gray-subtle",
docsName: "Nobody Online", docsName: "Nobody Online",
__filter: true, __filter: true,

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

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

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

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

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