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>
<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,15 +64,13 @@ export default function ServerListCategoryFrame({
<Material className="mt-10 p-4 grid grid-cols-6 gap-2">
{categoryObj?.entries.map((m) => (
<Link
key={m.name}
href={`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`}
>
<Material
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
elevation="high"
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(
@ -89,35 +84,12 @@ export default function ServerListCategoryFrame({
{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)
}
<CustomTest
value={value}
successfullyLinted={successfullyLinted}
/>
</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>
</>
);
})()}
</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()
);
}

@ -52,7 +52,7 @@ import {
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";
@ -60,8 +60,9 @@ 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)
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">
@ -86,13 +87,14 @@ export default function ServerListModificationFrame() {
>
<span className="flex items-center gap-1">
<FileCode size={16} />
{operations[i].filter && <Filter size={16}/>}
{operations[i].sort && <SortAsc 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"
@ -118,7 +120,7 @@ export default function ServerListModificationFrame() {
},
});
toast.success(
"Deleted file in " + (Date.now() - startTime) + "ms"
"Deleted file in " + (Date.now() - startTime) + "ms",
);
}}
>
@ -136,3 +138,21 @@ export default function ServerListModificationFrame() {
</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
</ul>
</span>
<div className="block">
<div className="flex items-center mb-2 justify-end gap-2">
>
<DropdownMenu>
<DropdownMenuTrigger>
<a className="text-muted-foreground hover:text-shadcn-primary transition-colors cursor-pointer">Discord</a>
<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="block py-2">
<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="block py-2">
<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>
</li>
</ul>
</span>
<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>
));
}

@ -47,13 +47,13 @@ export function DebugMenu({
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]);
@ -147,7 +147,7 @@ export function DebugMenu({
</SettingContent>
</Setting>
<Setting className="py-3">
<SettingContent>
{debugOptions.mhsfData !== undefined && <SettingContent>
<SettingMeta>
<SettingTitle>Total Statistical Data Count</SettingTitle>
<SettingDescription>
@ -155,12 +155,24 @@ export function DebugMenu({
</SettingDescription>
</SettingMeta>
{convert(
debugOptions.mhsfData.achievements.historically.length +
debugOptions.mhsfData.playerData.historically.length +
debugOptions.mhsfData.favoriteData.favoriteHistoricalData
.length
(
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>
</SettingContent>}
</Setting>
<Setting className="py-3">

@ -15,6 +15,9 @@ 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,
@ -41,7 +44,7 @@ export function StatisticsMainRow({
"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"
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
)}
onClick={() => setStatisticType("playerCount")}
>
@ -56,7 +59,7 @@ export function StatisticsMainRow({
"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"
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
)}
onClick={() => setStatisticType("favorites")}
>
@ -64,7 +67,7 @@ export function StatisticsMainRow({
<Badge className="px-1">
<code>
{convert(
mhsfData.server?.favoriteData.favoriteNumber as number
mhsfData.server?.favoriteData.favoriteNumber as number,
)}
</code>
</Badge>
@ -73,7 +76,12 @@ export function StatisticsMainRow({
<Separator />
</div>
<div className="mt-2">
{!mhsfData.loading && (
{!mhsfData.loading ? (
<>
{(statisticType === "playerCount"
? mhsfData.server?.playerData.historically
: mhsfData.server?.favoriteData.favoriteHistoricalData
)?.length !== 0 ? (
<StatisticsChart
data={
statisticType === "playerCount"
@ -82,6 +90,18 @@ export function StatisticsMainRow({
}
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>

@ -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==