diff --git a/apps/www/next.config.mjs b/apps/www/next.config.mjs
index 467a269..b7a7bea 100644
--- a/apps/www/next.config.mjs
+++ b/apps/www/next.config.mjs
@@ -38,6 +38,10 @@ const nextConfig = {
protocol: "https",
hostname: "img.clerk.com",
},
+ {
+ protocol: "https",
+ hostname: "avatars.githubusercontent.com"
+ }
],
},
async redirects() {
diff --git a/apps/www/package.json b/apps/www/package.json
index 022d105..2fa2438 100644
--- a/apps/www/package.json
+++ b/apps/www/package.json
@@ -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",
diff --git a/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/modification/custom/[custom-mod]/page.tsx b/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/modification/custom/[custom-mod]/page.tsx
index 68c04ba..5b214c9 100644
--- a/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/modification/custom/[custom-mod]/page.tsx
+++ b/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/modification/custom/[custom-mod]/page.tsx
@@ -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 (
+
+ );
+
+ const modObj = ((user?.unsafeMetadata
+ .activatedModifications as ClerkCustomActivatedModification[]) ?? [])[
+ modIndex
+ ];
return (
@@ -86,13 +115,48 @@ export default function ModificationPage({
This is a custom modification. Enable it! (or not) It's your own! (are
you proud?)
-
- {modObj?.active ? "Disable" : "Enable"}
-
+
+
+ {modObj?.active ? "Disable" : "Enable"}
+
+
+
+
+
+
+
+
+ {
+ 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`);
+ }}
+ >
+ Delete
+
+
+
+
-
+
@@ -115,6 +179,23 @@ export default function ModificationPage({
+
+
+
+ File name
+
+
+
+ {modObj.originalFileName}.ts
+
+
+
+
diff --git a/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/page.tsx b/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/page.tsx
index 5910a26..d7f0bc9 100644
--- a/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/page.tsx
+++ b/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/page.tsx
@@ -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 (
@@ -67,57 +64,32 @@ export default function ServerListCategoryFrame({
{categoryObj?.entries.map((m) => (
-
- router.push(
- `/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`,
- )
- }
+
-
-
-
-
- {m.name}
-
-
+
+
+
+
+ {m.name}
+
+
+
))}
- {categoryObj?.__custom &&
- (
- (user?.unsafeMetadata
- .activatedModifications as ClerkCustomActivatedModification[]) ??
- []
- ).map((m) => (
-
- router.push(
- `/servers/embedded/sl-modification-frame/category/${category}/modification/_custom/${btoa(m.friendlyName)}`,
- )
- }
- >
-
-
-
-
- {m.friendlyName}
-
-
- ))}
+ {categoryObj?.__custom && (
+
+ )}
diff --git a/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page.tsx b/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page.tsx
index 1a51021..f10c86f 100644
--- a/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page.tsx
+++ b/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page.tsx
@@ -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) ?? []
).findIndex((c) => c.name === filename);
if (file === -1) {
- return <>Bruh.>;
+ return (
+
+ );
}
- 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) ?? [])[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({
{syntaxErrors !== null && syntaxErrors.length !== 0 && (
-
-
-
-
-
-
-
- Type Errors
-
- {syntaxErrors.map((c, i) => (
-
- {c.messageText.toString()}{" "}
-
-
-
- (TS{typeof c !== "string" && c.code})
-
-
-
-
-
- typescript.tv
-
-
-
- ts-error-translator
-
-
-
-
-
- ))}
-
-
-
+
)}
-
- Lint
-
+
{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."}
- {(() => {
- 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 (
- <>
- setOpen(true)}
- >
- Test
-
-
-
-
- You can run an interactive server-list environment
- with actual online servers to test your modifications.
-
-
-
-
-
-
- Function to test
-
- You can pick to either test a sorting system
- or a filter.
-
-
-
-
-
-
-
-
-
- filter
-
-
- sort
-
-
-
-
-
-
-
-
- {
- 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 && }Test
-
- {success && (
- <>
-
- 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.
-
- {(
- (user?.unsafeMetadata
- .activatedModifications as ClerkCustomActivatedModification[]) ??
- []
- ).find((c) => c.originalFileName === filename && c.testMode === testMode) !==
- undefined && (
-
- This modification was already activated! Hitting
- activate here will just overwrite the contents
- and the new friendly name.
-
- )}
-
-
-
-
-
- Name
-
- Set a friendly name for your modification.
-
-
-
- setFileName(c.target.value)
- }
- />
-
-
-
-
- {
- 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
-
-
- >
- )}
-
-
- >
- );
- })()}
+
{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()
- );
-}
diff --git a/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/files/page.tsx b/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/files/page.tsx
index 49fd4ea..2918326 100644
--- a/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/files/page.tsx
+++ b/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/files/page.tsx
@@ -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) ?? [];
- const operations = use((async () => await Promise.all(files.map(async (c) => await findSupportedOperations(c.contents))))())
- console.log(operations)
- return (
-
-
-
-
-
- Files
-
-
- {files.length === 0 && (
- }
- title="We couldn't find any files"
- description="Try creating a filter!"
- />
- )}
- {files.map((c, i) => (
-
-
-
- {operations[i].filter && }
- {operations[i].sort && }
- {c.name}.ts
-
-
-
-
-
-
-
-
-
- {
- 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"
- );
- }}
- >
- Delete
-
-
- Rename
-
-
-
-
-
- ))}
-
-
- );
+ const { user } = useUser();
+ const files =
+ (user?.unsafeMetadata.customFiles as Array) ?? [];
+ const operations = usePlatforms(files);
+
+
+ return (
+
+
+
+
+
+ Files
+
+
+ {files.length === 0 && (
+ }
+ title="We couldn't find any files"
+ description="Try creating a filter!"
+ />
+ )}
+ {files.map((c, i) => (
+
+
+
+ {operations[i].filter && }
+ {operations[i].sort && }
+ {c.name}.ts
+
+
+
+
+
+
+
+
+
+
+ {
+ 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",
+ );
+ }}
+ >
+ Delete
+
+
+ Rename
+
+
+
+
+
+ ))}
+
+
+ );
+}
+
+function usePlatforms(files: Array) {
+ 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;
}
diff --git a/apps/www/src/components/feat/footer/footer.tsx b/apps/www/src/components/feat/footer/footer.tsx
index 6a66e3a..962cfe7 100644
--- a/apps/www/src/components/feat/footer/footer.tsx
+++ b/apps/www/src/components/feat/footer/footer.tsx
@@ -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 (
-
+
@@ -36,32 +39,48 @@ export function Footer() {
Contact
-
-
-
- Discord
-
-
-
-
- Minehut Discord
- Not officially owned by MHSF, however conversations about MHSF and related take place there.
-
-
-
-
- MHSF Discord
- A read-only server for updates related to MHSF.
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Minehut Discord
+ Not officially owned by MHSF, however conversations about MHSF and related take place there.
+
+
+
+
+
+
+
+ MHSF Discord
+ A read-only server for updates related to MHSF.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/www/src/components/feat/server-list/modification/custom-files/custom-errors.tsx b/apps/www/src/components/feat/server-list/modification/custom-files/custom-errors.tsx
new file mode 100644
index 0000000..37f5e8f
--- /dev/null
+++ b/apps/www/src/components/feat/server-list/modification/custom-files/custom-errors.tsx
@@ -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;
+ filename: string;
+}) {
+ const [syntaxErrors, setSyntaxErrors] = useState();
+
+ 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 (
+
+
+
+
+
+
+
+ Type Errors
+
+ {syntaxErrors.map((c, i) => (
+
+ {c.messageText.toString()}{" "}
+
+
+
+ (TS{typeof c !== "string" && c.code})
+
+
+
+
+
+ typescript.tv
+
+
+ ts-error-translator
+
+
+
+
+ ))}
+
+
+
+ );
+
+ return null;
+}
diff --git a/apps/www/src/components/feat/server-list/modification/custom-files/custom-lint.tsx b/apps/www/src/components/feat/server-list/modification/custom-files/custom-lint.tsx
new file mode 100644
index 0000000..780178f
--- /dev/null
+++ b/apps/www/src/components/feat/server-list/modification/custom-files/custom-lint.tsx
@@ -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 (
+
+ Lint
+
+ );
+}
diff --git a/apps/www/src/components/feat/server-list/modification/custom-files/custom-test-success.tsx b/apps/www/src/components/feat/server-list/modification/custom-files/custom-test-success.tsx
new file mode 100644
index 0000000..0f9c1c9
--- /dev/null
+++ b/apps/www/src/components/feat/server-list/modification/custom-files/custom-test-success.tsx
@@ -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 (
+ <>
+
+ 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.
+
+ {(
+ (user?.unsafeMetadata
+ .activatedModifications as ClerkCustomActivatedModification[]) ?? []
+ ).find(
+ (c) => c.originalFileName === filename && c.testMode === testMode,
+ ) !== undefined && (
+
+ This modification was already activated! Hitting activate here will
+ just overwrite the contents and the new friendly name.
+
+ )}
+
+
+
+
+
+ Name
+
+ Set a friendly name for your modification.
+
+
+ setFriendlyName(c.target.value)}
+ />
+
+
+
+
+ {
+ 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
+
+
+ >
+ );
+}
diff --git a/apps/www/src/components/feat/server-list/modification/custom-files/custom-test.tsx b/apps/www/src/components/feat/server-list/modification/custom-files/custom-test.tsx
new file mode 100644
index 0000000..b008c3b
--- /dev/null
+++ b/apps/www/src/components/feat/server-list/modification/custom-files/custom-test.tsx
@@ -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 (
+ <>
+ setOpen(true)}>
+ Test
+
+
+
+
+ You can run an interactive server-list environment with actual
+ online servers to test your modifications.
+
+
+
+
+
+
+ Function to test
+
+ You can pick to either test a sorting system or a filter.
+
+
+ void}
+ disabled={success}
+ >
+
+
+
+
+
+
+ filter
+
+
+ sort
+
+
+
+
+
+
+
+
+ {
+ 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 && }Test
+
+ {success && (
+
+ )}
+
+
+ >
+ );
+}
diff --git a/apps/www/src/app/api/trpc/[trpc]/route.ts b/apps/www/src/components/feat/server-list/modification/modification-custom-modification-row.tsx
similarity index 53%
rename from apps/www/src/app/api/trpc/[trpc]/route.ts
rename to apps/www/src/components/feat/server-list/modification/modification-custom-modification-row.tsx
index e4733b7..4dd9196 100644
--- a/apps/www/src/app/api/trpc/[trpc]/route.ts
+++ b/apps/www/src/components/feat/server-list/modification/modification-custom-modification-row.tsx
@@ -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) => (
+
+
+
+
+
+
+ {m.friendlyName}
+
+
+
+ ));
+}
diff --git a/apps/www/src/components/feat/server-page/debug/debug-menu.tsx b/apps/www/src/components/feat/server-page/debug/debug-menu.tsx
index 3de6783..c5d9b46 100644
--- a/apps/www/src/components/feat/server-page/debug/debug-menu.tsx
+++ b/apps/www/src/components/feat/server-page/debug/debug-menu.tsx
@@ -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 (
-
-
-
- Debug Options
-
-
- 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.{" "}
-
- This only shows up when Debug Mode is enabled. (or when using
- Ctrl+Shift+O)
-
-
-
-
-
-
- Server name
-
- Name of server after being parsed through Minehut API (aka
- server.name)
-
-
- {debugOptions.serverName}
-
-
-
-
-
-
-
- Server Id
-
- Passed usually through query
-
-
- {debugOptions.serverId}
-
-
-
-
-
- {debugOptions.serverData === null && } Parsed Minehut
- data
-
- clipboard.writeText(JSON.stringify(debugOptions.serverData))
- }
- >
- Copy (no toast!)
-
-
-
-
-
-
- {debugOptions.mhsfData === null && } Parsed MHSF data
-
- clipboard.writeText(JSON.stringify(debugOptions.mhsfData))
- }
- >
- Copy (no toast!)
-
-
- {debugOptions.mhsfData !== null && (
- <>
-
-
-
- See all data
-
- WARNING: this data is MASSIVE. (@keyboard yk what else is
- massive?)
-
-
-
- Open data
-
-
-
-
-
-
- Total Statistical Data Count
-
- How many times has MHSF grabbed data about this server?
-
-
- {convert(
- debugOptions.mhsfData.achievements.historically.length +
- debugOptions.mhsfData.playerData.historically.length +
- debugOptions.mhsfData.favoriteData.favoriteHistoricalData
- .length
- )}
-
-
+ return (
+
+
+
+ Debug Options
+
+
+ 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.{" "}
+
+ This only shows up when Debug Mode is enabled. (or when using
+ Ctrl+Shift+O)
+
+
+
+
+
+
+ Server name
+
+ Name of server after being parsed through Minehut API (aka
+ server.name)
+
+
+ {debugOptions.serverName}
+
+
+
+
+
+
+
+ Server Id
+
+ Passed usually through query
+
+
+ {debugOptions.serverId}
+
+
+
+
+
+ {debugOptions.serverData === null && } Parsed Minehut
+ data
+
+ clipboard.writeText(JSON.stringify(debugOptions.serverData))
+ }
+ >
+ Copy (no toast!)
+
+
+
+
+
+
+ {debugOptions.mhsfData === null && } Parsed MHSF data
+
+ clipboard.writeText(JSON.stringify(debugOptions.mhsfData))
+ }
+ >
+ Copy (no toast!)
+
+
+ {debugOptions.mhsfData !== null && (
+ <>
+
+
+
+ See all data
+
+ WARNING: this data is MASSIVE. (@keyboard yk what else is
+ massive?)
+
+
+
+ Open data
+
+
+
+
+ {debugOptions.mhsfData !== undefined &&
+
+ Total Statistical Data Count
+
+ How many times has MHSF grabbed data about this server?
+
+
+ {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,
+ )}
+ }
+
+
-
-
-
-
- Disable image caching on customization images
-
-
- Enabling this could result in being tracked but{" "}
- very rarely could render the image
- faster. (removes wsrv.nl caching)
-
-
-
-
-
- >
- )}
-
-
-
- );
+
+
+
+
+ Disable image caching on customization images
+
+
+ Enabling this could result in being tracked but{" "}
+ very rarely could render the image
+ faster. (removes wsrv.nl caching)
+
+
+
+
+
+ >
+ )}
+
+
+
+ );
}
diff --git a/apps/www/src/components/feat/server-page/stats/stats-main-row.tsx b/apps/www/src/components/feat/server-page/stats/stats-main-row.tsx
index ecb9231..e526fda 100644
--- a/apps/www/src/components/feat/server-page/stats/stats-main-row.tsx
+++ b/apps/www/src/components/feat/server-page/stats/stats-main-row.tsx
@@ -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;
+ server: ServerResponse;
+ mhsfData: ReturnType;
}) {
- const [statisticType, setStatisticType] = useQueryState("st", {
- defaultValue: "playerCount",
- });
+ const [statisticType, setStatisticType] = useQueryState("st", {
+ defaultValue: "playerCount",
+ });
- return (
-
-
-
- Statistics
- setStatisticType("playerCount")}
- >
- Player Count
-
- {convert(server.joins)}
-
-
- setStatisticType("favorites")}
- >
- Favorites
-
-
- {convert(
- mhsfData.server?.favoriteData.favoriteNumber as number
- )}
-
-
-
-
-
-
-
- {!mhsfData.loading && (
-
- )}
-
-
- );
+ return (
+
+
+
+ Statistics
+ setStatisticType("playerCount")}
+ >
+ Player Count
+
+ {convert(server.joins)}
+
+
+ setStatisticType("favorites")}
+ >
+ Favorites
+
+
+ {convert(
+ mhsfData.server?.favoriteData.favoriteNumber as number,
+ )}
+
+
+
+
+
+
+
+ {!mhsfData.loading ? (
+ <>
+ {(statisticType === "playerCount"
+ ? mhsfData.server?.playerData.historically
+ : mhsfData.server?.favoriteData.favoriteHistoricalData
+ )?.length !== 0 ? (
+
+ ) : (
+
+ }
+ title="There is no data to be collected"
+ description="This server probably never had any data collected in your choosen timespan."
+ />
+
+ )}
+ >
+ ) : (
+
+ )}
+
+
+ );
}
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 (
-
-
-
-
- {
- return `${new Date(value).toLocaleDateString("en-US", {
- day: "numeric",
- month: "short",
- })} ${new Date(value).toLocaleTimeString("en-US", {
- timeStyle: "short",
- })}`;
- }}
- />
- }
- />
-
-
-
-
-
-
-
-
-
- );
+ console.log(data);
+ return (
+
+
+
+
+ {
+ return `${new Date(value).toLocaleDateString("en-US", {
+ day: "numeric",
+ month: "short",
+ })} ${new Date(value).toLocaleTimeString("en-US", {
+ timeStyle: "short",
+ })}`;
+ }}
+ />
+ }
+ />
+
+
+
+
+
+
+
+
+
+ );
}
diff --git a/apps/www/src/config/tags.tsx b/apps/www/src/config/tags.tsx
index 26d7f23..9681698 100644
--- a/apps/www/src/config/tags.tsx
+++ b/apps/www/src/config/tags.tsx
@@ -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,
diff --git a/yarn.lock b/yarn.lock
index b5ba46d..4c9bad4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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==