feat: custom filters

This commit is contained in:
dvelo 2025-03-21 08:36:05 -05:00
parent bba5504f5d
commit 7385705c8d
35 changed files with 4161 additions and 72 deletions

@ -49,6 +49,9 @@ const nextConfig = {
}, },
] ]
}, },
webpack: (config) => {
return config;
},
}; };
export default withContentlayer(nextConfig); export default withContentlayer(nextConfig);

@ -55,6 +55,7 @@
"mini-svg-data-uri": "^1.4.4", "mini-svg-data-uri": "^1.4.4",
"minimessage-2-html": "1.6.0", "minimessage-2-html": "1.6.0",
"minimessage-js": "^1.1.3", "minimessage-js": "^1.1.3",
"monaco-editor": "^0.52.2",
"mongodb": "^6.8.0", "mongodb": "^6.8.0",
"next": "15.2.0", "next": "15.2.0",
"next-contentlayer": "^0.3.4", "next-contentlayer": "^0.3.4",

@ -0,0 +1,95 @@
/*
* 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 "../globals.css";
import { useSearchParams } from "next/navigation";
import { Placeholder } from "@/components/ui/placeholder";
import { X } from "lucide-react";
import { IsScript } from "@/components/util/is-script";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { NavBar } from "@/components/feat/navbar/navbar";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ThemeProvider } from "@/components/util/theme-provider";
import { FontBoundary } from "@/components/util/font-boundary";
import { ClerkProvider } from "@/components/util/clerk-provider";
import { Toaster } from "sonner";
import { Footer } from "@/components/feat/footer/footer";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { IframeProtector } from "@/components/util/iframe-protector";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const searchParams = useSearchParams();
const search = searchParams?.get("theme") || "light";
return (
<html lang="en">
<noscript>
<main className="flex justify-center items-center text-center min-h-screen h-max">
<Placeholder
icon={<X />}
title="JavaScript is required for MHSF"
description="MHSF cannot grab servers or do other external requests without JavaScript."
>
<Link href="https://www.enable-javascript.com/">
<Button>Here's how</Button>
</Link>
</Placeholder>
</main>
</noscript>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<ClerkProvider>
<IsScript>
<NuqsAdapter>
<FontBoundary className="max-w-[800px]">
<IframeProtector>
<TooltipProvider>
<Toaster richColors position="top-center" />
<div className="overflow-x-hidden">{children}</div>
</TooltipProvider>
</IframeProtector>
</FontBoundary>
</NuqsAdapter>
</IsScript>
</ClerkProvider>
</ThemeProvider>
</html>
);
}

@ -0,0 +1,79 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2025 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { ModificationAction } from "@/components/feat/server-list/modification/modification-action";
import { Button } from "@/components/ui/button";
import { Link } from "@/components/util/link";
import { serverModDB } from "@/config/sl-mod-db";
import { ArrowLeft } from "lucide-react";
import { useQueryState } from "nuqs";
import { use } from "react";
import Markdown from "react-markdown";
export default function ModificationPage({
params,
}: {
params: Promise<{ category: string; mod: string }>;
}) {
const { category, mod } = use(params);
const [backRoute] = useQueryState("b", {
defaultValue: "/servers/embedded/sl-modification-frame",
});
console.log(mod);
const categoryObj = serverModDB.find(
(c) => c.displayTitle === atob(decodeURIComponent(category))
);
let modObj = null;
if (categoryObj !== undefined)
modObj = categoryObj?.entries.find(
(c) => c.name === atob(decodeURIComponent(mod))
);
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?.name}</h1>
<Markdown className="text-wrap pt-2">{modObj?.description}</Markdown>
<ModificationAction value={modObj?.value} />
</span>
</main>
);
}

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

@ -0,0 +1,150 @@
"use client";
import { use } from "react";
import { useUser } from "@clerk/nextjs";
import type { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import { Link } from "@/components/util/link";
import { ArrowLeft } from "lucide-react";
import Editor from "@monaco-editor/react";
import poimandres from "@/theme.json";
const typeDefs = `
export interface Server {
staticInfo: {
_id: string;
serverPlan: string;
serviceStartDate: number;
platform: string;
planMaxPlayers: number;
planRam: number;
alwaysOnline: boolean;
rawPlan: string;
connectedServers: any[];
};
maxPlayers: number;
name: string;
motd: string;
icon: string;
playerData: {
playerCount: number;
timeNoPlayers: number;
};
connectable: boolean;
visibility: boolean;
allCategories: string[];
usingCosmetics: boolean;
author?: string;
authorRank: string;
}
`;
export default function CustomFilePage({
params,
}: {
params: Promise<{ filename: string }>;
}) {
const { filename } = use(params);
const { user } = useUser();
const file = (
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
).find((c) => c.name === filename);
if (!file) {
return <>Bruh.</>;
}
const fileContents = file.contents;
return (
<main className="max-w-[800px] p-4">
<strong className="font-bold w-full">
<Link href="/servers/embedded/sl-modification-frame/files">
<ArrowLeft />
</Link>
{filename}.ts
</strong>
<div className="h-[400px]">
<Editor
height="100%"
defaultLanguage="typescript"
defaultValue={fileContents}
theme="vs-dark"
onMount={(editor, monaco) => {
// Ensure TypeScript is properly configured
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true, // This is important!
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
noEmit: true,
esModuleInterop: true,
jsx: monaco.languages.typescript.JsxEmit.React,
reactNamespace: "React",
allowJs: true,
typeRoots: ["node_modules/@types"],
});
// Create a virtual TS file for the types
const libUri = "file:///node_modules/@types/mhsf/index.d.ts";
// Add typedefs as a library
monaco.languages.typescript.typescriptDefaults.addExtraLib(
typeDefs,
libUri,
);
// 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",
parameterHints: {
enabled: true,
},
hover: {
enabled: true,
delay: 300,
sticky: true,
},
}}
/>
</div>
</main>
);
}

@ -0,0 +1,54 @@
/*
* 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 { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import { Link } from "@/components/util/link";
import { useUser } from "@clerk/nextjs";
import { File, FileCode } from "lucide-react";
export default function ServerListModificationFrame() {
const { user } = useUser();
const files =
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [];
return (
<main className="max-w-[800px] p-4">
<h1 className="text-xl font-bold w-full">Files</h1>
<div className="grid gap-1">
{files.map((c) => (
<Link href={`/servers/embedded/sl-modification-frame/file/${c.name}`} className="w-full py-1 px-2 rounded-xl flex items-center gap-1 hover:bg-slate-100" key={c.name}>
<FileCode size={16}/>{c.name}.ts
</Link>
))}
</div>
</main>
);
}

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

@ -0,0 +1,26 @@
import { Button } from "@/components/ui/button";
import type { Filter } from "@/lib/types/filter";
import type { Sort } from "@/lib/types/sort";
import { ModificationFileCreationDialog } from "./modification-file-creation-dialog";
type Action = Filter | Sort | { customAction: string };
export function ModificationAction({ value }: { value?: Action }) {
return (
<>
{value !== undefined && "customAction" in value ? (
<ModificationFileCreationDialog
type={value.customAction.endsWith("sort") ? "sort" : "filter"}
>
<Button size="sm" className="mt-1">
{value.customAction === "custom-sort"
? "Create Sort"
: "Create Filter"}
</Button>
</ModificationFileCreationDialog>
) : (
<Button size="sm">Apply</Button>
)}
</>
);
}

@ -0,0 +1,47 @@
/*
* 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 { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { ModificationFrame } from "./modification-frame";
export function ModificationButton() {
return (
<Dialog>
<DialogTrigger>
<Button>Filters & Sorting</Button>
</DialogTrigger>
<DialogContent className="p-0 h-[600px] w-[1000px] !max-w-[800px] overflow-x-hidden">
<ModificationFrame />
</DialogContent>
</Dialog>
);
}

@ -0,0 +1,126 @@
/*
* 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 { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { useRouter } from "@/lib/useRouter";
import { useUser } from "@clerk/nextjs";
import { useState, type ReactNode } from "react";
import { toast } from "sonner";
const sortTemplate = `import type { Server } from "mhsf";
export function sort(serverA: Server, serverB: Server): number {
// Your code here
// Use logic like \`Array.sort\` or <V>(a: V, b: V) => number
}`;
const filterTemplate = `import type { Server } from "mhsf";
export function filter(server: Server): boolean {
// Your code here
// Returning true indicates the server will stay, while returning false will remove the server from the queue.
}`;
export type ClerkCustomModification = {
name: string; // Add .ts to the end
active: boolean;
contents: string;
};
export function ModificationFileCreationDialog({
children,
type,
}: {
children?: ReactNode;
type: "filter" | "sort";
}) {
const { user, isSignedIn } = useUser();
const router = useRouter();
const [fileName, setFileName] = useState("");
return (
<Dialog>
<DialogTrigger>{children}</DialogTrigger>
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
Files can be a new filter or sort, made w/ TypeScript.
</DialogDescription>
<div className="flex items-center w-full">
<Input
className="rounded-r-none w-full"
placeholder="you-should-use-this-format-for-typescript-files-please"
onChange={(e) => setFileName(e.target.value)}
value={fileName}
/>
<span className="px-4 text-sm py-2 border border-l-none rounded-r-md">
.ts
</span>
</div>
<br />
<DialogTrigger>
<Button
className="w-full"
onClick={(e) => {
if (!isSignedIn) return toast.error("Please login.");
user?.update({
unsafeMetadata: {
customFiles: [
...((user.unsafeMetadata
.customFiles as Array<ClerkCustomModification>) ?? []),
{
name: fileName,
active: false,
contents:
type === "filter" ? filterTemplate : sortTemplate,
},
] satisfies Array<ClerkCustomModification>,
},
});
toast.success("Created file!")
router.push(`/servers/embedded/sl-modification-frame/file/${fileName}`)
}}
>
Submit
</Button>
</DialogTrigger>
</DialogContent>
</Dialog>
);
}

@ -0,0 +1,47 @@
/*
* 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 { useIframeCommunication } from "@/lib/hooks/use-iframe-communication"
import { useEffectOnce } from "@/lib/useEffectOnce"
import { useEffect, useRef } from "react"
export function ModificationFrame() {
const ref = useRef<HTMLIFrameElement>(null)
const communication = useIframeCommunication(ref);
useEffect(() => {
communication.toIframe.handle("ping", (c) => {
if (c.from === "iframe")
communication.toIframe.send("ping", {from: "top-layer"})
})
}, [ref])
return <iframe ref={ref} src="/servers/embedded/sl-modification-frame" height={800} width={800} title="Server-list Modification Frame" />
}

@ -62,11 +62,11 @@ export default function ServerCard({ server }: { server: OnlineServer }) {
return ( return (
<Material <Material
className="min-h-[250px] max-h-[250px] cursor-pointer outline-0 group hover:drop-shadow-card-hover focus:drop-shadow-card-hover transition-all" className="min-h-[250px] max-h-[250px] cursor-pointer outline-0 group hover:drop-shadow-card-hover focus:drop-shadow-card-hover transition-all"
onClick={() => router.push(`/server/${server.staticInfo._id}`)} onClick={() => router.push(`/server/v2/minehut/${server.staticInfo._id}`)}
tabIndex={0} tabIndex={0}
onKeyDown={(e) => { onKeyDown={(e) => {
// Only send user when they hit "Enter" // Only send user when they hit "Enter"
if (e.key === "Enter") router.push(`/server/${server.staticInfo._id}`); if (e.key === "Enter") router.push(`/server/v2/minehut/${server.staticInfo._id}`);
}} }}
> >
<span className="text-sm hidden group-focus-visible:block text-muted-foreground mb-2"> <span className="text-sm hidden group-focus-visible:block text-muted-foreground mb-2">

@ -37,15 +37,12 @@ import { Statistics } from "./statistics";
import InfiniteScroll from "react-infinite-scroll-component"; import InfiniteScroll from "react-infinite-scroll-component";
import { useInfiniteScrolling } from "@/lib/hooks/use-infinite-scrolling"; import { useInfiniteScrolling } from "@/lib/hooks/use-infinite-scrolling";
import { useMHSFServer } from "@/lib/hooks/use-mhsf-multiple"; import { useMHSFServer } from "@/lib/hooks/use-mhsf-multiple";
import { ModificationButton } from "./modification/modification-button";
export function ServerList() { export function ServerList() {
const { servers, loading, serverCount, playerCount } = useServers(); const { servers, loading, serverCount, playerCount } = useServers();
const { itemsLength, fetchMoreData, hasMoreData, data } = const { itemsLength, fetchMoreData, hasMoreData, data } =
useInfiniteScrolling(servers); useInfiniteScrolling(servers);
const mhsfServers = useMHSFServer(
servers.map((s) => s.staticInfo._id),
true
);
if (loading) if (loading)
return ( return (
@ -68,6 +65,7 @@ export function ServerList() {
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl"> <h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl">
Servers Servers
</h1> </h1>
<ModificationButton />
<InfiniteScroll <InfiniteScroll
dataLength={itemsLength} dataLength={itemsLength}
next={fetchMoreData} next={fetchMoreData}

@ -4,6 +4,11 @@ import { TextArea } from "@/components/ui/text-area";
import { Link } from "@/components/util/link"; import { Link } from "@/components/util/link";
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server"; import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
import { useState } from "react"; import { useState } from "react";
import Image from "next/image";
import { SignedIn, SignedOut, useUser } from "@clerk/nextjs";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { toast } from "sonner";
export function ReportingDialog({ export function ReportingDialog({
server, server,
@ -14,7 +19,9 @@ export function ReportingDialog({
open: boolean; open: boolean;
setOpen: (newState: boolean) => void; setOpen: (newState: boolean) => void;
}) { }) {
const { user, isSignedIn } = useUser();
const [reason, setReason] = useState(""); const [reason, setReason] = useState("");
const [loading, setLoading] = useState(false);
return ( return (
<Drawer direction="left" open={open} onOpenChange={setOpen}> <Drawer direction="left" open={open} onOpenChange={setOpen}>
@ -52,9 +59,50 @@ export function ReportingDialog({
</ul> </ul>
</Alert> </Alert>
<br /> <br />
<TextArea label="Reason for reporting" /> <TextArea
label="Reason for reporting"
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
<br /> <br />
<span></span> <SignedIn>
<span className="flex items-center gap-2 text-sm text-muted-foreground">
<Image
alt="Clerk Image"
src={
user?.imageUrl === undefined
? "https://img.clerk.com/preview.png?size=144&seed=seed&initials=AD&isSquare=true&bgType=marble&bgColor=6c47ff&fgType=silhouette&fgColor=FFFFFF&type=user&w=48&q=75"
: user?.imageUrl
}
width={16}
height={16}
className="rounded-full"
/>
Signed in as @{user?.username}
</span>
</SignedIn>
<SignedOut>
<span className="flex items-center gap-2 text-sm text-muted-foreground">
You must be signed in to perform this action.
</span>
</SignedOut>
<br />
<Button
disabled={!isSignedIn || loading}
className="flex items-center gap-2"
onClick={async () => {
if (reason === "" || reason === " ") {
toast.error("The reason cannot be empty.");
return;
}
setLoading(true);
await server.reportServer(reason);
setLoading(false);
setOpen(false);
}}
>
Report Server {loading && <Spinner />}
</Button>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
); );

@ -40,6 +40,7 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
@ -103,6 +104,9 @@ export function ServerPageButtons({
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuSeparator>
Destructive
</DropdownMenuSeparator>
<DropdownMenuItem <DropdownMenuItem
className="text-red-400 flex items-center gap-2" className="text-red-400 flex items-center gap-2"
onClick={() => { onClick={() => {

@ -66,6 +66,7 @@ const TextArea: React.FC<TextAreaProps> = ({
rows = 4, rows = 4,
id = generateID(), id = generateID(),
className = "", className = "",
onChange,
...restProps ...restProps
}) => { }) => {
const elementRef = useRef<HTMLTextAreaElement | null>(null); const elementRef = useRef<HTMLTextAreaElement | null>(null);
@ -93,7 +94,7 @@ const TextArea: React.FC<TextAreaProps> = ({
disabled={disabled} disabled={disabled}
rows={rows} rows={rows}
value={value} value={value}
onChange={(e) => restProps.onChange?.(e)} onChange={onChange}
ref={elementRef} ref={elementRef}
className={`${sizeClass[size]} ${borderClass} focus:border-slate-800 dark:focus:border-zinc-200 bg-white dark:bg-zinc-950 focus:outline-hidden focus:ring-2 ring-slate-800/50 rounded-xl dark:ring-zinc-200/50 transition-all text-sm w-full disabled:bg-slate-100 disabled:cursor-not-allowed invalid:border-red-500! peer invalid:text-red-500 z-10 ${className}`} className={`${sizeClass[size]} ${borderClass} focus:border-slate-800 dark:focus:border-zinc-200 bg-white dark:bg-zinc-950 focus:outline-hidden focus:ring-2 ring-slate-800/50 rounded-xl dark:ring-zinc-200/50 transition-all text-sm w-full disabled:bg-slate-100 disabled:cursor-not-allowed invalid:border-red-500! peer invalid:text-red-500 z-10 ${className}`}
/> />

@ -44,8 +44,10 @@ const overflowXHiddenPages = ["/home"];
export function FontBoundary({ export function FontBoundary({
children, children,
className
}: { }: {
children?: ReactNode | ReactNode[]; children?: ReactNode | ReactNode[];
className?: string;
}) { }) {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const [fontFamily, setFontFamily] = useState("inter"); const [fontFamily, setFontFamily] = useState("inter");
@ -71,7 +73,7 @@ export function FontBoundary({
default: default:
return "system-ui-font--font-boundary"; return "system-ui-font--font-boundary";
} }
})()} ${pathname !== null && overflowXHiddenPages.includes(pathname) ? "overflow-x-hidden" : ""}`} })()} ${pathname !== null && overflowXHiddenPages.includes(pathname) ? "overflow-x-hidden" : ""} ${className}`}
> >
{children} {children}
</body> </body>

@ -28,41 +28,32 @@
* OTHER DEALINGS IN THE SOFTWARE. * OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { LinearClient, LinearFetch, User } from "@linear/sdk"; "use client";
export async function createReportIssue( import { useIframeCommunication } from "@/lib/hooks/use-iframe-communication";
server: string, import { useEffectOnce } from "@/lib/useEffectOnce";
reportDescription: string, import { type ReactNode, useState } from "react";
userId: string, import { Spinner } from "../ui/spinner";
) {
const linearClient = new LinearClient({
apiKey: process.env.LINEAR,
});
const allTeams = await linearClient.teams(); export function IframeProtector({ children }: { children: ReactNode }) {
// Always grabs the first issue category. const [loading, setLoading] = useState(true);
const team = allTeams.nodes[0]; const iframeCommunication = useIframeCommunication();
// Ensure there *actually* is a team there useEffectOnce(() => {
if (team.id) { // Make sure top layer frames are actually coming from MHSF
await linearClient.createIssue({ iframeCommunication.fromIframe.send("ping", { from: "iframe" });
teamId: team.id, iframeCommunication.fromIframe.handle("ping", (obj) => {
title: `Issue against server \`${server}\``, if (obj.from === "top-layer") setLoading(false);
description: desc(userId, server, reportDescription), });
assigneeId: (await team.members()).nodes[0].id, });
});
} if (loading)
return (
<div className="max-w-[800px]">
<div className="absolute top-[50%] left-[50%]">
<Spinner />
</div>
</div>
);
return children;
} }
const desc = (user: string, server: string, reason: string) => `There was a report against the server, submitted by a user.
Every issue must be [considered with care](https://list.mlnehut.com/docs/legal/external-content-agreement) before evaluating its outcome.
**User**: \`${user}\`
**Server**: \`${server}\`
**For reason**:
${reason}
*This was an automatically added issue by the report bot. Add the canceled status to remove the issue from the active issues, along with the labels Not Controllable & Spam for their respective values.*
`;

@ -0,0 +1,76 @@
/*
* 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 {
ArrowDownUpIcon,
SlidersHorizontal,
type LucideIcon,
} from "lucide-react";
import type { Filter } from "../lib/types/filter";
import type { Sort } from "../lib/types/sort";
type ModDBCategory = {
displayTitle: string;
description: string;
__custom?: boolean;
entries: {
name: string;
icon: LucideIcon;
color: string;
value: Filter | Sort | { customAction: string };
description: string;
}[];
};
export const serverModDB: ModDBCategory[] = [
{
displayTitle: "Custom Files",
__custom: true,
description:
`Create custom TypeScript-based filter or sorting systems, completely from the comfort of your own browser.
Types used are *builtin* and you can see live type definitions and IntelliSense in the editor.`,
entries: [
{
name: "Create Sort",
icon: ArrowDownUpIcon,
value: { customAction: "custom-sort" },
color: "#a3a68b",
description: "Create a new custom sort system using TypeScript, completely from the comfort of your own browser."
},
{
name: "Create Filter",
icon: SlidersHorizontal,
value: { customAction: "custom-filter" },
color: "#a3a68b",
description: "Create a new custom filtering system using TypeScript, completely from the comfort of your own browser."
},
],
},
];

@ -0,0 +1,98 @@
/*
* 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 } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb";
const reportObj = (
serverName: string,
userId: string,
userAvatar: string,
username: string,
reason: string,
serverHasCustomizationData: boolean
) => {
return {
content: "<@&1283912654536314961>",
embeds: [
{
title: `Report on server \`${serverName}\``,
description: `There was a report on server \`${serverName}\` by user \`${userId}\`.`,
color: 16759796,
fields: [
{
name: "Reason",
value: reason,
},
{
name: "Server has customization data?",
value: serverHasCustomizationData ? "Yes" : "No",
},
],
author: {
name: username,
url: `${process.env.CLERK_USER_PREFIX}/${userId}`,
icon_url: userAvatar,
},
},
],
attachments: [],
};
};
export async function sendDiscordReport(
serverName: string,
userId: string,
reason: string
) {
const client = await clerkClient();
const user = await client.users.getUser(userId);
const mongo = new MongoClient(process.env.MONGO_DB as string);
await mongo.connect();
const collection = mongo.db("mhsf").collection("customization");
const server = await collection.findOne({ server: serverName });
await fetch(process.env.DISCORD_WEBHOOK as string, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(
reportObj(
serverName,
userId,
user.imageUrl,
user.username ?? "",
reason,
server !== null
)
),
});
}

@ -0,0 +1,64 @@
/*
* 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 type { RefObject } from "react";
export function useIframeCommunication(bottomIframe?: RefObject<HTMLIFrameElement | null>) {
return {
toIframe: {
send: (key: string, object: any) => {
if (!bottomIframe) {
throw new Error("No hook Iframe")
}
bottomIframe.current?.contentWindow?.postMessage({__key: key, ...object}, '*');
},
handle: (key: string, callback: (object: any) => void) => {
window.addEventListener('message', (e) => {
if (e.data.__key === key) {
callback(e.data)
}
})
}
},
fromIframe: {
send: (key: string, object: any) => {
window.top?.postMessage({__key: key, ...object}, '*')
},
handle: (key: string, callback: (object: any) => void) => {
window.addEventListener('message', (e) => {
console.log(e);
if (e.data.__key === key) {
callback(e.data)
}
})
}
}
}
}

@ -109,6 +109,7 @@ export function useMHSFServer(id: string) {
const response = await fetch(server.actions.report, { const response = await fetch(server.actions.report, {
body: JSON.stringify({ reason }), body: JSON.stringify({ reason }),
headers: { "Content-Type": "application/json" },
method: "POST", method: "POST",
}); });

@ -0,0 +1,37 @@
/*
* 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 type { OnlineServer } from "./mh-server";
export interface Filter {
toIdentifier(): string;
fromIdentifier(identifier: string): Filter;
applyToServer(server: OnlineServer): boolean;
}

27
apps/www/src/lib/types/mh-server.d.ts vendored Normal file

@ -0,0 +1,27 @@
declare type Server = {
staticInfo: {
_id: string;
serverPlan: string;
serviceStartDate: number;
platform: string;
planMaxPlayers: number;
planRam: number;
alwaysOnline: boolean;
rawPlan: string;
connectedServers: any[];
};
maxPlayers: number;
name: string;
motd: string;
icon: string;
playerData: {
playerCount: number;
timeNoPlayers: number;
};
connectable: boolean;
visibility: boolean;
allCategories: string[];
usingCosmetics: boolean;
author?: string;
authorRank: string;
}

@ -78,6 +78,36 @@ export interface Deletion {
storage_completed_at: number; storage_completed_at: number;
} }
export const globalType = `
export interface OnlineServer {
staticInfo: {
_id: string;
serverPlan: string;
serviceStartDate: number;
platform: string;
planMaxPlayers: number;
planRam: number;
alwaysOnline: boolean;
rawPlan: string;
connectedServers: any[];
};
maxPlayers: number;
name: string;
motd: string;
icon: string;
playerData: {
playerCount: number;
timeNoPlayers: number;
};
connectable: boolean;
visibility: boolean;
allCategories: string[];
usingCosmetics: boolean;
author?: string;
authorRank: string;
}
`
export interface OnlineServer { export interface OnlineServer {
staticInfo: StaticInfo; staticInfo: StaticInfo;
maxPlayers: number; maxPlayers: number;

29
apps/www/src/lib/types/mhsf.d.ts vendored Normal file

@ -0,0 +1,29 @@
declare namespace MHSF {
export type Server = {
staticInfo: {
_id: string;
serverPlan: string;
serviceStartDate: number;
platform: string;
planMaxPlayers: number;
planRam: number;
alwaysOnline: boolean;
rawPlan: string;
connectedServers: any[];
};
maxPlayers: number;
name: string;
motd: string;
icon: string;
playerData: {
playerCount: number;
timeNoPlayers: number;
};
connectable: boolean;
visibility: boolean;
allCategories: string[];
usingCosmetics: boolean;
author?: string;
authorRank: string;
};
}

@ -0,0 +1,37 @@
/*
* 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 type { OnlineServer } from "./mh-server";
export interface Sort {
toIdentifier(): string;
fromIdentifier(identifier: string): Sort;
sortToServers(serverA: OnlineServer, serverB: OnlineServer): number;
}

@ -28,22 +28,63 @@
* OTHER DEALINGS IN THE SOFTWARE. * OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; import {
import { NextRequest, NextResponse } from "next/server"; clerkClient,
clerkMiddleware,
createRouteMatcher,
} from "@clerk/nextjs/server";
import { type NextRequest, NextResponse } from "next/server";
import { ServerResponse } from "./lib/types/mh-server";
// Thanks for the router matcher API Clerk <3 // Thanks for the router matcher API Clerk <3
const isRootRoute = createRouteMatcher(["/"]); const isRootRoute = createRouteMatcher(["/"]);
const isOldServerRoute = createRouteMatcher([
"/server/:serverName",
"/server/:serverName/statistics",
]);
const apiRoute = createRouteMatcher(["/api/(.*)"]);
export default process.env.NEXT_PUBLIC_IS_AUTH === "true" export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
? clerkMiddleware(async (auth, req) => { ? clerkMiddleware(async (auth, req) => {
const authRes = await auth();
const client = await clerkClient();
if (isRootRoute(req)) { if (isRootRoute(req)) {
switch ((await auth()).userId === null) { switch (authRes.userId === null) {
case false: case false:
return NextResponse.redirect(new URL("/servers", req.url)); return NextResponse.redirect(new URL("/servers", req.url));
case true: case 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)) {
const minehut = await fetch(
`https://api.minehut.com/server/${req.url.split("/server/")[1].split("/")[0]}?byName=true`,
);
const minehutRes: { server: ServerResponse | null } =
await minehut.json();
if (minehutRes.server !== null)
return NextResponse.redirect(
new URL(`/server/v2/minehut/${minehutRes.server._id}`, req.url),
);
}
}) })
: (request: NextRequest) => {}; : (request: NextRequest) => {};

@ -32,7 +32,6 @@ import type { OnlineServer } from "@/lib/types/mh-server";
import { Inngest } from "inngest"; import { Inngest } from "inngest";
import { serve } from "inngest/next"; import { serve } from "inngest/next";
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
import { createReportIssue } from "@/lib/linear";
// Create a client to send and receive events // Create a client to send and receive events
export const inngest = new Inngest({ id: "mhsf" }); export const inngest = new Inngest({ id: "mhsf" });
@ -41,20 +40,6 @@ export const inngest = new Inngest({ id: "mhsf" });
export default serve({ export default serve({
client: inngest, client: inngest,
functions: [ functions: [
inngest.createFunction(
{ id: "report" },
{ event: "report-server" },
async ({ event, step }) => {
// by the way, I bombed the Discord stuff
await createReportIssue(
event.data.server,
event.data.reason,
event.data.userId
);
return { event, body: "Done" };
}
),
inngest.createFunction( inngest.createFunction(
{ id: "short-term-data" }, { id: "short-term-data" },
[{ cron: "*/30 * * * *" }, { event: "test/30-min" }], [{ cron: "*/30 * * * *" }, { event: "test/30-min" }],

@ -31,8 +31,9 @@
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { getAuth } from "@clerk/nextjs/server"; import { getAuth } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
import { inngest } from "@/pages/api/inngest";
import { waitUntil } from "@vercel/functions"; import { waitUntil } from "@vercel/functions";
import { getServerName } from "@/lib/history-util";
import { sendDiscordReport } from "@/lib/discord";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
@ -65,16 +66,11 @@ export default async function handler(
reason: reason, reason: reason,
userId: userId, userId: userId,
}); });
// Don't wait for this to finish, just continue anyway // Don't wait for this to finish, just continue anyway
inngest.send({ waitUntil(
name: "report-server", sendDiscordReport(await getServerName(server as string), userId, reason)
data: { );
_id: entry.insertedId.toString(),
server,
reason,
userId,
},
});
// Close the database, but don't close this // Close the database, but don't close this
// serverless instance until it happens // serverless instance until it happens

File diff suppressed because it is too large Load Diff

1393
apps/www/src/theme.json Normal file

File diff suppressed because it is too large Load Diff

@ -9386,6 +9386,11 @@ mlly@^1.7.1, mlly@^1.7.4:
pkg-types "^1.3.0" pkg-types "^1.3.0"
ufo "^1.5.4" ufo "^1.5.4"
monaco-editor@^0.52.2:
version "0.52.2"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.52.2.tgz#53c75a6fcc6802684e99fd1b2700299857002205"
integrity sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==
mongodb-connection-string-url@^3.0.0: mongodb-connection-string-url@^3.0.0:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz#e223089dfa0a5fa9bf505f8aedcbc67b077b33e7" resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz#e223089dfa0a5fa9bf505f8aedcbc67b077b33e7"