From e31881e890eba14ba135e4ace853b17e3f6fa1dc Mon Sep 17 00:00:00 2001 From: dvelo <52332868+DeveloLongScript@users.noreply.github.com> Date: Sat, 22 Mar 2025 20:23:14 -0500 Subject: [PATCH] feat: time for testing! --- apps/www/package.json | 3 + .../app/(sl-modification-frame)/layout.tsx | 2 +- .../category/[category]/page.tsx | 9 +- .../file/[filename]/page.tsx | 403 +++++++++++++++--- .../sl-modification-frame/files/page.tsx | 90 +++- .../embedded/sl-modification-frame/page.tsx | 6 +- .../modification-file-creation-dialog.tsx | 9 +- apps/www/src/lib/try-catch.ts | 55 +++ apps/www/src/lib/types/mh-server.d.ts | 27 -- apps/www/src/middleware.ts | 2 +- yarn.lock | 10 + 11 files changed, 517 insertions(+), 99 deletions(-) create mode 100644 apps/www/src/lib/try-catch.ts delete mode 100644 apps/www/src/lib/types/mh-server.d.ts diff --git a/apps/www/package.json b/apps/www/package.json index abe33f2..6fd5a3e 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -35,6 +35,7 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-switch": "1.1.0", "@radix-ui/react-tabs": "^1.1.3", + "@types/lodash": "^4.17.16", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@unocss/eslint-plugin": "^0.61.5", @@ -51,7 +52,9 @@ "inngest": "^3.21.2", "input-otp": "^1.2.4", "json-beautify": "^1.1.1", + "lodash": "^4.17.21", "lucide-react": "^0.479.0", + "lz-string": "^1.5.0", "mini-svg-data-uri": "^1.4.4", "minimessage-2-html": "1.6.0", "minimessage-js": "^1.1.3", diff --git a/apps/www/src/app/(sl-modification-frame)/layout.tsx b/apps/www/src/app/(sl-modification-frame)/layout.tsx index 7fb0827..4efce4a 100644 --- a/apps/www/src/app/(sl-modification-frame)/layout.tsx +++ b/apps/www/src/app/(sl-modification-frame)/layout.tsx @@ -81,7 +81,7 @@ export default function RootLayout({ - +
{children}
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 c288e5e..7457c56 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 @@ -53,18 +53,19 @@ export default function ServerListCategoryFrame({ return (
-

+

- + {categoryObj?.displayTitle}

{categoryObj?.description} -
+ {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}`)}` @@ -85,7 +86,7 @@ export default function ServerListCategoryFrame({ ))} -
+
); } 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 dd571fc..888c320 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 @@ -1,43 +1,101 @@ "use client"; -import { use } from "react"; +import { use, useEffect, useRef, useState } from "react"; import { useUser } from "@clerk/nextjs"; import type { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog"; import { Link } from "@/components/util/link"; -import { ArrowLeft } from "lucide-react"; +import { AlertOctagon, ArrowLeft, ExternalLink } from "lucide-react"; import Editor from "@monaco-editor/react"; -import poimandres from "@/theme.json"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import * as ts from "typescript"; +import useClipboard from "@/lib/useClipboard"; +import { useTheme } from "@/lib/hooks/use-theme"; +import { + Tooltip, + 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 { cn } from "@/lib/utils"; +import { debounce } from "lodash"; +import { tryCatch } from "@/lib/try-catch"; -const typeDefs = ` -export interface Server { - staticInfo: { - _id: string; - serverPlan: string; - serviceStartDate: number; - platform: string; - planMaxPlayers: number; - planRam: number; - alwaysOnline: boolean; - rawPlan: string; - connectedServers: any[]; - }; - maxPlayers: number; - name: string; - motd: string; - icon: string; - playerData: { - playerCount: number; - timeNoPlayers: number; - }; - connectable: boolean; - visibility: boolean; - allCategories: string[]; - usingCosmetics: boolean; - author?: string; - authorRank: string; +const typeDefs = `// Hi :) how'd you get here? +// Here, in return I'll provide you with a random number: ${Math.ceil(Math.random() * 100)} +// I just wanted you to know that people love you +// and people are there for you :) +// Even when things get sad, just think about the bright side (seriously!) + +export namespace Minehut { + /** + * A Minehut server that is online. You could get this value by using the \`/servers\` endpoint. + */ + export interface OnlineServer { + staticInfo: { + _id: string; + serverPlan: string; + serviceStartDate: number; + platform: string; + planMaxPlayers: number; + planRam: number; + alwaysOnline: boolean; + rawPlan: string; + connectedServers: any[]; + }; + maxPlayers: number; + name: string; + motd: string; + icon: string; + playerData: { + playerCount: number; + timeNoPlayers: number; + }; + connectable: boolean; + visibility: boolean; + allCategories: string[]; + usingCosmetics: boolean; + author?: string; + authorRank: string; + } } `; +const transpileTypeScript = (code: string) => { + try { + const result = ts.transpileModule(typeDefs + code, { + compilerOptions: { + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ESNext, + jsx: ts.JsxEmit.ReactJSX, + esModuleInterop: true, + }, + }); + return result.outputText; + } catch (error) { + console.error("TypeScript transpilation error:", error); + toast.error(`TypeScript error: ${error}`); + return null; + } +}; + +const geistMono = Geist_Mono({ subsets: ["latin"] }); + export default function CustomFilePage({ params, }: { @@ -45,35 +103,242 @@ export default function CustomFilePage({ }) { const { filename } = use(params); const { user } = useUser(); + const monacoRef = + useRef(null); + const { resolvedTheme } = useTheme(); + const [successfullyLinted, setSuccessfullyLinted] = useState(false); + const [syntaxErrors, setSyntaxErrors] = useState< + languages.typescript.Diagnostic[] | null + >(null); const file = ( (user?.unsafeMetadata.customFiles as Array) ?? [] - ).find((c) => c.name === filename); + ).findIndex((c) => c.name === filename); - if (!file) { + if (file === -1) { return <>Bruh.; } - const fileContents = file.contents; + 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 = + (user?.unsafeMetadata.customFiles as Array) ?? + []; + const index = ( + (user?.unsafeMetadata.customFiles as Array) ?? [] + ).findIndex((c) => c.name === filename); + + metadata[index].contents = value; + + await user?.update({ + unsafeMetadata: { + customFiles: metadata, + }, + }); + }; + + 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; + } + console.log("[MHSF Filters] Transpiled TypeScript:", transpiledCode ?? ""); + 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) + toast.error( + "Whoa! We encountered an error while auto-saving. Please copy your code locally to ensure you'll keep your code changes." + ); + }, 300); + + useEffect(() => { + setSuccessfullyLinted(false); + validateCode(value); + debouncedSave(); + }, [value]); return (
- - - - - {filename}.ts - -
+
+ + + + + {filename}.ts + + + {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 + + + + + + ))} +
+
+
+ )} + + + + + + {syntaxErrors !== null && syntaxErrors.length !== 0 + ? "You must have no type errors in the editor to lint, you have " + + syntaxErrors.length + + " error(s)." + : "Check for possible runtime errors."} + + + + + + + + {successfullyLinted + ? "Open a full server-list instance with your filter activated in test mode." + : "You must lint before testing."} + + +
+
+
{ + setValue(newValue || ""); + }} onMount={(editor, monaco) => { + monacoRef.current = monaco; // Ensure TypeScript is properly configured monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.Latest, - allowNonTsExtensions: true, // This is important! + allowNonTsExtensions: true, moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, module: monaco.languages.typescript.ModuleKind.CommonJS, @@ -91,18 +356,35 @@ export default function CustomFilePage({ // Add typedefs as a library monaco.languages.typescript.typescriptDefaults.addExtraLib( typeDefs, - libUri, + libUri ); + // Add actions + [ + { + id: "manually-save-file", + label: "MHSF: Manually Save File", + run: () => { + saveFile(); + toast.success("Manually saved file!"); + }, + }, + { + id: "lint-file", + label: "MHSF: Lint File", + run: lintFile + }, + ].forEach((e) => editor.addAction(e)); + // Create a model for the libUri file if (!monaco.editor.getModel(monaco.Uri.parse(libUri))) { monaco.editor.createModel( typeDefs, "typescript", - monaco.Uri.parse(libUri), + monaco.Uri.parse(libUri) ); } - + // Make sure the current file is using the correct language const currentModel = editor.getModel(); if (currentModel) { @@ -111,11 +393,7 @@ export default function CustomFilePage({ const currentUri = monaco.Uri.parse(`file:///${filename}.ts`); if (!monaco.editor.getModel(currentUri)) { - monaco.editor.createModel( - fileContents, - "typescript", - currentUri, - ); + monaco.editor.createModel(fileContents, "typescript", currentUri); editor.setModel(monaco.editor.getModel(currentUri)); } }} @@ -134,6 +412,7 @@ export default function CustomFilePage({ acceptSuggestionOnEnter: "on", tabCompletion: "on", wordBasedSuggestions: "currentDocument", + cursorSmoothCaretAnimation: "on", parameterHints: { enabled: true, }, @@ -148,3 +427,23 @@ export default function CustomFilePage({
); } + +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 241e5db..b5a7af6 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 @@ -31,9 +31,27 @@ "use client"; import { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog"; +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 { Link } from "@/components/util/link"; import { useUser } from "@clerk/nextjs"; -import { File, FileCode } from "lucide-react"; +import { + ArrowLeft, + Braces, + EllipsisVertical, + File, + FileCode, + Pencil, + Trash, +} from "lucide-react"; +import { toast } from "sonner"; export default function ServerListModificationFrame() { const { user } = useUser(); @@ -41,14 +59,72 @@ export default function ServerListModificationFrame() { (user?.unsafeMetadata.customFiles as Array) ?? []; return (
-

Files

-
- {files.map((c) => ( - - {c.name}.ts +

+ + + + Files +

+ + {files.length === 0 && ( + } + title="We couldn't find any files" + description="Try creating a filter!" + /> + )} + {files.map((c, i) => ( + + + + {c.name}.ts + + + + + + + + { + e.stopPropagation(); + const startTime = Date.now(); + files.splice(i, 1); + await user?.update({ + unsafeMetadata: { + customFiles: files, + }, + }); + toast.success( + "Deleted file in " + (Date.now() - startTime) + "ms" + ); + }} + > + Delete + + + Rename + + + + ))} -
+
); } diff --git a/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/page.tsx b/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/page.tsx index 614949c..346a935 100644 --- a/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/page.tsx +++ b/apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/page.tsx @@ -56,8 +56,7 @@ export default function ServerListModificationFrame() { viewing experience. We frequently add new filters in accordance to new features, as well. - -
+ {serverModDB.map((c) => (

@@ -73,6 +72,7 @@ export default function ServerListModificationFrame() {
{c.entries.map((m) => ( @@ -95,7 +95,7 @@ export default function ServerListModificationFrame() {
))} -

+ ); } diff --git a/apps/www/src/components/feat/server-list/modification/modification-file-creation-dialog.tsx b/apps/www/src/components/feat/server-list/modification/modification-file-creation-dialog.tsx index 37838cb..0833088 100644 --- a/apps/www/src/components/feat/server-list/modification/modification-file-creation-dialog.tsx +++ b/apps/www/src/components/feat/server-list/modification/modification-file-creation-dialog.tsx @@ -42,16 +42,16 @@ import { useUser } from "@clerk/nextjs"; import { useState, type ReactNode } from "react"; import { toast } from "sonner"; -const sortTemplate = `import type { Server } from "mhsf"; +const sortTemplate = `import type { Minehut } from "mhsf"; -export function sort(serverA: Server, serverB: Server): number { +export function sort(serverA: Minehut.OnlineServer, serverB: Minehut.OnlineServer): number { // Your code here // Use logic like \`Array.sort\` or (a: V, b: V) => number }`; -const filterTemplate = `import type { Server } from "mhsf"; +const filterTemplate = `import type { Minehut } from "mhsf"; -export function filter(server: Server): boolean { +export function filter(server: Minehut.OnlineServer): boolean { // Your code here // Returning true indicates the server will stay, while returning false will remove the server from the queue. }`; @@ -60,6 +60,7 @@ export type ClerkCustomModification = { name: string; // Add .ts to the end active: boolean; contents: string; + testId?: string; }; export function ModificationFileCreationDialog({ diff --git a/apps/www/src/lib/try-catch.ts b/apps/www/src/lib/try-catch.ts new file mode 100644 index 0000000..1062a48 --- /dev/null +++ b/apps/www/src/lib/try-catch.ts @@ -0,0 +1,55 @@ +/* + * 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. + */ +// Thanks theo <3 + +// Types for the result object with discriminated union +type Success = { + data: T; + error: null; +}; + +type Failure = { + data: null; + error: E; +}; + +type Result = Success | Failure; + +// Main wrapper function +export async function tryCatch( + promise: Promise, +): Promise> { + try { + const data = await promise; + return { data, error: null }; + } catch (error) { + return { data: null, error: error as E }; + } +} \ No newline at end of file diff --git a/apps/www/src/lib/types/mh-server.d.ts b/apps/www/src/lib/types/mh-server.d.ts deleted file mode 100644 index 9f1ca0e..0000000 --- a/apps/www/src/lib/types/mh-server.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -declare type Server = { - staticInfo: { - _id: string; - serverPlan: string; - serviceStartDate: number; - platform: string; - planMaxPlayers: number; - planRam: number; - alwaysOnline: boolean; - rawPlan: string; - connectedServers: any[]; - }; - maxPlayers: number; - name: string; - motd: string; - icon: string; - playerData: { - playerCount: number; - timeNoPlayers: number; - }; - connectable: boolean; - visibility: boolean; - allCategories: string[]; - usingCosmetics: boolean; - author?: string; - authorRank: string; -} \ No newline at end of file diff --git a/apps/www/src/middleware.ts b/apps/www/src/middleware.ts index 4211ff0..9495797 100644 --- a/apps/www/src/middleware.ts +++ b/apps/www/src/middleware.ts @@ -34,7 +34,7 @@ import { createRouteMatcher, } from "@clerk/nextjs/server"; import { type NextRequest, NextResponse } from "next/server"; -import { ServerResponse } from "./lib/types/mh-server"; +import type { ServerResponse } from "./lib/types/mh-server"; // Thanks for the router matcher API Clerk <3 const isRootRoute = createRouteMatcher(["/"]); diff --git a/yarn.lock b/yarn.lock index 87e188e..5895f13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3399,6 +3399,11 @@ resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.16.7.tgz#03ab680ab4fa4fbc6cb46ecf987ecad5d8019868" integrity sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ== +"@types/lodash@^4.17.16": + version "4.17.16" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.16.tgz#94ae78fab4a38d73086e962d0b65c30d816bfb0a" + integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g== + "@types/luxon@~3.4.0": version "3.4.2" resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7" @@ -8123,6 +8128,11 @@ luxon@~3.5.0: resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20" integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ== +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + magic-bytes.js@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz#c41cf4bc2f802992b05e64962411c9dd44fdef92"