mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-07 15:05:02 -05:00
feat: custom modifications
This commit is contained in:
parent
bddf5f1528
commit
ed2d6f0ac1
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "mh-stats",
|
||||
"name": "mhsf",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"packageManager": "yarn@1.22.22",
|
||||
@ -35,9 +35,15 @@
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@trpc/client": "^11.0.0",
|
||||
"@trpc/next": "^11.0.0",
|
||||
"@trpc/react-query": "^11.0.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/request-ip": "^0.0.41",
|
||||
"@unocss/eslint-plugin": "^0.61.5",
|
||||
"@unocss/postcss": "^0.61.5",
|
||||
"@unocss/transformer-directives": "^0.61.5",
|
||||
@ -80,8 +86,10 @@
|
||||
"recharts": "^2.15.1",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"request-ip": "^3.3.0",
|
||||
"sonner": "^1.7.0",
|
||||
"stripe-gradient": "^1.0.1",
|
||||
"superjson": "^2.2.2",
|
||||
"swapy": "^1.0.5",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^4.0.7",
|
||||
@ -89,7 +97,8 @@
|
||||
"tailwindcss-patch": "^4.0.0",
|
||||
"turbo": "^2.4.0",
|
||||
"unplugin-tailwindcss-mangle": "^3.0.1",
|
||||
"vaul": "^1.1.2"
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clerk/themes": "^2.1.19",
|
||||
|
||||
122
apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/modification/custom/[custom-mod]/page.tsx
Normal file
122
apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/modification/custom/[custom-mod]/page.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
* All external content is rather licensed under the ECA Agreement
|
||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||
*
|
||||
* All code under MHSF is licensed under the MIT License
|
||||
* by open source contributors
|
||||
*
|
||||
* Copyright (c) 2025 dvelo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { ModificationAction } from "@/components/feat/server-list/modification/modification-action";
|
||||
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||
import {
|
||||
Setting,
|
||||
SettingContent,
|
||||
SettingDescription,
|
||||
SettingMeta,
|
||||
SettingTitle,
|
||||
} from "@/components/feat/settings/setting";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Material } from "@/components/ui/material";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Link } from "@/components/util/link";
|
||||
import { serverModDB } from "@/config/sl-mod-db";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import { ArrowLeft, Filter, SortAsc } from "lucide-react";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { use } from "react";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
export default function ModificationPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ category: string; "custom-mod": string }>;
|
||||
}) {
|
||||
const { category, "custom-mod": mod } = use(params);
|
||||
const { user } = useUser();
|
||||
const [backRoute] = useQueryState("b", {
|
||||
defaultValue: "/servers/embedded/sl-modification-frame",
|
||||
});
|
||||
console.log(mod);
|
||||
const modObj = (
|
||||
(user?.unsafeMetadata
|
||||
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
|
||||
).find((c) => c.friendlyName === atob(decodeURIComponent(mod)));
|
||||
|
||||
if (modObj === undefined)
|
||||
return <>We couldn't find the modification you were looking for.</>;
|
||||
|
||||
return (
|
||||
<main className="max-w-[800px] p-4">
|
||||
<div
|
||||
className="h-[150px] w-full rounded-xl p-2"
|
||||
style={{ backgroundColor: modObj?.color }}
|
||||
>
|
||||
<Link href={backRoute}>
|
||||
<ArrowLeft />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<span className="p-4">
|
||||
<h1 className="text-xl font-bold w-full">{modObj?.friendlyName}</h1>
|
||||
<Markdown className="text-wrap pt-2">
|
||||
This is a custom modification. Enable it! (or not) It's your own! (are
|
||||
you proud?)
|
||||
</Markdown>
|
||||
<Button className="mt-2">
|
||||
{modObj?.active ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
|
||||
<Separator className="mt-3" />
|
||||
|
||||
<Material className="mt-6">
|
||||
<Setting>
|
||||
<SettingContent className="flex items-center">
|
||||
<SettingMeta>
|
||||
<SettingTitle>Type</SettingTitle>
|
||||
<SettingDescription>
|
||||
What type of modification is this?
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<div className="flex items-center">
|
||||
{modObj.testMode === "sort" ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<SortAsc size={16} />
|
||||
Sort
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<Filter size={16} /> Filter
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</Material>
|
||||
</span>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -31,12 +31,15 @@
|
||||
"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 { Material } from "@/components/ui/material";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Link } from "@/components/util/link";
|
||||
import { serverModDB } from "@/config/sl-mod-db";
|
||||
import { useRouter } from "@/lib/useRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { SignedIn, useUser } from "@clerk/nextjs";
|
||||
import { ArrowLeft, Binary } from "lucide-react";
|
||||
import { use } from "react";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
@ -45,9 +48,10 @@ export default function ServerListCategoryFrame({
|
||||
}: {
|
||||
params: Promise<{ category: string }>;
|
||||
}) {
|
||||
const { user } = useUser();
|
||||
const { category } = use(params);
|
||||
const categoryObj = serverModDB.find(
|
||||
(c) => c.displayTitle === atob(category)
|
||||
(c) => c.displayTitle === atob(category),
|
||||
);
|
||||
const router = useRouter();
|
||||
|
||||
@ -68,14 +72,14 @@ export default function ServerListCategoryFrame({
|
||||
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}`)}`
|
||||
`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`,
|
||||
)
|
||||
}
|
||||
key={m.name}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
||||
"w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center",
|
||||
)}
|
||||
style={{ backgroundColor: m.color }}
|
||||
>
|
||||
@ -86,6 +90,35 @@ export default function ServerListCategoryFrame({
|
||||
</span>
|
||||
</Material>
|
||||
))}
|
||||
<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>
|
||||
))}
|
||||
</SignedIn>
|
||||
</Material>
|
||||
</main>
|
||||
);
|
||||
|
||||
@ -2,9 +2,12 @@
|
||||
|
||||
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 type {
|
||||
ClerkCustomActivatedModification,
|
||||
ClerkCustomModification,
|
||||
} from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||
import { Link } from "@/components/util/link";
|
||||
import { AlertOctagon, ArrowLeft, ExternalLink } from "lucide-react";
|
||||
import { AlertOctagon, ArrowLeft, Check, ExternalLink } from "lucide-react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
@ -35,6 +38,31 @@ import { Geist_Mono } from "next/font/google";
|
||||
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";
|
||||
|
||||
const typeDefs = `// Hi :) how'd you get here?
|
||||
// Here, in return I'll provide you with a random number: ${Math.ceil(Math.random() * 100)}
|
||||
@ -110,6 +138,8 @@ 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);
|
||||
@ -125,13 +155,13 @@ export default function CustomFilePage({
|
||||
.getTypeScriptWorker()
|
||||
.then((worker) => {
|
||||
worker(
|
||||
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
|
||||
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri,
|
||||
).then((client) => {
|
||||
client
|
||||
.getSemanticDiagnostics(
|
||||
(
|
||||
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
|
||||
).toString()
|
||||
).toString(),
|
||||
)
|
||||
.then((diags) => {
|
||||
setSyntaxErrors(diags);
|
||||
@ -158,6 +188,7 @@ export default function CustomFilePage({
|
||||
|
||||
await user?.update({
|
||||
unsafeMetadata: {
|
||||
...user.unsafeMetadata,
|
||||
customFiles: metadata,
|
||||
},
|
||||
});
|
||||
@ -166,7 +197,7 @@ export default function CustomFilePage({
|
||||
const lintFile = async () => {
|
||||
toast.info("Transpiling TypeScript...");
|
||||
const { error, data: transpiledCode } = await tryCatch(
|
||||
(async () => transpileTypeScript(value))()
|
||||
(async () => transpileTypeScript(value))(),
|
||||
);
|
||||
if (error) {
|
||||
toast.error("Failed to transpile TypeScript! Error: " + error.message);
|
||||
@ -177,13 +208,12 @@ export default function CustomFilePage({
|
||||
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]*)\}/
|
||||
/function\s+filter\s*\([^)]*\)\s*\{([\s\S]*)\}/,
|
||||
)?.[1];
|
||||
const { error: filterErr, data: filterFunc } = await tryCatch(
|
||||
(async () => new Function("data", functionBody as string))()
|
||||
(async () => new Function("data", functionBody as string))(),
|
||||
);
|
||||
if (filterErr) {
|
||||
toast.error(`Failed to generate function! Error: ${filterErr.message}`);
|
||||
@ -202,7 +232,7 @@ export default function CustomFilePage({
|
||||
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."
|
||||
"Whoa! We encountered an error while auto-saving. Please copy your code locally to ensure you'll keep your code changes.",
|
||||
);
|
||||
}, 300);
|
||||
|
||||
@ -301,25 +331,272 @@ export default function CustomFilePage({
|
||||
</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={() => {
|
||||
const t = btoa(value);
|
||||
|
||||
const newTab = window.open(`/servers?tm=${encodeURIComponent(t)}`)
|
||||
const interval = setInterval(() => {
|
||||
newTab?.dispatchEvent(new Event("test-mode.enable"))
|
||||
}, 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.")
|
||||
})
|
||||
}}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
<Drawer
|
||||
direction="right"
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DrawerContent className="p-4 min-w-[400px] overflow-x-hidden max-h-screen overflow-y-auto">
|
||||
<p className="text-sm mb-2">
|
||||
You can run an interactive server-list environment
|
||||
with actual online servers to test your modifications.
|
||||
</p>
|
||||
|
||||
<Material>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Function to test</SettingTitle>
|
||||
<SettingDescription>
|
||||
You can pick to either test a sorting system
|
||||
or a filter.
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Select
|
||||
value={testMode}
|
||||
onValueChange={setTestMode}
|
||||
disabled={success}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
value="filter"
|
||||
disabled={!filterEnabled}
|
||||
>
|
||||
<code>filter</code>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="sort"
|
||||
disabled={!sortEnabled}
|
||||
>
|
||||
<code>sort</code>
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</Material>
|
||||
|
||||
<Button
|
||||
className="w-full mt-2 flex items-center gap-2"
|
||||
disabled={testMode === "" || success}
|
||||
variant={success ? "success-subtle" : "default"}
|
||||
onClick={() => {
|
||||
const t = btoa(value);
|
||||
|
||||
const newTab = window.open(
|
||||
`/servers?tm=${encodeURIComponent(t)}`,
|
||||
);
|
||||
const interval = setInterval(() => {
|
||||
newTab?.dispatchEvent(
|
||||
new Event("test-mode.enable." + testMode),
|
||||
);
|
||||
}, 500);
|
||||
toast.info(
|
||||
"Waiting for server tab to pick up thread...",
|
||||
);
|
||||
|
||||
newTab?.addEventListener(
|
||||
"test-mode.enabled",
|
||||
() => {
|
||||
clearInterval(interval);
|
||||
toast.success(
|
||||
"Connected to new tab; continue.",
|
||||
);
|
||||
newTab?.addEventListener(
|
||||
"test-mode.success",
|
||||
() => {
|
||||
toast.success(
|
||||
"Resolved success from thread!",
|
||||
);
|
||||
setSuccess(true);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
{success && <Check size={16} />}Test
|
||||
</Button>
|
||||
{success && (
|
||||
<>
|
||||
<p className="text-sm my-2">
|
||||
You can now activate this custom modification.
|
||||
Please note that the filter and sort versions of
|
||||
your modifications will be different, and the one
|
||||
used will be selected based on what type you
|
||||
tested on.
|
||||
</p>
|
||||
{(
|
||||
(user?.unsafeMetadata
|
||||
.activatedModifications as ClerkCustomActivatedModification[]) ??
|
||||
[]
|
||||
).find((c) => c.originalFileName === filename && c.testMode === testMode) !==
|
||||
undefined && (
|
||||
<Alert className="mb-2 gap-2" variant="warning">
|
||||
This modification was already activated! Hitting
|
||||
activate here will just overwrite the contents
|
||||
and the new friendly name.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Material>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Name</SettingTitle>
|
||||
<SettingDescription>
|
||||
Set a friendly name for your modification.
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Input
|
||||
placeholder="My cool mod"
|
||||
value={fileName}
|
||||
onChange={(c) =>
|
||||
setFileName(c.target.value)
|
||||
}
|
||||
/>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</Material>
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
className="w-full my-2"
|
||||
disabled={fileName === ""}
|
||||
onClick={async () => {
|
||||
const array =
|
||||
(user?.unsafeMetadata
|
||||
.activatedModifications as ClerkCustomActivatedModification[]) ??
|
||||
[];
|
||||
const index = array.findIndex(
|
||||
(c) => c.originalFileName === filename && c.testMode === testMode,
|
||||
);
|
||||
const color = '#' + Math.floor(Math.random() * 16777215).toString(16);
|
||||
const transpiledValue =
|
||||
transpileTypeScript(value);
|
||||
|
||||
if (transpiledValue === null)
|
||||
return toast.error("Error transpiling");
|
||||
|
||||
if (index !== -1) {
|
||||
// Original already exists
|
||||
array[index] = {
|
||||
originalFileName: filename,
|
||||
// I'm too lazy to change this
|
||||
friendlyName: fileName,
|
||||
transpiledContents: transpiledValue,
|
||||
active: true,
|
||||
testMode: testMode as "filter" | "sort",
|
||||
color
|
||||
};
|
||||
} else {
|
||||
array.push({
|
||||
originalFileName: filename,
|
||||
// ... and this too
|
||||
friendlyName: fileName,
|
||||
transpiledContents: transpiledValue,
|
||||
active: true,
|
||||
testMode: testMode as "filter" | "sort",
|
||||
color
|
||||
});
|
||||
}
|
||||
|
||||
await user?.update({
|
||||
unsafeMetadata: {
|
||||
...user.unsafeMetadata,
|
||||
activatedModifications: array,
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("Activated!")
|
||||
router.push("/servers/embedded/sl-modification-frame")
|
||||
}}
|
||||
>
|
||||
Activate
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</>
|
||||
)}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{successfullyLinted
|
||||
@ -361,7 +638,7 @@ export default function CustomFilePage({
|
||||
// Add typedefs as a library
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
typeDefs,
|
||||
libUri
|
||||
libUri,
|
||||
);
|
||||
|
||||
// Add actions
|
||||
@ -377,7 +654,7 @@ export default function CustomFilePage({
|
||||
{
|
||||
id: "lint-file",
|
||||
label: "MHSF: Lint File",
|
||||
run: lintFile
|
||||
run: lintFile,
|
||||
},
|
||||
].forEach((e) => editor.addAction(e));
|
||||
|
||||
@ -386,7 +663,7 @@ export default function CustomFilePage({
|
||||
monaco.editor.createModel(
|
||||
typeDefs,
|
||||
"typescript",
|
||||
monaco.Uri.parse(libUri)
|
||||
monaco.Uri.parse(libUri),
|
||||
);
|
||||
}
|
||||
|
||||
@ -433,6 +710,53 @@ export default function CustomFilePage({
|
||||
);
|
||||
}
|
||||
|
||||
export async function findSupportedOperations(
|
||||
fileValue: string,
|
||||
): Promise<{ filter: boolean; sort: boolean }> {
|
||||
const returnValue = { filter: true, sort: true };
|
||||
const transpiledValue = transpileTypeScript(fileValue);
|
||||
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) returnValue.filter = false;
|
||||
if (sortErr) returnValue.sort = false;
|
||||
|
||||
try {
|
||||
filterFunc?.({});
|
||||
} catch (e) {
|
||||
if (String(e).startsWith("ReferenceError: filter is not defined")) {
|
||||
returnValue.filter = false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
sortFunc?.({}, {});
|
||||
} catch (e) {
|
||||
if (String(e).startsWith("ReferenceError: sort is not defined")) {
|
||||
returnValue.sort = false;
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
function guidGenerator() {
|
||||
const S4 = () => {
|
||||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||
|
||||
@ -47,15 +47,21 @@ import {
|
||||
Braces,
|
||||
EllipsisVertical,
|
||||
FileCode,
|
||||
Filter,
|
||||
Pencil,
|
||||
SortAsc,
|
||||
Trash,
|
||||
} from "lucide-react";
|
||||
import { use } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { findSupportedOperations } from "../file/[filename]/page";
|
||||
|
||||
export default function ServerListModificationFrame() {
|
||||
const { user } = useUser();
|
||||
const files =
|
||||
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [];
|
||||
const operations = use((async () => await Promise.all(files.map(async (c) => await findSupportedOperations(c.contents))))())
|
||||
console.log(operations)
|
||||
return (
|
||||
<main className="max-w-[800px] p-4">
|
||||
<h1 className="text-xl font-bold w-full flex items-center gap-2">
|
||||
@ -80,6 +86,8 @@ 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}/>}
|
||||
{c.name}.ts
|
||||
</span>
|
||||
<span>
|
||||
@ -105,6 +113,7 @@ export default function ServerListModificationFrame() {
|
||||
files.splice(i, 1);
|
||||
await user?.update({
|
||||
unsafeMetadata: {
|
||||
...user.unsafeMetadata,
|
||||
customFiles: files,
|
||||
},
|
||||
});
|
||||
|
||||
@ -34,12 +34,15 @@ import { Material } from "@/components/ui/material";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Link } from "@/components/util/link";
|
||||
import { serverModDB } from "@/config/sl-mod-db";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { ArrowRight, Binary } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "@/lib/useRouter";
|
||||
import { SignedIn, useUser } from "@clerk/nextjs";
|
||||
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||
|
||||
export default function ServerListModificationFrame() {
|
||||
const router = useRouter();
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<main className="max-w-[800px] p-4">
|
||||
@ -58,7 +61,7 @@ export default function ServerListModificationFrame() {
|
||||
</span>
|
||||
<Material className="mt-10 p-4">
|
||||
{serverModDB.map((c) => (
|
||||
<span key={c.displayTitle}>
|
||||
<div key={c.displayTitle} className="my-4">
|
||||
<h2 className="text-lg font-bold pb-3 flex justify-between">
|
||||
{c.displayTitle}
|
||||
<Link
|
||||
@ -77,7 +80,7 @@ export default function ServerListModificationFrame() {
|
||||
key={m.name}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}/modification/${btoa(m.name)}`
|
||||
`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}/modification/${btoa(m.name)}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
@ -92,8 +95,37 @@ export default function ServerListModificationFrame() {
|
||||
</span>
|
||||
</Material>
|
||||
))}
|
||||
<SignedIn>
|
||||
{c.__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/${btoa(c.displayTitle)}/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>
|
||||
))}
|
||||
</SignedIn>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Material>
|
||||
</main>
|
||||
|
||||
49
apps/www/src/app/api/trpc/[trpc]/route.ts
Normal file
49
apps/www/src/app/api/trpc/[trpc]/route.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 { 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";
|
||||
|
||||
const handler = (request: NextRequest) => {
|
||||
const {userId} = getAuth(request);
|
||||
|
||||
return fetchRequestHandler({
|
||||
endpoint: "/api/trpc",
|
||||
req: request,
|
||||
router: appRouter,
|
||||
createContext: createContext(userId),
|
||||
});
|
||||
};
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
@ -236,7 +236,7 @@
|
||||
|
||||
.loading-shimmer {
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation-duration: 2.5s;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: loading-shimmer;
|
||||
background: var(--color-muted-foreground)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { BrandingGenericIcon } from "../icons/branding-icons";
|
||||
import { Link } from "../../util/link";
|
||||
import { FooterStatus } from "./status";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
@ -35,6 +36,29 @@ export function Footer() {
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
<li className="text-sm">
|
||||
<DropdownMenu
|
||||
|
||||
>
|
||||
<DropdownMenuTrigger>
|
||||
<a className="text-muted-foreground hover:text-shadcn-primary transition-colors cursor-pointer">Discord</a>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<Link href="https://t.mhsf.app/d/m" noExtraIcons>
|
||||
<DropdownMenuItem className="block py-2">
|
||||
Minehut Discord
|
||||
<small className="flex">Not officially owned by MHSF, however conversations about MHSF and related take place there.</small>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href="https://t.mhsf.app/d/u" noExtraIcons>
|
||||
<DropdownMenuItem className="block py-2">
|
||||
MHSF Discord
|
||||
<small className="flex">A read-only server for updates related to MHSF.</small>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<FooterStatus />
|
||||
|
||||
@ -63,6 +63,15 @@ export type ClerkCustomModification = {
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
export type ClerkCustomActivatedModification = {
|
||||
originalFileName: string;
|
||||
friendlyName: string;
|
||||
transpiledContents: string;
|
||||
active: boolean;
|
||||
testMode: "filter" | "sort";
|
||||
color: string;
|
||||
}
|
||||
|
||||
export function ModificationFileCreationDialog({
|
||||
children,
|
||||
type,
|
||||
@ -98,10 +107,11 @@ export function ModificationFileCreationDialog({
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={(e) => {
|
||||
onClick={async (e) => {
|
||||
if (!isSignedIn) return toast.error("Please login.");
|
||||
user?.update({
|
||||
await user?.update({
|
||||
unsafeMetadata: {
|
||||
...user.unsafeMetadata,
|
||||
customFiles: [
|
||||
...((user.unsafeMetadata
|
||||
.customFiles as Array<ClerkCustomModification>) ?? []),
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { AnimatedText } from "@/components/ui/animated-text";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useQueryState } from "nuqs";
|
||||
|
||||
export function ServerTestModeSelector({
|
||||
@ -19,15 +24,26 @@ export function ServerTestModeSelector({
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="relative flex size-2.5 pt-[1px] items-center cursor-pointer">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-orange-400 opacity-75" />
|
||||
<span className="relative inline-flex size-2.5 rounded-full bg-orange-500" />
|
||||
</span></TooltipTrigger>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75",
|
||||
testModeLoading ? "bg-orange-500" : "bg-green-400"
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"relative inline-flex size-2.5 rounded-full",
|
||||
testModeLoading ? "bg-orange-500" : "bg-green-500"
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Test mode was enabled.</TooltipContent>
|
||||
</Tooltip>
|
||||
<AnimatedText
|
||||
className="text-muted-foreground top-[2.5px] left-[6px] min-w-[70vw]"
|
||||
text={testModeStatus}
|
||||
glimmer
|
||||
glimmer={testModeLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -38,14 +38,12 @@ import { MultisessionAppSupport } from "@clerk/nextjs/internal";
|
||||
export const ClerkProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
if (resolvedTheme === "dark") {
|
||||
console.log(resolvedTheme);
|
||||
return (
|
||||
<ImportedClerkProvider appearance={{ baseTheme: dark }}>
|
||||
<MultisessionAppSupport>{children}</MultisessionAppSupport>
|
||||
</ImportedClerkProvider>
|
||||
);
|
||||
}
|
||||
console.log("a");
|
||||
|
||||
return (
|
||||
<ImportedClerkProvider>
|
||||
|
||||
@ -51,8 +51,7 @@ type ModDBCategory = {
|
||||
|
||||
export const serverModDB: ModDBCategory[] = [
|
||||
{
|
||||
displayTitle: "Custom Files",
|
||||
__custom: true,
|
||||
displayTitle: "Create Custom Files",
|
||||
description:
|
||||
`Create custom TypeScript-based filter or sorting systems, completely from the comfort of your own browser.
|
||||
Types used are *builtin* and you can see live type definitions and IntelliSense in the editor.`,
|
||||
@ -73,4 +72,11 @@ export const serverModDB: ModDBCategory[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayTitle: "Custom Files",
|
||||
description: "These are all of your activated modifications made in the editor.",
|
||||
__custom: true,
|
||||
// Entries are already pre-loaded.
|
||||
entries: []
|
||||
}
|
||||
];
|
||||
|
||||
92
apps/www/src/lib/backend-procedure.ts
Normal file
92
apps/www/src/lib/backend-procedure.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
* All external content is rather licensed under the ECA Agreement
|
||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||
*
|
||||
* All code under MHSF is licensed under the MIT License
|
||||
* by open source contributors
|
||||
*
|
||||
* Copyright (c) 2025 dvelo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { clerkClient as ClerkClient, getAuth } from "@clerk/nextjs/server";
|
||||
import { Db, MongoClient } from "mongodb";
|
||||
import { NextApiRequest } from "next";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import requestIp from 'request-ip'
|
||||
|
||||
type BackendProcedureValue = {
|
||||
status: "BANNED" | "OK" | "BLOCKED",
|
||||
allowed: boolean,
|
||||
mongoClient?: MongoClient,
|
||||
defaultDatabase?: Db
|
||||
}
|
||||
|
||||
export async function getBackendProcedure(request: NextApiRequest): Promise<BackendProcedureValue> {
|
||||
const mongoClient = new MongoClient(process.env.MONGO_DB as string);
|
||||
const {userId} = getAuth(request)
|
||||
await mongoClient.connect();
|
||||
const defaultDatabase = mongoClient.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const clerkClient = await ClerkClient();
|
||||
|
||||
if (userId !== null) {
|
||||
// User exists
|
||||
const user = await clerkClient.users.getUser(userId);
|
||||
const userBannedMetadata = user.publicMetadata.banned;
|
||||
|
||||
if (userBannedMetadata !== undefined) {
|
||||
// User is banned
|
||||
await mongoClient.close()
|
||||
return {
|
||||
status: "BANNED",
|
||||
allowed: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const detectedIp = requestIp.getClientIp(request);
|
||||
|
||||
if (detectedIp !== null) {
|
||||
const collection = defaultDatabase.collection("blocked-ips");
|
||||
console.log(await collection.findOne({ ip: detectedIp }), detectedIp)
|
||||
|
||||
if (await collection.findOne({ ip: detectedIp }) !== null) {
|
||||
await mongoClient.close()
|
||||
return { status: "BLOCKED", allowed: false }
|
||||
}
|
||||
}
|
||||
|
||||
await mongoClient.close()
|
||||
|
||||
return {
|
||||
status: "OK",
|
||||
allowed: true,
|
||||
}
|
||||
}
|
||||
|
||||
function convert(request: Headers) {
|
||||
const headersObject: Record<string, string> = {};
|
||||
for (const [key, value] of request) {
|
||||
headersObject[key] = value;
|
||||
}
|
||||
return headersObject;
|
||||
}
|
||||
@ -39,16 +39,16 @@ export function useFilters(data: OnlineServer[]) {
|
||||
const [filteredData, setFilteredData] = useState<OnlineServer[]>(data);
|
||||
const [t] = useQueryState("tm");
|
||||
const [testModeEnabled, setTestModeEnabled] = useState(false);
|
||||
const [testModeStatus, setTestModeStatus] = useState("Haven't connected thread yet (if stuck, select the other tab, and come back)");
|
||||
const [testModeStatus, setTestModeStatus] = useState(
|
||||
"Haven't connected thread yet (if stuck, select the other tab, and come back)",
|
||||
);
|
||||
const [testModeLoading, setTestModeLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (filteredData.length === 0) setFilteredData(data);
|
||||
}, [data, filteredData.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length !== 0)
|
||||
window.addEventListener("test-mode.enable", (c) => {
|
||||
const testModeInit = (type: "filter" | "sort") => {
|
||||
window.dispatchEvent(new Event("test-mode.enabled"));
|
||||
if (!t) {
|
||||
toast.error("Couldn't enable test mode; no query variable.");
|
||||
@ -59,11 +59,11 @@ export function useFilters(data: OnlineServer[]) {
|
||||
setTestModeStatus("Transpiling TypeScript...");
|
||||
const startTime = Date.now();
|
||||
const { error, data: transpiledCode } = await tryCatch(
|
||||
(async () => transpileTypeScript(code))()
|
||||
(async () => transpileTypeScript(code))(),
|
||||
);
|
||||
if (error) {
|
||||
setTestModeStatus(
|
||||
"Failed to transpile TypeScript! Error: " + error.message
|
||||
"Failed to transpile TypeScript! Error: " + error.message,
|
||||
);
|
||||
setTestModeLoading(false);
|
||||
return;
|
||||
@ -73,17 +73,13 @@ export function useFilters(data: OnlineServer[]) {
|
||||
setTestModeLoading(false);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
"[MHSF Filters] Transpiled TypeScript:",
|
||||
transpiledCode ?? ""
|
||||
);
|
||||
setTestModeStatus("Generating function...");
|
||||
if (
|
||||
!transpiledCode.includes("export default") &&
|
||||
!transpiledCode.includes("export")
|
||||
) {
|
||||
setTestModeStatus(
|
||||
"Transpiled code does not contain any export statements."
|
||||
"Transpiled code does not contain any export statements.",
|
||||
);
|
||||
setTestModeLoading(false);
|
||||
return;
|
||||
@ -93,54 +89,82 @@ export function useFilters(data: OnlineServer[]) {
|
||||
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
|
||||
const { error: filterErr, data: filterFunc } = await tryCatch(
|
||||
(async () =>
|
||||
new Function(
|
||||
type === "filter"
|
||||
? new Function(
|
||||
"server",
|
||||
`${functionBody}
|
||||
|
||||
return filter(server)`
|
||||
))()
|
||||
return filter(server)`,
|
||||
)
|
||||
: new Function(
|
||||
"serverA",
|
||||
"serverB",
|
||||
`${functionBody}
|
||||
|
||||
return sort(serverA, serverB)`,
|
||||
))(),
|
||||
);
|
||||
if (filterErr) {
|
||||
setTestModeStatus(
|
||||
`Failed to generate function! Error: ${filterErr.message}`
|
||||
`Failed to generate function! Error: ${filterErr.message}`,
|
||||
);
|
||||
setTestModeLoading(false);
|
||||
return;
|
||||
}
|
||||
if (typeof filterFunc === "function") {
|
||||
setTestModeStatus(
|
||||
"Compiled in " + (Date.now() - startTime) + "ms"
|
||||
);
|
||||
setTestModeStatus("Compiled in " + (Date.now() - startTime) + "ms");
|
||||
toast.promise(
|
||||
async () => {
|
||||
let newServers = [];
|
||||
if (type === "filter") {
|
||||
newServers = data.filter((c) => filterFunc(c));
|
||||
setTestModeStatus(
|
||||
"Server count " + data.length + " -> " + newServers.length
|
||||
"Server count " + data.length + " -> " + newServers.length,
|
||||
);
|
||||
if (newServers.length === 0)
|
||||
setTestModeStatus(
|
||||
"No servers were specified in the criteria; showing all servers instead",
|
||||
);
|
||||
setFilteredData(() => [...newServers]);
|
||||
}
|
||||
if (type === "sort") {
|
||||
newServers = data.sort((a, b) => filterFunc(a, b));
|
||||
setTestModeStatus("Sorted " + newServers.length + " servers.");
|
||||
console.log(newServers, data.sort((a, b) => filterFunc(a, b)))
|
||||
console.log(filterFunc)
|
||||
setFilteredData(() => [...newServers]);
|
||||
}
|
||||
|
||||
setTestModeLoading(false);
|
||||
window.dispatchEvent(new Event("test-mode.success"));
|
||||
},
|
||||
{
|
||||
loading: "Manipulating data...",
|
||||
success: "Manipulated data; test mode finished!",
|
||||
error: (e) =>
|
||||
`Error while manipulating data; go back to your editor and run again. ${e}`,
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
setTestModeStatus(
|
||||
"Code doesn't have a 'filter' function. Cannot be tested."
|
||||
"Code doesn't have a 'filter' function. Cannot be tested.",
|
||||
);
|
||||
setTestModeLoading(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
});
|
||||
}, [t, data]);
|
||||
};
|
||||
|
||||
console.log(filteredData, testModeStatus);
|
||||
useEffect(() => {
|
||||
if (data.length !== 0) {
|
||||
window.addEventListener("test-mode.enable.filter", () =>
|
||||
testModeInit("filter"),
|
||||
);
|
||||
window.addEventListener("test-mode.enable.sort", () =>
|
||||
testModeInit("sort"),
|
||||
);
|
||||
}
|
||||
}, [t, data]);
|
||||
|
||||
return { filteredData, testModeEnabled, testModeLoading, testModeStatus };
|
||||
}
|
||||
|
||||
@ -53,7 +53,6 @@ export function useIframeCommunication(bottomIframe?: RefObject<HTMLIFrameElemen
|
||||
},
|
||||
handle: (key: string, callback: (object: any) => void) => {
|
||||
window.addEventListener('message', (e) => {
|
||||
console.log(e);
|
||||
if (e.data.__key === key) {
|
||||
callback(e.data)
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
} from "@clerk/nextjs/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import type { ServerResponse } from "./lib/types/mh-server";
|
||||
import { getBackendProcedure } from "./lib/backend-procedure";
|
||||
|
||||
// Thanks for the router matcher API Clerk <3
|
||||
const isRootRoute = createRouteMatcher(["/"]);
|
||||
@ -57,22 +58,6 @@ export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
|
||||
return NextResponse.redirect(new URL("/home", req.url));
|
||||
}
|
||||
}
|
||||
// If user is banned, disable all API routes
|
||||
if (authRes.userId !== null) {
|
||||
// User exists
|
||||
const user = await client.users.getUser(authRes.userId);
|
||||
const userBannedMetadata = user.publicMetadata.banned;
|
||||
|
||||
if (userBannedMetadata !== undefined) {
|
||||
// User is banned
|
||||
if (apiRoute(req)) {
|
||||
return NextResponse.json({
|
||||
banned:
|
||||
"You were banned. (and I'm not telling you why) Why are you trying to use the API. Huh? Tell me. Now. You're not funny.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isOldServerRoute(req)) {
|
||||
const minehut = await fetch(
|
||||
`https://api.minehut.com/server/${req.url.split("/server/")[1].split("/")[0]}?byName=true`,
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { getBackendProcedure } from "@/lib/backend-procedure";
|
||||
import type { MHSFData } from "@/lib/types/data";
|
||||
import { MongoClient } from "mongodb";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
@ -49,8 +50,16 @@ export type RouteParams = {
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<{ server: (MHSFData & RouteParams) | null }>
|
||||
res: NextApiResponse<
|
||||
{ server: (MHSFData & RouteParams) | null } | { error: string }
|
||||
>,
|
||||
) {
|
||||
const backendProcedure = await getBackendProcedure(req);
|
||||
|
||||
if (backendProcedure.status !== "OK")
|
||||
return res.status(403).json({
|
||||
error: `Backend procedure marked request as '${backendProcedure.status}' instead of required 'OK'`,
|
||||
});
|
||||
const {
|
||||
server,
|
||||
maxFavoriteEntries,
|
||||
@ -127,7 +136,7 @@ export default async function handler(
|
||||
async function findCustomizationData(
|
||||
serverName: string,
|
||||
userId: string | undefined,
|
||||
db: any
|
||||
db: any,
|
||||
): Promise<{
|
||||
description: string | undefined;
|
||||
banner: string | undefined;
|
||||
@ -172,7 +181,7 @@ async function findFavoriteData(
|
||||
maxFavoriteEntries?: string | string[];
|
||||
favoriteTimespanStart?: string | string[];
|
||||
favoriteTimespanEnd?: string | string[];
|
||||
}
|
||||
},
|
||||
) {
|
||||
// Run queries in parallel
|
||||
const [userFavorites, metaData, historyData] = await Promise.all([
|
||||
@ -204,7 +213,7 @@ async function fetchHistoryData(
|
||||
maxFavoriteEntries?: string | string[];
|
||||
favoriteTimespanStart?: string | string[];
|
||||
favoriteTimespanEnd?: string | string[];
|
||||
}
|
||||
},
|
||||
) {
|
||||
// Build query filter
|
||||
const filter: any = { server: serverName };
|
||||
@ -235,7 +244,7 @@ async function fetchHistoryData(
|
||||
}
|
||||
|
||||
async function findServerData(
|
||||
server: string
|
||||
server: string,
|
||||
): Promise<{ exists: boolean; name: string }> {
|
||||
try {
|
||||
const response = await fetch("https://api.minehut.com/server/" + server);
|
||||
@ -262,7 +271,7 @@ async function findPlayerData(
|
||||
maxPlayerEntries?: string | string[];
|
||||
playerTimespanStart?: string | string[];
|
||||
playerTimespanEnd?: string | string[];
|
||||
}
|
||||
},
|
||||
) {
|
||||
// Get historical player data
|
||||
const historyCollection = db.collection("history");
|
||||
@ -304,7 +313,7 @@ async function findPlayerData(
|
||||
(item: { date: string; player_count?: number }) => ({
|
||||
date: item.date,
|
||||
playerCount: item.player_count || 0,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const max = maxResult.length > 0 ? maxResult[0].player_count : 0;
|
||||
@ -319,7 +328,7 @@ async function findAchievements(
|
||||
maxAchievementEntries?: string | string[];
|
||||
achievementTimespanStart?: string | string[];
|
||||
achievementTimespanEnd?: string | string[];
|
||||
}
|
||||
},
|
||||
) {
|
||||
// Get achievements data
|
||||
const achievementsCollection = db.collection("achievements");
|
||||
@ -348,7 +357,7 @@ async function findAchievements(
|
||||
const currently: any[] = [];
|
||||
for (const a of historically)
|
||||
a.achievements.forEach((item: any, interval: number) =>
|
||||
currently.push({ interval, ...item })
|
||||
currently.push({ interval, ...item }),
|
||||
);
|
||||
|
||||
return { historically, currently };
|
||||
|
||||
@ -33,11 +33,18 @@ import { clerkClient, getAuth } from "@clerk/nextjs/server";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { OnlineServer } from "@/lib/types/mh-server";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
import { getBackendProcedure } from "@/lib/backend-procedure";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const backendProcedure = await getBackendProcedure(req);
|
||||
|
||||
if (backendProcedure.status !== "OK")
|
||||
return res.status(403).json({
|
||||
error: `Backend procedure marked request as '${backendProcedure.status}' instead of required 'OK'`,
|
||||
});
|
||||
const { userId } = getAuth(req);
|
||||
const { server } = req.query;
|
||||
|
||||
|
||||
71
yarn.lock
71
yarn.lock
@ -3223,11 +3223,43 @@
|
||||
lodash.merge "^4.6.2"
|
||||
postcss-selector-parser "6.0.10"
|
||||
|
||||
"@tanstack/query-core@5.69.0":
|
||||
version "5.69.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.69.0.tgz#c434505987ade936dc53e6e27aa1406b0295516f"
|
||||
integrity sha512-Kn410jq6vs1P8Nm+ZsRj9H+U3C0kjuEkYLxbiCyn3MDEiYor1j2DGVULqAz62SLZtUZ/e9Xt6xMXiJ3NJ65WyQ==
|
||||
|
||||
"@tanstack/react-query@^5.69.0":
|
||||
version "5.69.0"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.69.0.tgz#8d58e800854cc11d0aa2c39569f53ae32ba442a9"
|
||||
integrity sha512-Ift3IUNQqTcaFa1AiIQ7WCb/PPy8aexZdq9pZWLXhfLcLxH0+PZqJ2xFImxCpdDZrFRZhLJrh76geevS5xjRhA==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.69.0"
|
||||
|
||||
"@tootallnate/quickjs-emscripten@^0.23.0":
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
|
||||
integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
|
||||
|
||||
"@trpc/client@^11.0.0":
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/client/-/client-11.0.0.tgz#3020392edf87abc046594cee0acd379f2c6289d9"
|
||||
integrity sha512-U2THlxsdr4ykAX5lpTU8k5WRADPQ+68Ex2gfUht3MlCxGK7njBmNSSzjpQSWNt7tMI/xsYrddFiRlmEPrh+Cbg==
|
||||
|
||||
"@trpc/next@^11.0.0":
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/next/-/next-11.0.0.tgz#d83e1fac595629c3bc02ba5bbe2595508b7c55ee"
|
||||
integrity sha512-HpowgsF0jfXG30jEBVK8v90ltbEZiQZq/x0rsjScfZuedkAfapqZvrsrkzv6Pkemz7sxaxJcZB3HEqXxWfkGoA==
|
||||
|
||||
"@trpc/react-query@^11.0.0":
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/react-query/-/react-query-11.0.0.tgz#6fb849baf715fb33d2eb6a417d7e75fc00307ae6"
|
||||
integrity sha512-HeE9bBLA6nqC2xk5wlNZIPQ5vmyli3tgNNab8fTE489+ksNMKxaIx66pZKsMJIorDcP1wS0rWNV+GroU0iR98g==
|
||||
|
||||
"@trpc/server@^11.0.0":
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@trpc/server/-/server-11.0.0.tgz#5a49758fa3c052a83314c328155d68027164f077"
|
||||
integrity sha512-xY9q/b/wR/tWGYTm5xmRjivkYD2EZZXmOKmHuNJRYZuLbieeNUsdfQRjJC409WB1pjKWInomhHwuA8bahZJ4lQ==
|
||||
|
||||
"@types/acorn@^4.0.0":
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22"
|
||||
@ -3491,7 +3523,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
|
||||
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
||||
|
||||
"@types/react-dom@^19", "@types/react-dom@^19.0.3":
|
||||
"@types/react-dom@19.0.4", "@types/react-dom@^19", "@types/react-dom@^19.0.3":
|
||||
version "19.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89"
|
||||
integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==
|
||||
@ -3503,13 +3535,20 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^19", "@types/react@^19.0.8":
|
||||
"@types/react@*", "@types/react@19.0.10", "@types/react@^19", "@types/react@^19.0.8":
|
||||
version "19.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb"
|
||||
integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/request-ip@^0.0.41":
|
||||
version "0.0.41"
|
||||
resolved "https://registry.yarnpkg.com/@types/request-ip/-/request-ip-0.0.41.tgz#c22a3244df2573402989346062851b06b7a5ac4e"
|
||||
integrity sha512-Qzz0PM2nSZej4lsLzzNfADIORZhhxO7PED0fXpg4FjXiHuJ/lMyUg+YFF5q8x9HPZH3Gl6N+NOM8QZjItNgGKg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/resolve@^1.17.1":
|
||||
version "1.20.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8"
|
||||
@ -4764,6 +4803,13 @@ cookie@~0.7.2:
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
|
||||
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
|
||||
|
||||
copy-anything@^3.0.2:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0"
|
||||
integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==
|
||||
dependencies:
|
||||
is-what "^4.1.8"
|
||||
|
||||
core-util-is@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||
@ -7637,6 +7683,11 @@ is-weakset@^2.0.3:
|
||||
call-bound "^1.0.3"
|
||||
get-intrinsic "^1.2.6"
|
||||
|
||||
is-what@^4.1.8:
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f"
|
||||
integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==
|
||||
|
||||
is-wsl@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
|
||||
@ -10877,6 +10928,11 @@ repeat-string@^1.6.1:
|
||||
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
|
||||
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
|
||||
|
||||
request-ip@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/request-ip/-/request-ip-3.3.0.tgz#863451e8fec03847d44f223e30a5d63e369fa611"
|
||||
integrity sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==
|
||||
|
||||
require-directory@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
@ -11677,6 +11733,13 @@ sucrase@^3.32.0, sucrase@^3.35.0:
|
||||
pirates "^4.0.1"
|
||||
ts-interface-checker "^0.1.9"
|
||||
|
||||
superjson@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.2.tgz#9d52bf0bf6b5751a3c3472f1292e714782ba3173"
|
||||
integrity sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==
|
||||
dependencies:
|
||||
copy-anything "^3.0.2"
|
||||
|
||||
supports-color@^7.1.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||
@ -12042,7 +12105,7 @@ turbo-windows-arm64@2.4.4:
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.4.4.tgz#e00c26e3d7fd9a82af90018ad3137f14e5221630"
|
||||
integrity sha512-403sqp9t5sx6YGEC32IfZTVWkRAixOQomGYB8kEc6ZD+//LirSxzeCHCnM8EmSXw7l57U1G+Fb0kxgTcKPU/Lg==
|
||||
|
||||
turbo@^2.4.0, turbo@^2.4.2:
|
||||
turbo@^2.4.0, turbo@^2.4.4:
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.4.4.tgz#cec5dbac5850adebdba71fbdf90e6e9a7723c3d6"
|
||||
integrity sha512-N9FDOVaY3yz0YCOhYIgOGYad7+m2ptvinXygw27WPLQvcZDl3+0Sa77KGVlLSiuPDChOUEnTKE9VJwLSi9BPGQ==
|
||||
@ -12867,7 +12930,7 @@ zod@3.23.8:
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
|
||||
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
||||
|
||||
zod@^3.20.6, zod@^3.21.4, zod@^3.23.8, zod@^3.24.1:
|
||||
zod@^3.20.6, zod@^3.21.4, zod@^3.24.1, zod@^3.24.2:
|
||||
version "3.24.2"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
|
||||
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user