Compare commits

...

3 Commits

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

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

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

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

@ -28,65 +28,70 @@
* OTHER DEALINGS IN THE SOFTWARE. * 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 { ArrowLeft } from "lucide-react"; import { SignedIn, useUser } from "@clerk/nextjs";
import { ArrowLeft, Binary } from "lucide-react";
import { use } from "react"; import { use } from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
export default function ServerListCategoryFrame({ export default async function ServerListCategoryFrame({
params, params,
}: { }: {
params: Promise<{ category: string }>; params: Promise<{ category: string }>;
}) { }) {
const { category } = use(params); const { category } = await params;
const categoryObj = serverModDB.find( const categoryObj = serverModDB.find(
(c) => c.displayTitle === atob(category) (c) => c.displayTitle === atob(decodeURIComponent(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) => (
<Material <Link
className="p-2 hover:drop-shadow-card-hover cursor-pointer" key={m.name}
elevation="high" href={`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`}
onClick={() => >
router.push( <Material
`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}` className="p-2 hover:drop-shadow-card-hover cursor-pointer"
) elevation="high"
} >
key={m.name} <div
> className={cn(
<div "w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center",
className={cn( )}
"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" />
> </div>
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" /> <span className="text-sm text-center w-full flex items-center justify-center">
</div> {m.name}
<span className="text-sm text-center w-full flex items-center justify-center"> </span>
{m.name} </Material>
</span> </Link>
</Material> ))}
))} <SignedIn>
</Material> {categoryObj?.__custom && (
</main> <ModificationCustomModificationRow category={category} />
); )}
</SignedIn>
</Material>
</main>
);
} }

@ -4,37 +4,29 @@ 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 { AlertOctagon, ArrowLeft, ExternalLink } from "lucide-react"; import { ArrowLeft, FileQuestion } 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, Uri } from "monaco-editor"; import type { languages } 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)}
@ -77,378 +69,269 @@ 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 <>Bruh.</>; return (
} <div className="w-full h-full flex justify-center items-center absolute top-[0%]">
<Link href="/servers/embedded/sl-modification-frame">
<ArrowLeft className="absolute left-[10px] top-[10px]" />
</Link>
<Placeholder
title="We couldn't find the file you were looking for."
icon={<FileQuestion />}
/>
</div>
);
}
const validateCode = (code: string) => { const fileContents = ((user?.unsafeMetadata
if (!monacoRef.current) return; .customFiles as Array<ClerkCustomModification>) ?? [])[file].contents;
const [value, setValue] = useState(fileContents);
const clipboard = useClipboard();
monacoRef.current.languages.typescript const saveFile = async () => {
.getTypeScriptWorker() const metadata =
.then((worker) => { (user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ??
worker( [];
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri const index = (
).then((client) => { (user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
client ).findIndex((c) => c.name === filename);
.getSemanticDiagnostics(
(
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
).toString()
)
.then((diags) => {
setSyntaxErrors(diags);
});
});
});
};
const fileContents = ((user?.unsafeMetadata metadata[index].contents = value;
.customFiles as Array<ClerkCustomModification>) ?? [])[file].contents;
const [value, setValue] = useState(fileContents);
const clipboard = useClipboard();
validateCode(value);
const saveFile = async () => { await user?.update({
const metadata = unsafeMetadata: {
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? ...user.unsafeMetadata,
[]; customFiles: metadata,
const index = ( },
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [] });
).findIndex((c) => c.name === filename); };
metadata[index].contents = value; const debouncedSave = debounce(async () => {
const { error } = await tryCatch(saveFile());
if (error)
toast.error(
"Whoa! We encountered an error while auto-saving. Please copy your code locally to ensure you'll keep your code changes.",
);
}, 300);
await user?.update({ // biome-ignore lint: L
unsafeMetadata: { useEffect(() => {
customFiles: metadata, setSuccessfullyLinted(false);
}, debouncedSave();
}); }, [value]);
};
const lintFile = async () => { return (
toast.info("Transpiling TypeScript..."); <main className="max-w-[800px] p-4">
const { error, data: transpiledCode } = await tryCatch( <div className="w-full justify-between flex items-center gap-2 my-2">
(async () => transpileTypeScript(value))() <strong className="flex items-center gap-1">
); <Link href="/servers/embedded/sl-modification-frame/files">
if (error) { <ArrowLeft size={20} />
toast.error("Failed to transpile TypeScript! Error: " + error.message); </Link>
return; {filename}.ts
} </strong>
const startTime = Date.now(); <span className="flex items-center gap-2">
if (transpiledCode === null) { {syntaxErrors !== null && syntaxErrors.length !== 0 && (
toast.error("Cannot continue."); <CustomErrors
return; filename={filename}
} value={value}
console.log("[MHSF Filters] Transpiled TypeScript:", transpiledCode ?? ""); monacoRef={monacoRef}
toast.info("Generating function..."); />
const functionBody = transpiledCode.match( )}
/function\s+filter\s*\([^)]*\)\s*\{([\s\S]*)\}/ <Tooltip>
)?.[1]; <TooltipTrigger>
const { error: filterErr, data: filterFunc } = await tryCatch( <CustomLint
(async () => new Function("data", functionBody as string))() successfullyLinted={successfullyLinted}
); setSuccessfullyLinted={setSuccessfullyLinted}
if (filterErr) { syntaxErrors={syntaxErrors}
toast.error(`Failed to generate function! Error: ${filterErr.message}`); value={value}
return; />
} </TooltipTrigger>
if (typeof filterFunc === "function") { <TooltipContent>
toast.success("Linted in " + (Date.now() - startTime) + "ms"); {syntaxErrors !== null && syntaxErrors.length !== 0
setSuccessfullyLinted(true); ? `You must have no type errors in the editor to lint, you have ${syntaxErrors.length} error(s).`
} else { : "Check for possible runtime errors."}
toast.error("Code doesn't have a 'filter' function. Cannot be tested."); </TooltipContent>
toast.error(typeof filterFunc); </Tooltip>
} <Tooltip>
}; <TooltipTrigger>
<CustomTest
value={value}
successfullyLinted={successfullyLinted}
/>
</TooltipTrigger>
<TooltipContent>
{successfullyLinted
? "Open a full server-list instance with your filter activated in test mode."
: "You must lint before testing."}
</TooltipContent>
</Tooltip>
</span>
</div>
<div>
<Editor
className={cn("h-[calc(100vh-100px)]")}
defaultLanguage="typescript"
value={value}
theme={resolvedTheme === "dark" ? "vs-dark" : "vs"}
onChange={(newValue) => {
setValue(newValue || "");
}}
onMount={(editor, monaco) => {
monacoRef.current = monaco;
// Ensure TypeScript is properly configured
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
noEmit: true,
esModuleInterop: true,
jsx: monaco.languages.typescript.JsxEmit.React,
reactNamespace: "React",
allowJs: true,
typeRoots: ["node_modules/@types"],
});
const debouncedSave = debounce(async () => { // Create a virtual TS file for the types
const { error } = await tryCatch(saveFile()); const libUri = "file:///node_modules/@types/mhsf/index.d.ts";
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);
useEffect(() => { // Add typedefs as a library
setSuccessfullyLinted(false); monaco.languages.typescript.typescriptDefaults.addExtraLib(
validateCode(value); typeDefs,
debouncedSave(); libUri,
}, [value]); );
return ( // Create a model for the libUri file
<main className="max-w-[800px] p-4"> if (!monaco.editor.getModel(monaco.Uri.parse(libUri))) {
<div className="w-full justify-between flex items-center gap-2 my-2"> monaco.editor.createModel(
<strong className="flex items-center gap-1"> typeDefs,
<Link href="/servers/embedded/sl-modification-frame/files"> "typescript",
<ArrowLeft size={20} /> monaco.Uri.parse(libUri),
</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);
const newTab = window.open(`/servers?tm=${encodeURIComponent(t)}`) // Make sure the current file is using the correct language
const interval = setInterval(() => { const currentModel = editor.getModel();
newTab?.dispatchEvent(new Event("test-mode.enable")) if (currentModel) {
}, 500) monaco.editor.setModelLanguage(currentModel, "typescript");
toast.info("Waiting for server tab to pick up thread...") }
newTab?.addEventListener("test-mode.enabled", () => { const currentUri = monaco.Uri.parse(`file:///${filename}.ts`);
clearInterval(interval); if (!monaco.editor.getModel(currentUri)) {
toast.success("Connected to new tab; continue.") monaco.editor.createModel(fileContents, "typescript", currentUri);
}) editor.setModel(monaco.editor.getModel(currentUri));
}} }
> }}
Test options={{
</Button> minimap: { enabled: false },
</TooltipTrigger> scrollBeyondLastLine: false,
<TooltipContent> fontSize: 14,
{successfullyLinted lineNumbers: "on",
? "Open a full server-list instance with your filter activated in test mode." roundedSelection: false,
: "You must lint before testing."} scrollbar: {
</TooltipContent> vertical: "visible",
</Tooltip> horizontal: "visible",
</span> },
</div> quickSuggestions: true,
<div> suggestOnTriggerCharacters: true,
<Editor acceptSuggestionOnEnter: "on",
className={cn("h-[calc(100vh-100px)]")} tabCompletion: "on",
defaultLanguage="typescript" wordBasedSuggestions: "currentDocument",
value={value} cursorSmoothCaretAnimation: "on",
theme={resolvedTheme === "dark" ? "vs-dark" : "vs"} parameterHints: {
onChange={(newValue) => { enabled: true,
setValue(newValue || ""); },
}} hover: {
onMount={(editor, monaco) => { enabled: true,
monacoRef.current = monaco; delay: 300,
// Ensure TypeScript is properly configured sticky: true,
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ },
target: monaco.languages.typescript.ScriptTarget.Latest, }}
allowNonTsExtensions: true, />
moduleResolution: </div>
monaco.languages.typescript.ModuleResolutionKind.NodeJs, </main>
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>
);
} }
function guidGenerator() { export async function findSupportedOperations(
const S4 = () => { fileValue: string,
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); ): Promise<{ filter: boolean; sort: boolean }> {
}; const returnValue = { filter: true, sort: true };
return ( const transpiledValue = transpileTypeScript(fileValue);
S4() + const functionBody = transpiledValue
S4() + ?.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
"-" + .replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
S4() + const { error: filterErr, data: filterFunc } = await tryCatch(
"-" + (async () =>
S4() + new Function(
"-" + "server",
S4() + `${functionBody}
"-" + return filter(server)`,
S4() + ))(),
S4() + );
S4() const { error: sortErr, data: sortFunc } = await tryCatch(
); (async () =>
new Function(
"serverA",
"serverB",
`${functionBody}
return sort(serverA, serverB)`,
))(),
);
if (filterErr) returnValue.filter = false;
if (sortErr) returnValue.sort = false;
try {
filterFunc?.({});
} catch (e) {
if (String(e).startsWith("ReferenceError: filter is not defined")) {
returnValue.filter = false;
}
}
try {
sortFunc?.({}, {});
} catch (e) {
if (String(e).startsWith("ReferenceError: sort is not defined")) {
returnValue.sort = false;
}
}
return returnValue;
} }

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

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

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

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

@ -0,0 +1,153 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2025 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerTitle,
} from "@/components/ui/drawer";
import { Button } from "@/components/ui/button";
import { Alert } from "@/components/ui/alert";
import { compressToEncodedURIComponent } from "lz-string";
import { AlertOctagon, ExternalLink } from "lucide-react";
import type { languages, Uri } from "monaco-editor";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Link } from "@/components/util/link";
import { type RefObject, useEffect, useState } from "react";
import type { MonacoRefType } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
export type SyntaxErrorInterface = languages.typescript.Diagnostic[] | null;
export function CustomErrors({
value,
monacoRef,
filename
}: {
value: string;
monacoRef: RefObject<MonacoRefType | null>;
filename: string;
}) {
const [syntaxErrors, setSyntaxErrors] = useState<SyntaxErrorInterface>();
const validateCode = () => {
if (!monacoRef.current) return;
monacoRef.current.languages.typescript
.getTypeScriptWorker()
.then((worker) => {
worker(
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri,
).then((client) => {
client
.getSemanticDiagnostics(
(
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
).toString(),
)
.then((diags) => {
setSyntaxErrors(diags);
});
});
});
};
validateCode();
// biome-ignore lint: L
useEffect(validateCode, [value]);
if (syntaxErrors !== null && syntaxErrors !== undefined)
return (
<Drawer direction="right">
<DrawerTrigger>
<Button
variant="danger-subtle"
size="square-md"
className="flex items-center justify-center"
>
<AlertOctagon />
</Button>
</DrawerTrigger>
<DrawerContent className="p-4 min-w-[400px] overflow-x-hidden max-h-screen overflow-y-auto">
<DrawerTitle>Type Errors</DrawerTitle>
<div className="p-2">
{syntaxErrors.map((c, i) => (
<Alert
variant={
c.category === 1
? "error"
: c.category === 0
? "warning"
: "info"
}
/* biome-ignore lint: No. */
key={i}
className="gap-1 my-2"
>
{c.messageText.toString()}{" "}
<DropdownMenu>
<DropdownMenuTrigger>
<small className="flex items-center gap-1 cursor-pointer">
(TS{typeof c !== "string" && c.code})
<ExternalLink size={16} />
</small>
</DropdownMenuTrigger>
<DropdownMenuContent>
<Link
noExtraIcons
target="_blank"
href={`https://typescript.tv/errors/#ts${c.code}`}
>
<DropdownMenuItem>typescript.tv</DropdownMenuItem>
</Link>
<Link
noExtraIcons
target="_blank"
href={`https://ts-error-translator.vercel.app/?error=${compressToEncodedURIComponent(c.messageText.toString())}`}
>
<DropdownMenuItem>ts-error-translator</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
</Alert>
))}
</div>
</DrawerContent>
</Drawer>
);
return null;
}

@ -0,0 +1,91 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2025 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
import { Button } from "@/components/ui/button";
import { tryCatch } from "@/lib/try-catch";
import { toast } from "sonner";
import type { SyntaxErrorInterface } from "./custom-errors";
export function CustomLint({
successfullyLinted,
setSuccessfullyLinted,
value,
syntaxErrors,
}: {
successfullyLinted: boolean;
setSuccessfullyLinted: (change: boolean) => void;
value: string;
syntaxErrors: SyntaxErrorInterface;
}) {
const lintFile = async () => {
toast.info("Transpiling TypeScript...");
const { error, data: transpiledCode } = await tryCatch(
(async () => transpileTypeScript(value))(),
);
if (error) {
toast.error(`Failed to transpile TypeScript! Error: ${error.message}`);
return;
}
const startTime = Date.now();
if (transpiledCode === null) {
toast.error("Cannot continue.");
return;
}
toast.info("Generating function...");
const functionBody = transpiledCode.match(
/function\s+filter\s*\([^)]*\)\s*\{([\s\S]*)\}/,
)?.[1];
const { error: filterErr, data: filterFunc } = await tryCatch(
(async () => new Function("data", functionBody as string))(),
);
if (filterErr) {
toast.error(`Failed to generate function! Error: ${filterErr.message}`);
return;
}
if (typeof filterFunc === "function") {
toast.success(`Linted in ${Date.now() - startTime}ms`);
setSuccessfullyLinted(true);
} else {
toast.error("Code doesn't have a 'filter' function. Cannot be tested.");
toast.error(typeof filterFunc);
}
};
return (
<Button
onClick={lintFile}
disabled={syntaxErrors === null || syntaxErrors.length !== 0}
variant={successfullyLinted ? "success-subtle" : "secondary"}
>
Lint
</Button>
);
}

@ -0,0 +1,150 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2025 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { toast } from "sonner";
import type { ClerkCustomActivatedModification } from "../modification-file-creation-dialog";
import { useUser } from "@clerk/nextjs";
import { Alert } from "@/components/ui/alert";
import { Material } from "@/components/ui/material";
import {
Setting,
SettingContent,
SettingDescription,
SettingMeta,
SettingTitle,
} from "@/components/feat/settings/setting";
import { Input } from "@/components/ui/input";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { DialogTrigger } from "@/components/ui/dialog";
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
import { useRouter } from "@/lib/useRouter";
export function CustomTestSuccess({
filename,
testMode,
value,
}: { filename: string; testMode: "filter" | "sort" | ""; value: string }) {
const { user } = useUser();
const [friendlyName, setFriendlyName] = useState("");
const router = useRouter();
return (
<>
<p className="text-sm my-2">
You can now activate this custom modification. Please note that the
filter and sort versions of your modifications will be different, and
the one used will be selected based on what type you tested on.
</p>
{(
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
).find(
(c) => c.originalFileName === filename && c.testMode === testMode,
) !== undefined && (
<Alert className="mb-2 gap-2" variant="warning">
This modification was already activated! Hitting activate here will
just overwrite the contents and the new friendly name.
</Alert>
)}
<Material>
<Setting>
<SettingContent>
<SettingMeta>
<SettingTitle>Name</SettingTitle>
<SettingDescription>
Set a friendly name for your modification.
</SettingDescription>
</SettingMeta>
<Input
placeholder="My cool mod"
value={friendlyName}
onChange={(c) => setFriendlyName(c.target.value)}
/>
</SettingContent>
</Setting>
</Material>
<DialogTrigger>
<Button
className="w-full my-2"
disabled={friendlyName === "" || friendlyName.replace(" ", "") === ""}
onClick={async () => {
const array =
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ??
[];
const index = array.findIndex(
(c) => c.originalFileName === filename && c.testMode === testMode,
);
const color = `#${Math.floor(Math.random() * 16777215).toString(16)}`;
const transpiledValue = transpileTypeScript(value);
if (transpiledValue === null)
return toast.error("Error transpiling");
if (index !== -1) {
// Original already exists
array[index] = {
originalFileName: filename,
friendlyName,
transpiledContents: transpiledValue,
active: true,
testMode: testMode as "filter" | "sort",
color,
};
} else {
array.push({
originalFileName: filename,
friendlyName,
transpiledContents: transpiledValue,
active: true,
testMode: testMode as "filter" | "sort",
color,
});
}
await user?.update({
unsafeMetadata: {
...user.unsafeMetadata,
activatedModifications: array,
},
});
toast.success("Activated!");
router.push("/servers/embedded/sl-modification-frame");
}}
>
Activate
</Button>
</DialogTrigger>
</>
);
}

@ -0,0 +1,182 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2025 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
import { Setting, SettingContent, SettingDescription, SettingMeta, SettingTitle } from "@/components/feat/settings/setting";
import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent } from "@/components/ui/drawer";
import { Material } from "@/components/ui/material";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { tryCatch } from "@/lib/try-catch";
import { Check } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { CustomTestSuccess } from "./custom-test-success";
export function CustomTest({value, successfullyLinted}: {value: string, successfullyLinted: boolean}) {
const [open, setOpen] = useState(false);
const [filterEnabled, setFilterEnabled] = useState(true);
const [sortEnabled, setSortEnabled] = useState(true);
const [success, setSuccess] = useState(false);
const [fileName, setFileName] = useState("");
const [testMode, setTestMode] = useState<"filter" | "sort" | "">("");
// biome-ignore lint: values needed (but not shown by linter)
useEffect(() => setSuccess(false), [value]);
// biome-ignore lint: values not needed
useEffect(() => {
setFilterEnabled(true);
setSortEnabled(true);
setTestMode("");
(async () => {
const transpiledValue = transpileTypeScript(value);
const functionBody = transpiledValue
?.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
const { error: filterErr, data: filterFunc } = await tryCatch(
(async () =>
new Function(
"server",
`${functionBody}
return filter(server)`,
))(),
);
const { error: sortErr, data: sortFunc } = await tryCatch(
(async () =>
new Function(
"serverA",
"serverB",
`${functionBody}
return sort(serverA, serverB)`,
))(),
);
if (filterErr) setFilterEnabled(false);
if (sortErr) setSortEnabled(false);
try {
filterFunc?.({});
} catch (e) {
if (String(e).startsWith("ReferenceError: filter is not defined")) {
setFilterEnabled(false);
}
}
try {
sortFunc?.({}, {});
} catch (e) {
if (String(e).startsWith("ReferenceError: sort is not defined")) {
setSortEnabled(false);
}
}
})();
}, [open, value]);
return (
<>
<Button disabled={!successfullyLinted} onClick={() => setOpen(true)}>
Test
</Button>
<Drawer direction="right" open={open} onOpenChange={setOpen}>
<DrawerContent className="p-4 min-w-[400px] overflow-x-hidden max-h-screen overflow-y-auto">
<p className="text-sm mb-2">
You can run an interactive server-list environment with actual
online servers to test your modifications.
</p>
<Material>
<Setting>
<SettingContent>
<SettingMeta>
<SettingTitle>Function to test</SettingTitle>
<SettingDescription>
You can pick to either test a sorting system or a filter.
</SettingDescription>
</SettingMeta>
<Select
value={testMode}
onValueChange={setTestMode as (change: string) => void}
disabled={success}
>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="filter" disabled={!filterEnabled}>
<code>filter</code>
</SelectItem>
<SelectItem value="sort" disabled={!sortEnabled}>
<code>sort</code>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</SettingContent>
</Setting>
</Material>
<Button
className="w-full mt-2 flex items-center gap-2"
disabled={testMode === "" || success}
variant={success ? "success-subtle" : "default"}
onClick={() => {
const t = btoa(value);
const newTab = window.open(
`/servers?tm=${encodeURIComponent(t)}`,
);
const interval = setInterval(() => {
newTab?.dispatchEvent(
new Event(`test-mode.enable.${testMode}`),
);
}, 500);
toast.info("Waiting for server tab to pick up thread...");
newTab?.addEventListener("test-mode.enabled", () => {
clearInterval(interval);
toast.success("Connected to new tab; continue.");
newTab?.addEventListener("test-mode.success", () => {
toast.success("Resolved success from thread!");
setSuccess(true);
});
});
}}
>
{success && <Check size={16} />}Test
</Button>
{success && (
<CustomTestSuccess filename={fileName} testMode={testMode} value={value} />
)}
</DrawerContent>
</Drawer>
</>
);
}

@ -0,0 +1,66 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2025 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { useUser } from "@clerk/nextjs";
import { ClerkCustomActivatedModification } from "./modification-file-creation-dialog";
import { Link } from "@/components/util/link";
import { Material } from "@/components/ui/material";
import { Binary } from "lucide-react";
export function ModificationCustomModificationRow({category}: {category: string}) {
const { user } = useUser();
return (
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
).map((m) => (
<Link
href={`/servers/embedded/sl-modification-frame/category/${category}/modification/_custom/${btoa(m.friendlyName)}`}
key={m.friendlyName}
>
<Material
elevation="high"
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
>
<div
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
style={{ backgroundColor: m.color }}
>
<Binary className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
</div>
<span className="text-sm text-center w-full flex items-center justify-center">
{m.friendlyName}
</span>
</Material>
</Link>
));
}

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

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

@ -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,166 +22,178 @@ 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">
<SettingContent> {debugOptions.mhsfData !== undefined && <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.playerData.historically.length + debugOptions.mhsfData.achievements ?? {
debugOptions.mhsfData.favoriteData.favoriteHistoricalData historically: { length: 0 },
.length }
)} ).historically.length +
</SettingContent> (
</Setting> debugOptions.mhsfData.playerData ?? {
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,156 +15,176 @@ 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 <>
data={ {(statisticType === "playerCount"
statisticType === "playerCount" ? mhsfData.server?.playerData.historically
? mhsfData.server?.playerData.historically : mhsfData.server?.favoriteData.favoriteHistoricalData
: mhsfData.server?.favoriteData.favoriteHistoricalData )?.length !== 0 ? (
} <StatisticsChart
mainDataPoint={statisticType} data={
/> statisticType === "playerCount"
)} ? mhsfData.server?.playerData.historically
</div> : mhsfData.server?.favoriteData.favoriteHistoricalData
</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,14 +38,12 @@ import { MultisessionAppSupport } from "@clerk/nextjs/internal";
export const ClerkProvider = ({ children }: { children: React.ReactNode }) => { export const ClerkProvider = ({ children }: { children: React.ReactNode }) => {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
if (resolvedTheme === "dark") { if (resolvedTheme === "dark") {
console.log(resolvedTheme);
return ( return (
<ImportedClerkProvider appearance={{ baseTheme: dark }}> <ImportedClerkProvider appearance={{ baseTheme: dark }}>
<MultisessionAppSupport>{children}</MultisessionAppSupport> <MultisessionAppSupport>{children}</MultisessionAppSupport>
</ImportedClerkProvider> </ImportedClerkProvider>
); );
} }
console.log("a");
return ( return (
<ImportedClerkProvider> <ImportedClerkProvider>

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

@ -112,11 +112,10 @@ export const allTags: Array<{
</> </>
), ),
condition: async (c) => condition: async (c) =>
(c.online === undefined ? c.server?.playerCount : c.online.playerData) === (c.online === undefined ? c.server?.playerCount: c.online.playerData.playerCount) ===
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,

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

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

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

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

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

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

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