feat: clean up code

This commit is contained in:
dvelo 2025-04-04 21:10:56 -05:00
parent ed2d6f0ac1
commit db992412d0
16 changed files with 1265 additions and 986 deletions

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

@ -15,7 +15,7 @@
},
"dependencies": {
"@babel/parser": "^7.24.7",
"@biomejs/biome": "^1.8.3",
"@biomejs/biome": "^1.9.4",
"@clerk/elements": "^0.22.2",
"@clerk/nextjs": "^6.9.2",
"@emotion/is-prop-valid": "^1.3.0",

@ -30,8 +30,7 @@
"use client";
import { ModificationAction } from "@/components/feat/server-list/modification/modification-action";
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import type { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import {
Setting,
SettingContent,
@ -40,15 +39,30 @@ import {
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, Filter, SortAsc } from "lucide-react";
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,
@ -61,13 +75,28 @@ export default function ModificationPage({
defaultValue: "/servers/embedded/sl-modification-frame",
});
console.log(mod);
const modObj = (
const modIndex = (
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
).find((c) => c.friendlyName === atob(decodeURIComponent(mod)));
).findIndex((c) => c.friendlyName === atob(decodeURIComponent(mod)));
if (modObj === undefined)
return <>We couldn't find the modification you were looking for.</>;
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">
@ -86,13 +115,48 @@ export default function ModificationPage({
This is a custom modification. Enable it! (or not) It's your own! (are
you proud?)
</Markdown>
<Button className="mt-2">
{modObj?.active ? "Disable" : "Enable"}
</Button>
<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">
<Material className="mt-6 grid gap-3">
<Setting>
<SettingContent className="flex items-center">
<SettingMeta>
@ -115,6 +179,23 @@ export default function ModificationPage({
</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,9 +28,8 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { ModificationAction } from "@/components/feat/server-list/modification/modification-action";
import { ModificationCustomModificationRow } from "@/components/feat/server-list/modification/modification-custom-modification-row";
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
import { Material } from "@/components/ui/material";
import { Separator } from "@/components/ui/separator";
@ -43,17 +42,15 @@ import { ArrowLeft, Binary } from "lucide-react";
import { use } from "react";
import Markdown from "react-markdown";
export default function ServerListCategoryFrame({
export default async function ServerListCategoryFrame({
params,
}: {
params: Promise<{ category: string }>;
}) {
const { user } = useUser();
const { category } = use(params);
const { category } = await params;
const categoryObj = serverModDB.find(
(c) => c.displayTitle === atob(category),
(c) => c.displayTitle === atob(decodeURIComponent(category)),
);
const router = useRouter();
return (
<main className="max-w-[800px] p-4">
@ -67,57 +64,32 @@ export default function ServerListCategoryFrame({
<Material className="mt-10 p-4 grid grid-cols-6 gap-2">
{categoryObj?.entries.map((m) => (
<Material
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
elevation="high"
onClick={() =>
router.push(
`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`,
)
}
<Link
key={m.name}
href={`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`}
>
<div
className={cn(
"w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center",
)}
style={{ backgroundColor: m.color }}
<Material
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
elevation="high"
>
<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
className={cn(
"w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center",
)}
style={{ backgroundColor: m.color }}
>
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
</div>
<span className="text-sm text-center w-full flex items-center justify-center">
{m.name}
</span>
</Material>
</Link>
))}
<SignedIn>
{categoryObj?.__custom &&
(
(user?.unsafeMetadata
.activatedModifications as ClerkCustomActivatedModification[]) ??
[]
).map((m) => (
<Material
elevation="high"
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
key={m.friendlyName}
onClick={() =>
router.push(
`/servers/embedded/sl-modification-frame/category/${category}/modification/_custom/${btoa(m.friendlyName)}`,
)
}
>
<div
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
style={{ backgroundColor: m.color }}
>
<Binary className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
</div>
<span className="text-sm text-center w-full flex items-center justify-center">
{m.friendlyName}
</span>
</Material>
))}
{categoryObj?.__custom && (
<ModificationCustomModificationRow category={category} />
)}
</SignedIn>
</Material>
</main>

@ -2,14 +2,10 @@
import { use, useEffect, useRef, useState } from "react";
import { useUser } from "@clerk/nextjs";
import type {
ClerkCustomActivatedModification,
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 { AlertOctagon, ArrowLeft, Check, ExternalLink } from "lucide-react";
import { ArrowLeft, FileQuestion } from "lucide-react";
import Editor from "@monaco-editor/react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import * as ts from "typescript";
import useClipboard from "@/lib/useClipboard";
@ -19,50 +15,18 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { languages, Uri } from "monaco-editor";
import {
Drawer,
DrawerContent,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Alert } from "@/components/ui/alert";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { compressToEncodedURIComponent } from "lz-string";
import { Geist_Mono } from "next/font/google";
import type { languages } from "monaco-editor";
import { cn } from "@/lib/utils";
import { debounce } from "lodash";
import { tryCatch } from "@/lib/try-catch";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Material } from "@/components/ui/material";
import {
Setting,
SettingContent,
SettingDescription,
SettingMeta,
SettingTitle,
} from "@/components/feat/settings/setting";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { DialogTrigger } from "@/components/ui/dialog";
import { useRouter } from "@/lib/useRouter";
import { Placeholder } from "@/components/ui/placeholder";
import { CustomErrors } from "@/components/feat/server-list/modification/custom-files/custom-errors";
import { CustomLint } from "@/components/feat/server-list/modification/custom-files/custom-lint";
import { CustomTest } from "@/components/feat/server-list/modification/custom-files/custom-test";
export type MonacoRefType = typeof import(
"monaco-editor/esm/vs/editor/editor.api"
);
const typeDefs = `// Hi :) how'd you get here?
// Here, in return I'll provide you with a random number: ${Math.ceil(Math.random() * 100)}
@ -122,8 +86,6 @@ export const transpileTypeScript = (code: string) => {
}
};
const geistMono = Geist_Mono({ subsets: ["latin"] });
export default function CustomFilePage({
params,
}: {
@ -138,43 +100,28 @@ export default function CustomFilePage({
const [syntaxErrors, setSyntaxErrors] = useState<
languages.typescript.Diagnostic[] | null
>(null);
const [testMode, setTestMode] = useState("");
const router = useRouter();
const file = (
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
).findIndex((c) => c.name === filename);
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) => {
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);
});
});
});
};
const fileContents = ((user?.unsafeMetadata
.customFiles as Array<ClerkCustomModification>) ?? [])[file].contents;
const [value, setValue] = useState(fileContents);
const clipboard = useClipboard();
validateCode(value);
const saveFile = async () => {
const metadata =
@ -194,40 +141,6 @@ export default function CustomFilePage({
});
};
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);
}
};
const debouncedSave = debounce(async () => {
const { error } = await tryCatch(saveFile());
if (error)
@ -236,9 +149,9 @@ export default function CustomFilePage({
);
}, 300);
// biome-ignore lint: L
useEffect(() => {
setSuccessfullyLinted(false);
validateCode(value);
debouncedSave();
}, [value]);
@ -253,350 +166,33 @@ export default function CustomFilePage({
</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>
<CustomErrors
filename={filename}
value={value}
monacoRef={monacoRef}
/>
)}
<Tooltip>
<TooltipTrigger>
<Button
onClick={lintFile}
disabled={syntaxErrors === null || syntaxErrors.length !== 0}
variant={successfullyLinted ? "success-subtle" : "secondary"}
>
Lint
</Button>
<CustomLint
successfullyLinted={successfullyLinted}
setSuccessfullyLinted={setSuccessfullyLinted}
syntaxErrors={syntaxErrors}
value={value}
/>
</TooltipTrigger>
<TooltipContent>
{syntaxErrors !== null && syntaxErrors.length !== 0
? "You must have no type errors in the editor to lint, you have " +
syntaxErrors.length +
" error(s)."
? `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>
{(() => {
const [open, setOpen] = useState(false);
const [filterEnabled, setFilterEnabled] = useState(true);
const [sortEnabled, setSortEnabled] = useState(true);
const [success, setSuccess] = useState(false);
const [fileName, setFileName] = useState("");
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]);
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}
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 && (
<>
<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={fileName}
onChange={(c) =>
setFileName(c.target.value)
}
/>
</SettingContent>
</Setting>
</Material>
<DialogTrigger>
<Button
className="w-full my-2"
disabled={fileName === ""}
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,
// I'm too lazy to change this
friendlyName: fileName,
transpiledContents: transpiledValue,
active: true,
testMode: testMode as "filter" | "sort",
color
};
} else {
array.push({
originalFileName: filename,
// ... and this too
friendlyName: fileName,
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>
</>
)}
</DrawerContent>
</Drawer>
</>
);
})()}
<CustomTest
value={value}
successfullyLinted={successfullyLinted}
/>
</TooltipTrigger>
<TooltipContent>
{successfullyLinted
@ -641,23 +237,6 @@ export default function CustomFilePage({
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(
@ -756,23 +335,3 @@ export async function findSupportedOperations(
return returnValue;
}
function guidGenerator() {
const S4 = () => {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return (
S4() +
S4() +
"-" +
S4() +
"-" +
S4() +
"-" +
S4() +
"-" +
S4() +
S4() +
S4()
);
}

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

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

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

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

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

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

@ -28,22 +28,39 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { createContext } from "@/server/context";
import { appRouter } from "@/server/router/_app";
import { getAuth } from "@clerk/nextjs/server";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
// I still have no clue why this works.
import type { NextRequest } from "../../../../../../../node_modules/@clerk/nextjs/node_modules/next/dist/server/web/spec-extension/request";
"use client";
const handler = (request: NextRequest) => {
const {userId} = getAuth(request);
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";
return fetchRequestHandler({
endpoint: "/api/trpc",
req: request,
router: appRouter,
createContext: createContext(userId),
});
};
export function ModificationCustomModificationRow({category}: {category: string}) {
const { user } = useUser();
export { handler as GET, handler as POST };
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>
));
}

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

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

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

@ -99,7 +99,7 @@
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@biomejs/biome@^1.8.3":
"@biomejs/biome@^1.9.4":
version "1.9.4"
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.9.4.tgz#89766281cbc3a0aae865a7ff13d6aaffea2842bf"
integrity sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==