mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-09 07:55:00 -05:00
Compare commits
3 Commits
bddf5f1528
...
d93693dc2d
| Author | SHA1 | Date | |
|---|---|---|---|
| d93693dc2d | |||
| db992412d0 | |||
| ed2d6f0ac1 |
@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
## The MHSF Project
|
## The MHSF Project
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> You are on the `v2` branch. Features may be broken as MHSF is getting rebuilt from the ground up.
|
||||||
|
|
||||||
A modern, third-party Minehut server list that is completely open-source, built using top of the line web technologies. Not built to take your money or time, find a server in minutes. Completely ad-free.
|
A modern, third-party Minehut server list that is completely open-source, built using top of the line web technologies. Not built to take your money or time, find a server in minutes. Completely ad-free.
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|||||||
@ -38,6 +38,10 @@ const nextConfig = {
|
|||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "img.clerk.com",
|
hostname: "img.clerk.com",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "avatars.githubusercontent.com"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "mh-stats",
|
"name": "mhsf",
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "yarn@1.22.22",
|
"packageManager": "yarn@1.22.22",
|
||||||
@ -15,7 +15,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.24.7",
|
"@babel/parser": "^7.24.7",
|
||||||
"@biomejs/biome": "^1.8.3",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@clerk/elements": "^0.22.2",
|
"@clerk/elements": "^0.22.2",
|
||||||
"@clerk/nextjs": "^6.9.2",
|
"@clerk/nextjs": "^6.9.2",
|
||||||
"@emotion/is-prop-valid": "^1.3.0",
|
"@emotion/is-prop-valid": "^1.3.0",
|
||||||
@ -35,9 +35,15 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@radix-ui/react-switch": "1.1.0",
|
"@radix-ui/react-switch": "1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.3",
|
"@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/lodash": "^4.17.16",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
|
"@types/request-ip": "^0.0.41",
|
||||||
"@unocss/eslint-plugin": "^0.61.5",
|
"@unocss/eslint-plugin": "^0.61.5",
|
||||||
"@unocss/postcss": "^0.61.5",
|
"@unocss/postcss": "^0.61.5",
|
||||||
"@unocss/transformer-directives": "^0.61.5",
|
"@unocss/transformer-directives": "^0.61.5",
|
||||||
@ -80,8 +86,10 @@
|
|||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
|
"request-ip": "^3.3.0",
|
||||||
"sonner": "^1.7.0",
|
"sonner": "^1.7.0",
|
||||||
"stripe-gradient": "^1.0.1",
|
"stripe-gradient": "^1.0.1",
|
||||||
|
"superjson": "^2.2.2",
|
||||||
"swapy": "^1.0.5",
|
"swapy": "^1.0.5",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss": "^4.0.7",
|
"tailwindcss": "^4.0.7",
|
||||||
@ -89,7 +97,8 @@
|
|||||||
"tailwindcss-patch": "^4.0.0",
|
"tailwindcss-patch": "^4.0.0",
|
||||||
"turbo": "^2.4.0",
|
"turbo": "^2.4.0",
|
||||||
"unplugin-tailwindcss-mangle": "^3.0.1",
|
"unplugin-tailwindcss-mangle": "^3.0.1",
|
||||||
"vaul": "^1.1.2"
|
"vaul": "^1.1.2",
|
||||||
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@clerk/themes": "^2.1.19",
|
"@clerk/themes": "^2.1.19",
|
||||||
|
|||||||
@ -74,22 +74,22 @@ export default function RootLayout({
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<ClerkProvider>
|
<ClerkProvider>
|
||||||
<IsScript>
|
<IsScript>
|
||||||
<NuqsAdapter>
|
<NuqsAdapter>
|
||||||
<FontBoundary>
|
<FontBoundary>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster richColors position="top-center" />
|
<Toaster richColors position="top-center" />
|
||||||
<ClerkProvider>
|
<ClerkProvider>
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div className="pt-16 min-h-screen">{children}</div>
|
<div className="pt-16 min-h-screen">{children}</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</ClerkProvider>
|
</ClerkProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</FontBoundary>
|
</FontBoundary>
|
||||||
</NuqsAdapter>
|
</NuqsAdapter>
|
||||||
</IsScript>
|
</IsScript>
|
||||||
</ClerkProvider>
|
</ClerkProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
203
apps/www/src/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/category/[category]/modification/custom/[custom-mod]/page.tsx
Normal file
203
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,203 @@
|
|||||||
|
/*
|
||||||
|
* 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 type { 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 {
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
}: {
|
||||||
|
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 modIndex = (
|
||||||
|
(user?.unsafeMetadata
|
||||||
|
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
|
||||||
|
).findIndex((c) => c.friendlyName === atob(decodeURIComponent(mod)));
|
||||||
|
|
||||||
|
if (modIndex === -1)
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex justify-center items-center absolute top-[0%]">
|
||||||
|
<Link href={backRoute}>
|
||||||
|
<ArrowLeft className="absolute left-[10px] top-[10px]" />
|
||||||
|
</Link>
|
||||||
|
<Placeholder
|
||||||
|
title="We couldn't find the file you were looking for."
|
||||||
|
icon={<FileQuestion />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const modObj = ((user?.unsafeMetadata
|
||||||
|
.activatedModifications as ClerkCustomActivatedModification[]) ?? [])[
|
||||||
|
modIndex
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="max-w-[800px] p-4">
|
||||||
|
<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>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Button className="mt-2">
|
||||||
|
{modObj?.active ? "Disable" : "Enable"}
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<EllipsisVertical size={16} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="gap-2"
|
||||||
|
onClick={async () => {
|
||||||
|
const time = Date.now();
|
||||||
|
const array =
|
||||||
|
(user?.unsafeMetadata
|
||||||
|
.activatedModifications as ClerkCustomActivatedModification[]) ??
|
||||||
|
[];
|
||||||
|
array.splice(modIndex, 1);
|
||||||
|
await user?.update({
|
||||||
|
unsafeMetadata: {
|
||||||
|
...user.unsafeMetadata,
|
||||||
|
activatedModifications: array,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
toast.success(`Deleted in ${Date.now() - time}ms`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash size={16} /> Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="mt-3" />
|
||||||
|
|
||||||
|
<Material className="mt-6 grid gap-3">
|
||||||
|
<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>
|
||||||
|
<Setting>
|
||||||
|
<SettingContent className="flex items-center">
|
||||||
|
<SettingMeta>
|
||||||
|
<SettingTitle>File name</SettingTitle>
|
||||||
|
</SettingMeta>
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
`"/servers/embedded/sl-modification-frame/file/${modObj.originalFileName}`
|
||||||
|
}
|
||||||
|
className="text-blue-600"
|
||||||
|
>
|
||||||
|
<code className="flex items-center">
|
||||||
|
{modObj.originalFileName}.ts
|
||||||
|
</code>
|
||||||
|
</Link>
|
||||||
|
</SettingContent>
|
||||||
|
</Setting>
|
||||||
|
</Material>
|
||||||
|
</span>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -28,65 +28,70 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ModificationAction } from "@/components/feat/server-list/modification/modification-action";
|
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 { Material } from "@/components/ui/material";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Link } from "@/components/util/link";
|
import { Link } from "@/components/util/link";
|
||||||
import { serverModDB } from "@/config/sl-mod-db";
|
import { serverModDB } from "@/config/sl-mod-db";
|
||||||
import { useRouter } from "@/lib/useRouter";
|
import { useRouter } from "@/lib/useRouter";
|
||||||
import { cn } from "@/lib/utils";
|
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 { use } from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
|
|
||||||
export default function ServerListCategoryFrame({
|
export default async function ServerListCategoryFrame({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ category: string }>;
|
params: Promise<{ category: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { category } = use(params);
|
const { category } = await params;
|
||||||
const categoryObj = serverModDB.find(
|
const categoryObj = serverModDB.find(
|
||||||
(c) => c.displayTitle === atob(category)
|
(c) => c.displayTitle === atob(decodeURIComponent(category)),
|
||||||
);
|
);
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="max-w-[800px] p-4">
|
<main className="max-w-[800px] p-4">
|
||||||
<h1 className="text-xl font-bold w-full flex items-center gap-2">
|
<h1 className="text-xl font-bold w-full flex items-center gap-2">
|
||||||
<Link href="/servers/embedded/sl-modification-frame">
|
<Link href="/servers/embedded/sl-modification-frame">
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
</Link>
|
</Link>
|
||||||
{categoryObj?.displayTitle}
|
{categoryObj?.displayTitle}
|
||||||
</h1>
|
</h1>
|
||||||
<Markdown className="text-wrap pt-2">{categoryObj?.description}</Markdown>
|
<Markdown className="text-wrap pt-2">{categoryObj?.description}</Markdown>
|
||||||
|
|
||||||
<Material className="mt-10 p-4 grid grid-cols-6 gap-2">
|
<Material className="mt-10 p-4 grid grid-cols-6 gap-2">
|
||||||
{categoryObj?.entries.map((m) => (
|
{categoryObj?.entries.map((m) => (
|
||||||
<Material
|
<Link
|
||||||
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
|
key={m.name}
|
||||||
elevation="high"
|
href={`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`}
|
||||||
onClick={() =>
|
>
|
||||||
router.push(
|
<Material
|
||||||
`/servers/embedded/sl-modification-frame/category/${category}/modification/${btoa(m.name)}?b=${encodeURIComponent(`/servers/embedded/sl-modification-frame/category/${category}`)}`
|
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
|
||||||
)
|
elevation="high"
|
||||||
}
|
>
|
||||||
key={m.name}
|
<div
|
||||||
>
|
className={cn(
|
||||||
<div
|
"w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center",
|
||||||
className={cn(
|
)}
|
||||||
"w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
style={{ backgroundColor: m.color }}
|
||||||
)}
|
>
|
||||||
style={{ backgroundColor: m.color }}
|
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
||||||
>
|
</div>
|
||||||
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
<span className="text-sm text-center w-full flex items-center justify-center">
|
||||||
</div>
|
{m.name}
|
||||||
<span className="text-sm text-center w-full flex items-center justify-center">
|
</span>
|
||||||
{m.name}
|
</Material>
|
||||||
</span>
|
</Link>
|
||||||
</Material>
|
))}
|
||||||
))}
|
<SignedIn>
|
||||||
</Material>
|
{categoryObj?.__custom && (
|
||||||
</main>
|
<ModificationCustomModificationRow category={category} />
|
||||||
);
|
)}
|
||||||
|
</SignedIn>
|
||||||
|
</Material>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,37 +4,29 @@ import { use, useEffect, useRef, useState } from "react";
|
|||||||
import { useUser } from "@clerk/nextjs";
|
import { useUser } from "@clerk/nextjs";
|
||||||
import type { 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 { Link } from "@/components/util/link";
|
||||||
import { AlertOctagon, ArrowLeft, ExternalLink } from "lucide-react";
|
import { ArrowLeft, FileQuestion } from "lucide-react";
|
||||||
import Editor from "@monaco-editor/react";
|
import Editor from "@monaco-editor/react";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import * as ts from "typescript";
|
import * as ts from "typescript";
|
||||||
import useClipboard from "@/lib/useClipboard";
|
import useClipboard from "@/lib/useClipboard";
|
||||||
import { useTheme } from "@/lib/hooks/use-theme";
|
import { useTheme } from "@/lib/hooks/use-theme";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import type { languages, Uri } from "monaco-editor";
|
import type { languages } 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 { cn } from "@/lib/utils";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import { tryCatch } from "@/lib/try-catch";
|
import { tryCatch } from "@/lib/try-catch";
|
||||||
|
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?
|
const typeDefs = `// Hi :) how'd you get here?
|
||||||
// Here, in return I'll provide you with a random number: ${Math.ceil(Math.random() * 100)}
|
// Here, in return I'll provide you with a random number: ${Math.ceil(Math.random() * 100)}
|
||||||
@ -77,378 +69,269 @@ export namespace Minehut {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const transpileTypeScript = (code: string) => {
|
export const transpileTypeScript = (code: string) => {
|
||||||
try {
|
try {
|
||||||
const result = ts.transpileModule(typeDefs + code, {
|
const result = ts.transpileModule(typeDefs + code, {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
module: ts.ModuleKind.ESNext,
|
module: ts.ModuleKind.ESNext,
|
||||||
target: ts.ScriptTarget.ESNext,
|
target: ts.ScriptTarget.ESNext,
|
||||||
jsx: ts.JsxEmit.ReactJSX,
|
jsx: ts.JsxEmit.ReactJSX,
|
||||||
esModuleInterop: true,
|
esModuleInterop: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return result.outputText;
|
return result.outputText;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("TypeScript transpilation error:", error);
|
console.error("TypeScript transpilation error:", error);
|
||||||
toast.error(`TypeScript error: ${error}`);
|
toast.error(`TypeScript error: ${error}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const geistMono = Geist_Mono({ subsets: ["latin"] });
|
|
||||||
|
|
||||||
export default function CustomFilePage({
|
export default function CustomFilePage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ filename: string }>;
|
params: Promise<{ filename: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { filename } = use(params);
|
const { filename } = use(params);
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const monacoRef =
|
const monacoRef =
|
||||||
useRef<typeof import("monaco-editor/esm/vs/editor/editor.api")>(null);
|
useRef<typeof import("monaco-editor/esm/vs/editor/editor.api")>(null);
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
const [successfullyLinted, setSuccessfullyLinted] = useState(false);
|
const [successfullyLinted, setSuccessfullyLinted] = useState(false);
|
||||||
const [syntaxErrors, setSyntaxErrors] = useState<
|
const [syntaxErrors, setSyntaxErrors] = useState<
|
||||||
languages.typescript.Diagnostic[] | null
|
languages.typescript.Diagnostic[] | null
|
||||||
>(null);
|
>(null);
|
||||||
const file = (
|
const file = (
|
||||||
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
|
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
|
||||||
).findIndex((c) => c.name === filename);
|
).findIndex((c) => c.name === filename);
|
||||||
|
|
||||||
if (file === -1) {
|
if (file === -1) {
|
||||||
return <>Bruh.</>;
|
return (
|
||||||
}
|
<div className="w-full h-full flex justify-center items-center absolute top-[0%]">
|
||||||
|
<Link href="/servers/embedded/sl-modification-frame">
|
||||||
|
<ArrowLeft className="absolute left-[10px] top-[10px]" />
|
||||||
|
</Link>
|
||||||
|
<Placeholder
|
||||||
|
title="We couldn't find the file you were looking for."
|
||||||
|
icon={<FileQuestion />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const validateCode = (code: string) => {
|
const fileContents = ((user?.unsafeMetadata
|
||||||
if (!monacoRef.current) return;
|
.customFiles as Array<ClerkCustomModification>) ?? [])[file].contents;
|
||||||
|
const [value, setValue] = useState(fileContents);
|
||||||
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
monacoRef.current.languages.typescript
|
const saveFile = async () => {
|
||||||
.getTypeScriptWorker()
|
const metadata =
|
||||||
.then((worker) => {
|
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ??
|
||||||
worker(
|
[];
|
||||||
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
|
const index = (
|
||||||
).then((client) => {
|
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
|
||||||
client
|
).findIndex((c) => c.name === filename);
|
||||||
.getSemanticDiagnostics(
|
|
||||||
(
|
|
||||||
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
|
|
||||||
).toString()
|
|
||||||
)
|
|
||||||
.then((diags) => {
|
|
||||||
setSyntaxErrors(diags);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileContents = ((user?.unsafeMetadata
|
metadata[index].contents = value;
|
||||||
.customFiles as Array<ClerkCustomModification>) ?? [])[file].contents;
|
|
||||||
const [value, setValue] = useState(fileContents);
|
|
||||||
const clipboard = useClipboard();
|
|
||||||
validateCode(value);
|
|
||||||
|
|
||||||
const saveFile = async () => {
|
await user?.update({
|
||||||
const metadata =
|
unsafeMetadata: {
|
||||||
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ??
|
...user.unsafeMetadata,
|
||||||
[];
|
customFiles: metadata,
|
||||||
const index = (
|
},
|
||||||
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? []
|
});
|
||||||
).findIndex((c) => c.name === filename);
|
};
|
||||||
|
|
||||||
metadata[index].contents = value;
|
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);
|
||||||
|
|
||||||
await user?.update({
|
// biome-ignore lint: L
|
||||||
unsafeMetadata: {
|
useEffect(() => {
|
||||||
customFiles: metadata,
|
setSuccessfullyLinted(false);
|
||||||
},
|
debouncedSave();
|
||||||
});
|
}, [value]);
|
||||||
};
|
|
||||||
|
|
||||||
const lintFile = async () => {
|
return (
|
||||||
toast.info("Transpiling TypeScript...");
|
<main className="max-w-[800px] p-4">
|
||||||
const { error, data: transpiledCode } = await tryCatch(
|
<div className="w-full justify-between flex items-center gap-2 my-2">
|
||||||
(async () => transpileTypeScript(value))()
|
<strong className="flex items-center gap-1">
|
||||||
);
|
<Link href="/servers/embedded/sl-modification-frame/files">
|
||||||
if (error) {
|
<ArrowLeft size={20} />
|
||||||
toast.error("Failed to transpile TypeScript! Error: " + error.message);
|
</Link>
|
||||||
return;
|
{filename}.ts
|
||||||
}
|
</strong>
|
||||||
const startTime = Date.now();
|
<span className="flex items-center gap-2">
|
||||||
if (transpiledCode === null) {
|
{syntaxErrors !== null && syntaxErrors.length !== 0 && (
|
||||||
toast.error("Cannot continue.");
|
<CustomErrors
|
||||||
return;
|
filename={filename}
|
||||||
}
|
value={value}
|
||||||
console.log("[MHSF Filters] Transpiled TypeScript:", transpiledCode ?? "");
|
monacoRef={monacoRef}
|
||||||
toast.info("Generating function...");
|
/>
|
||||||
const functionBody = transpiledCode.match(
|
)}
|
||||||
/function\s+filter\s*\([^)]*\)\s*\{([\s\S]*)\}/
|
<Tooltip>
|
||||||
)?.[1];
|
<TooltipTrigger>
|
||||||
const { error: filterErr, data: filterFunc } = await tryCatch(
|
<CustomLint
|
||||||
(async () => new Function("data", functionBody as string))()
|
successfullyLinted={successfullyLinted}
|
||||||
);
|
setSuccessfullyLinted={setSuccessfullyLinted}
|
||||||
if (filterErr) {
|
syntaxErrors={syntaxErrors}
|
||||||
toast.error(`Failed to generate function! Error: ${filterErr.message}`);
|
value={value}
|
||||||
return;
|
/>
|
||||||
}
|
</TooltipTrigger>
|
||||||
if (typeof filterFunc === "function") {
|
<TooltipContent>
|
||||||
toast.success("Linted in " + (Date.now() - startTime) + "ms");
|
{syntaxErrors !== null && syntaxErrors.length !== 0
|
||||||
setSuccessfullyLinted(true);
|
? `You must have no type errors in the editor to lint, you have ${syntaxErrors.length} error(s).`
|
||||||
} else {
|
: "Check for possible runtime errors."}
|
||||||
toast.error("Code doesn't have a 'filter' function. Cannot be tested.");
|
</TooltipContent>
|
||||||
toast.error(typeof filterFunc);
|
</Tooltip>
|
||||||
}
|
<Tooltip>
|
||||||
};
|
<TooltipTrigger>
|
||||||
|
<CustomTest
|
||||||
|
value={value}
|
||||||
|
successfullyLinted={successfullyLinted}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{successfullyLinted
|
||||||
|
? "Open a full server-list instance with your filter activated in test mode."
|
||||||
|
: "You must lint before testing."}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Editor
|
||||||
|
className={cn("h-[calc(100vh-100px)]")}
|
||||||
|
defaultLanguage="typescript"
|
||||||
|
value={value}
|
||||||
|
theme={resolvedTheme === "dark" ? "vs-dark" : "vs"}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
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,
|
||||||
|
moduleResolution:
|
||||||
|
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||||
|
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
||||||
|
noEmit: true,
|
||||||
|
esModuleInterop: true,
|
||||||
|
jsx: monaco.languages.typescript.JsxEmit.React,
|
||||||
|
reactNamespace: "React",
|
||||||
|
allowJs: true,
|
||||||
|
typeRoots: ["node_modules/@types"],
|
||||||
|
});
|
||||||
|
|
||||||
const debouncedSave = debounce(async () => {
|
// Create a virtual TS file for the types
|
||||||
const { error } = await tryCatch(saveFile());
|
const libUri = "file:///node_modules/@types/mhsf/index.d.ts";
|
||||||
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(() => {
|
// Add typedefs as a library
|
||||||
setSuccessfullyLinted(false);
|
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||||
validateCode(value);
|
typeDefs,
|
||||||
debouncedSave();
|
libUri,
|
||||||
}, [value]);
|
);
|
||||||
|
|
||||||
return (
|
// Create a model for the libUri file
|
||||||
<main className="max-w-[800px] p-4">
|
if (!monaco.editor.getModel(monaco.Uri.parse(libUri))) {
|
||||||
<div className="w-full justify-between flex items-center gap-2 my-2">
|
monaco.editor.createModel(
|
||||||
<strong className="flex items-center gap-1">
|
typeDefs,
|
||||||
<Link href="/servers/embedded/sl-modification-frame/files">
|
"typescript",
|
||||||
<ArrowLeft size={20} />
|
monaco.Uri.parse(libUri),
|
||||||
</Link>
|
);
|
||||||
{filename}.ts
|
}
|
||||||
</strong>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
{syntaxErrors !== null && syntaxErrors.length !== 0 && (
|
|
||||||
<Drawer direction="right">
|
|
||||||
<DrawerTrigger>
|
|
||||||
<Button
|
|
||||||
variant="danger-subtle"
|
|
||||||
size="square-md"
|
|
||||||
className="flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<AlertOctagon />
|
|
||||||
</Button>
|
|
||||||
</DrawerTrigger>
|
|
||||||
<DrawerContent className="p-4 min-w-[400px] overflow-x-hidden max-h-screen overflow-y-auto">
|
|
||||||
<DrawerTitle>Type Errors</DrawerTitle>
|
|
||||||
<div className="p-2">
|
|
||||||
{syntaxErrors.map((c, i) => (
|
|
||||||
<Alert
|
|
||||||
variant={
|
|
||||||
c.category === 1
|
|
||||||
? "error"
|
|
||||||
: c.category === 0
|
|
||||||
? "warning"
|
|
||||||
: "info"
|
|
||||||
}
|
|
||||||
key={i}
|
|
||||||
className="gap-1 my-2"
|
|
||||||
>
|
|
||||||
{c.messageText.toString()}{" "}
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<small className="flex items-center gap-1 cursor-pointer">
|
|
||||||
(TS{typeof c !== "string" && c.code})
|
|
||||||
<ExternalLink size={16} />
|
|
||||||
</small>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<Link
|
|
||||||
noExtraIcons
|
|
||||||
target="_blank"
|
|
||||||
href={`https://typescript.tv/errors/#ts${c.code}`}
|
|
||||||
>
|
|
||||||
<DropdownMenuItem>typescript.tv</DropdownMenuItem>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
noExtraIcons
|
|
||||||
target="_blank"
|
|
||||||
href={`https://ts-error-translator.vercel.app/?error=${compressToEncodedURIComponent(c.messageText.toString())}`}
|
|
||||||
>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
ts-error-translator
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Alert>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
)}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Button
|
|
||||||
onClick={lintFile}
|
|
||||||
disabled={syntaxErrors === null || syntaxErrors.length !== 0}
|
|
||||||
variant={successfullyLinted ? "success-subtle" : "secondary"}
|
|
||||||
>
|
|
||||||
Lint
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
{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."}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Button
|
|
||||||
disabled={!successfullyLinted}
|
|
||||||
onClick={() => {
|
|
||||||
const t = btoa(value);
|
|
||||||
|
|
||||||
const newTab = window.open(`/servers?tm=${encodeURIComponent(t)}`)
|
// Make sure the current file is using the correct language
|
||||||
const interval = setInterval(() => {
|
const currentModel = editor.getModel();
|
||||||
newTab?.dispatchEvent(new Event("test-mode.enable"))
|
if (currentModel) {
|
||||||
}, 500)
|
monaco.editor.setModelLanguage(currentModel, "typescript");
|
||||||
toast.info("Waiting for server tab to pick up thread...")
|
}
|
||||||
|
|
||||||
newTab?.addEventListener("test-mode.enabled", () => {
|
const currentUri = monaco.Uri.parse(`file:///${filename}.ts`);
|
||||||
clearInterval(interval);
|
if (!monaco.editor.getModel(currentUri)) {
|
||||||
toast.success("Connected to new tab; continue.")
|
monaco.editor.createModel(fileContents, "typescript", currentUri);
|
||||||
})
|
editor.setModel(monaco.editor.getModel(currentUri));
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
Test
|
options={{
|
||||||
</Button>
|
minimap: { enabled: false },
|
||||||
</TooltipTrigger>
|
scrollBeyondLastLine: false,
|
||||||
<TooltipContent>
|
fontSize: 14,
|
||||||
{successfullyLinted
|
lineNumbers: "on",
|
||||||
? "Open a full server-list instance with your filter activated in test mode."
|
roundedSelection: false,
|
||||||
: "You must lint before testing."}
|
scrollbar: {
|
||||||
</TooltipContent>
|
vertical: "visible",
|
||||||
</Tooltip>
|
horizontal: "visible",
|
||||||
</span>
|
},
|
||||||
</div>
|
quickSuggestions: true,
|
||||||
<div>
|
suggestOnTriggerCharacters: true,
|
||||||
<Editor
|
acceptSuggestionOnEnter: "on",
|
||||||
className={cn("h-[calc(100vh-100px)]")}
|
tabCompletion: "on",
|
||||||
defaultLanguage="typescript"
|
wordBasedSuggestions: "currentDocument",
|
||||||
value={value}
|
cursorSmoothCaretAnimation: "on",
|
||||||
theme={resolvedTheme === "dark" ? "vs-dark" : "vs"}
|
parameterHints: {
|
||||||
onChange={(newValue) => {
|
enabled: true,
|
||||||
setValue(newValue || "");
|
},
|
||||||
}}
|
hover: {
|
||||||
onMount={(editor, monaco) => {
|
enabled: true,
|
||||||
monacoRef.current = monaco;
|
delay: 300,
|
||||||
// Ensure TypeScript is properly configured
|
sticky: true,
|
||||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
},
|
||||||
target: monaco.languages.typescript.ScriptTarget.Latest,
|
}}
|
||||||
allowNonTsExtensions: true,
|
/>
|
||||||
moduleResolution:
|
</div>
|
||||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
</main>
|
||||||
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
);
|
||||||
noEmit: true,
|
|
||||||
esModuleInterop: true,
|
|
||||||
jsx: monaco.languages.typescript.JsxEmit.React,
|
|
||||||
reactNamespace: "React",
|
|
||||||
allowJs: true,
|
|
||||||
typeRoots: ["node_modules/@types"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a virtual TS file for the types
|
|
||||||
const libUri = "file:///node_modules/@types/mhsf/index.d.ts";
|
|
||||||
|
|
||||||
// Add typedefs as a library
|
|
||||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
|
||||||
typeDefs,
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the current file is using the correct language
|
|
||||||
const currentModel = editor.getModel();
|
|
||||||
if (currentModel) {
|
|
||||||
monaco.editor.setModelLanguage(currentModel, "typescript");
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentUri = monaco.Uri.parse(`file:///${filename}.ts`);
|
|
||||||
if (!monaco.editor.getModel(currentUri)) {
|
|
||||||
monaco.editor.createModel(fileContents, "typescript", currentUri);
|
|
||||||
editor.setModel(monaco.editor.getModel(currentUri));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={{
|
|
||||||
minimap: { enabled: false },
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
fontSize: 14,
|
|
||||||
lineNumbers: "on",
|
|
||||||
roundedSelection: false,
|
|
||||||
scrollbar: {
|
|
||||||
vertical: "visible",
|
|
||||||
horizontal: "visible",
|
|
||||||
},
|
|
||||||
quickSuggestions: true,
|
|
||||||
suggestOnTriggerCharacters: true,
|
|
||||||
acceptSuggestionOnEnter: "on",
|
|
||||||
tabCompletion: "on",
|
|
||||||
wordBasedSuggestions: "currentDocument",
|
|
||||||
cursorSmoothCaretAnimation: "on",
|
|
||||||
parameterHints: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
hover: {
|
|
||||||
enabled: true,
|
|
||||||
delay: 300,
|
|
||||||
sticky: true,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function guidGenerator() {
|
export async function findSupportedOperations(
|
||||||
const S4 = () => {
|
fileValue: string,
|
||||||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
): Promise<{ filter: boolean; sort: boolean }> {
|
||||||
};
|
const returnValue = { filter: true, sort: true };
|
||||||
return (
|
const transpiledValue = transpileTypeScript(fileValue);
|
||||||
S4() +
|
const functionBody = transpiledValue
|
||||||
S4() +
|
?.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
|
||||||
"-" +
|
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
|
||||||
S4() +
|
const { error: filterErr, data: filterFunc } = await tryCatch(
|
||||||
"-" +
|
(async () =>
|
||||||
S4() +
|
new Function(
|
||||||
"-" +
|
"server",
|
||||||
S4() +
|
`${functionBody}
|
||||||
"-" +
|
return filter(server)`,
|
||||||
S4() +
|
))(),
|
||||||
S4() +
|
);
|
||||||
S4()
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,97 +33,126 @@
|
|||||||
import { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
import { ClerkCustomModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Material } from "@/components/ui/material";
|
import { Material } from "@/components/ui/material";
|
||||||
import { Placeholder } from "@/components/ui/placeholder";
|
import { Placeholder } from "@/components/ui/placeholder";
|
||||||
import { Link } from "@/components/util/link";
|
import { Link } from "@/components/util/link";
|
||||||
import { useUser } from "@clerk/nextjs";
|
import { useUser } from "@clerk/nextjs";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Braces,
|
Braces,
|
||||||
EllipsisVertical,
|
EllipsisVertical,
|
||||||
FileCode,
|
FileCode,
|
||||||
Pencil,
|
Filter,
|
||||||
Trash,
|
Pencil,
|
||||||
|
SortAsc,
|
||||||
|
Trash,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { use, useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { findSupportedOperations } from "../file/[filename]/page";
|
||||||
|
|
||||||
export default function ServerListModificationFrame() {
|
export default function ServerListModificationFrame() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const files =
|
const files =
|
||||||
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [];
|
(user?.unsafeMetadata.customFiles as Array<ClerkCustomModification>) ?? [];
|
||||||
return (
|
const operations = usePlatforms(files);
|
||||||
<main className="max-w-[800px] p-4">
|
|
||||||
<h1 className="text-xl font-bold w-full flex items-center gap-2">
|
|
||||||
<Link href="/servers/embedded/sl-modification-frame">
|
return (
|
||||||
<ArrowLeft size={16} />
|
<main className="max-w-[800px] p-4">
|
||||||
</Link>
|
<h1 className="text-xl font-bold w-full flex items-center gap-2">
|
||||||
Files
|
<Link href="/servers/embedded/sl-modification-frame">
|
||||||
</h1>
|
<ArrowLeft size={16} />
|
||||||
<Material className="grid gap-1 mt-4">
|
</Link>
|
||||||
{files.length === 0 && (
|
Files
|
||||||
<Placeholder
|
</h1>
|
||||||
icon={<Braces />}
|
<Material className="grid gap-1 mt-4">
|
||||||
title="We couldn't find any files"
|
{files.length === 0 && (
|
||||||
description="Try creating a filter!"
|
<Placeholder
|
||||||
/>
|
icon={<Braces />}
|
||||||
)}
|
title="We couldn't find any files"
|
||||||
{files.map((c, i) => (
|
description="Try creating a filter!"
|
||||||
<Link
|
/>
|
||||||
href={`/servers/embedded/sl-modification-frame/file/${c.name}`}
|
)}
|
||||||
className="w-full py-1 px-2 rounded-xl flex items-center gap-1 justify-between hover:bg-slate-100 dark:hover:bg-zinc-700/30"
|
{files.map((c, i) => (
|
||||||
key={c.name}
|
<Link
|
||||||
>
|
href={`/servers/embedded/sl-modification-frame/file/${c.name}`}
|
||||||
<span className="flex items-center gap-1">
|
className="w-full py-1 px-2 rounded-xl flex items-center gap-1 justify-between hover:bg-slate-100 dark:hover:bg-zinc-700/30"
|
||||||
<FileCode size={16} />
|
key={c.name}
|
||||||
{c.name}.ts
|
>
|
||||||
</span>
|
<span className="flex items-center gap-1">
|
||||||
<span>
|
<FileCode size={16} />
|
||||||
<DropdownMenu>
|
{operations[i].filter && <Filter size={16} />}
|
||||||
<DropdownMenuTrigger>
|
{operations[i].sort && <SortAsc size={16} />}
|
||||||
<Button
|
{c.name}.ts
|
||||||
variant="tertiary"
|
</span>
|
||||||
className="flex items-center justify-center hover:bg-slate-200 dark:hover:bg-zinc-700/60"
|
<span>
|
||||||
size="square-sm"
|
<DropdownMenu>
|
||||||
>
|
<DropdownMenuTrigger>
|
||||||
<EllipsisVertical
|
<DropdownMenu></DropdownMenu>
|
||||||
size={16}
|
<Button
|
||||||
className="text-muted-foreground"
|
variant="tertiary"
|
||||||
/>
|
className="flex items-center justify-center hover:bg-slate-200 dark:hover:bg-zinc-700/60"
|
||||||
</Button>
|
size="square-sm"
|
||||||
</DropdownMenuTrigger>
|
>
|
||||||
<DropdownMenuContent>
|
<EllipsisVertical
|
||||||
<DropdownMenuItem
|
size={16}
|
||||||
className="flex items-center gap-2"
|
className="text-muted-foreground"
|
||||||
onClick={async (e) => {
|
/>
|
||||||
e.stopPropagation();
|
</Button>
|
||||||
const startTime = Date.now();
|
</DropdownMenuTrigger>
|
||||||
files.splice(i, 1);
|
<DropdownMenuContent>
|
||||||
await user?.update({
|
<DropdownMenuItem
|
||||||
unsafeMetadata: {
|
className="flex items-center gap-2"
|
||||||
customFiles: files,
|
onClick={async (e) => {
|
||||||
},
|
e.stopPropagation();
|
||||||
});
|
const startTime = Date.now();
|
||||||
toast.success(
|
files.splice(i, 1);
|
||||||
"Deleted file in " + (Date.now() - startTime) + "ms"
|
await user?.update({
|
||||||
);
|
unsafeMetadata: {
|
||||||
}}
|
...user.unsafeMetadata,
|
||||||
>
|
customFiles: files,
|
||||||
<Trash size={16} /> Delete
|
},
|
||||||
</DropdownMenuItem>
|
});
|
||||||
<DropdownMenuItem className="flex items-center gap-2">
|
toast.success(
|
||||||
<Pencil size={16} /> Rename
|
"Deleted file in " + (Date.now() - startTime) + "ms",
|
||||||
</DropdownMenuItem>
|
);
|
||||||
</DropdownMenuContent>
|
}}
|
||||||
</DropdownMenu>
|
>
|
||||||
</span>
|
<Trash size={16} /> Delete
|
||||||
</Link>
|
</DropdownMenuItem>
|
||||||
))}
|
<DropdownMenuItem className="flex items-center gap-2">
|
||||||
</Material>
|
<Pencil size={16} /> Rename
|
||||||
</main>
|
</DropdownMenuItem>
|
||||||
);
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</Material>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePlatforms(files: Array<ClerkCustomModification>) {
|
||||||
|
const [result, setResult] = useState<
|
||||||
|
Array<{ sort: boolean; filter: boolean }>
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setResult(
|
||||||
|
await Promise.all(
|
||||||
|
files.map(async (c) => await findSupportedOperations(c.contents)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,68 +34,100 @@ import { Material } from "@/components/ui/material";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Link } from "@/components/util/link";
|
import { Link } from "@/components/util/link";
|
||||||
import { serverModDB } from "@/config/sl-mod-db";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { useRouter } from "@/lib/useRouter";
|
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() {
|
export default function ServerListModificationFrame() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="max-w-[800px] p-4">
|
<main className="max-w-[800px] p-4">
|
||||||
<h1 className="text-xl font-bold w-full">Filters & Sorting</h1>
|
<h1 className="text-xl font-bold w-full">Filters & Sorting</h1>
|
||||||
<div className="flex items-center gap-2 my-2">
|
<div className="flex items-center gap-2 my-2">
|
||||||
<Button size="sm">Active modifications</Button>
|
<Button size="sm">Active modifications</Button>
|
||||||
<Link href="/servers/embedded/sl-modification-frame/files">
|
<Link href="/servers/embedded/sl-modification-frame/files">
|
||||||
<Button size="sm">Custom files</Button>
|
<Button size="sm">Custom files</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button size="sm">Settings</Button>
|
<Button size="sm">Settings</Button>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-wrap pt-2">
|
<span className="text-wrap pt-2">
|
||||||
Pick out different filters & sorting systems to customize your server
|
Pick out different filters & sorting systems to customize your server
|
||||||
viewing experience. We frequently add new filters in accordance to new
|
viewing experience. We frequently add new filters in accordance to new
|
||||||
features, as well.
|
features, as well.
|
||||||
</span>
|
</span>
|
||||||
<Material className="mt-10 p-4">
|
<Material className="mt-10 p-4">
|
||||||
{serverModDB.map((c) => (
|
{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">
|
<h2 className="text-lg font-bold pb-3 flex justify-between">
|
||||||
{c.displayTitle}
|
{c.displayTitle}
|
||||||
<Link
|
<Link
|
||||||
href={`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}`}
|
href={`/servers/embedded/sl-modification-frame/category/${btoa(c.displayTitle)}`}
|
||||||
className="flex gap-2 text-sm font-normal items-center"
|
className="flex gap-2 text-sm font-normal items-center"
|
||||||
>
|
>
|
||||||
<ArrowRight size={16} />
|
<ArrowRight size={16} />
|
||||||
View more
|
View more
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-6 gap-2">
|
<div className="grid grid-cols-6 gap-2">
|
||||||
{c.entries.map((m) => (
|
{c.entries.map((m) => (
|
||||||
<Material
|
<Material
|
||||||
elevation="high"
|
elevation="high"
|
||||||
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
|
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
|
||||||
key={m.name}
|
key={m.name}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
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)}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
||||||
style={{ backgroundColor: m.color }}
|
style={{ backgroundColor: m.color }}
|
||||||
>
|
>
|
||||||
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-center w-full flex items-center justify-center">
|
<span className="text-sm text-center w-full flex items-center justify-center">
|
||||||
{m.name}
|
{m.name}
|
||||||
</span>
|
</span>
|
||||||
</Material>
|
</Material>
|
||||||
))}
|
))}
|
||||||
</div>
|
<SignedIn>
|
||||||
</span>
|
{c.__custom &&
|
||||||
))}
|
(
|
||||||
</Material>
|
(user?.unsafeMetadata
|
||||||
</main>
|
.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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -236,7 +236,7 @@
|
|||||||
|
|
||||||
.loading-shimmer {
|
.loading-shimmer {
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
animation-duration: 2.5s;
|
animation-duration: 2s;
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
animation-name: loading-shimmer;
|
animation-name: loading-shimmer;
|
||||||
background: var(--color-muted-foreground)
|
background: var(--color-muted-foreground)
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { BrandingGenericIcon } from "../icons/branding-icons";
|
import { BrandingGenericIcon, Discord } from "../icons/branding-icons";
|
||||||
import { Link } from "../../util/link";
|
import { Link } from "../../util/link";
|
||||||
import { FooterStatus } from "./status";
|
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() {
|
export function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="w-full border-t p-[20px] mt-15">
|
<footer className="w-full border-t p-[20px] mt-15">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-start">
|
||||||
<span className="flex items-center gap-4 text-muted-foreground">
|
<span className="flex items-center gap-4 text-muted-foreground">
|
||||||
<Link href="Special:Root">
|
<Link href="Special:Root">
|
||||||
<BrandingGenericIcon className="max-w-[32px] max-h-[32px]" />
|
<BrandingGenericIcon className="max-w-[32px] max-h-[32px]" />
|
||||||
@ -37,7 +41,46 @@ export function Footer() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
<FooterStatus />
|
<div className="block">
|
||||||
|
<div className="flex items-center mb-2 justify-end gap-2">
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button variant="tertiary" size="square-md" className="flex items-center">
|
||||||
|
<Discord className="w-[1.25em] h-[1.25em]" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<Link href="https://t.mhsf.app/d/m" noExtraIcons>
|
||||||
|
<DropdownMenuItem className="py-2 flex items-center gap-2">
|
||||||
|
<Image className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" src="https://avatars.githubusercontent.com/u/16529253?s=200&v=4" alt="Minehut" width={30} height={30} />
|
||||||
|
<span className="block">
|
||||||
|
Minehut Discord
|
||||||
|
<small className="flex">Not officially owned by MHSF, however conversations about MHSF and related take place there.</small>
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
<Link href="https://t.mhsf.app/d/u" noExtraIcons>
|
||||||
|
<DropdownMenuItem className="py-2 flex items-center gap-2">
|
||||||
|
<BrandingGenericIcon className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" width={30} height={30} />
|
||||||
|
<span className="block">
|
||||||
|
MHSF Discord
|
||||||
|
<small className="flex">A read-only server for updates related to MHSF.</small>
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Link href="https://github.com/DeveloLongScript/MHSF" noExtraIcons>
|
||||||
|
<Button variant="tertiary" size="square-md" className="flex items-center">
|
||||||
|
<Github className="w-[1.25em] h-[1.25em]" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<FooterStatus />
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="block mt-4">
|
<span className="block mt-4">
|
||||||
<small className="text-muted-foreground text-[0.75rem]">
|
<small className="text-muted-foreground text-[0.75rem]">
|
||||||
|
|||||||
153
apps/www/src/components/feat/server-list/modification/custom-files/custom-errors.tsx
Normal file
153
apps/www/src/components/feat/server-list/modification/custom-files/custom-errors.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* MHSF, Minehut Server List
|
||||||
|
* All external content is rather licensed under the ECA Agreement
|
||||||
|
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||||
|
*
|
||||||
|
* All code under MHSF is licensed under the MIT License
|
||||||
|
* by open source contributors
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 dvelo
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerTitle,
|
||||||
|
} from "@/components/ui/drawer";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Alert } from "@/components/ui/alert";
|
||||||
|
import { compressToEncodedURIComponent } from "lz-string";
|
||||||
|
import { AlertOctagon, ExternalLink } from "lucide-react";
|
||||||
|
import type { languages, Uri } from "monaco-editor";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Link } from "@/components/util/link";
|
||||||
|
import { type RefObject, useEffect, useState } from "react";
|
||||||
|
import type { MonacoRefType } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
||||||
|
|
||||||
|
export type SyntaxErrorInterface = languages.typescript.Diagnostic[] | null;
|
||||||
|
|
||||||
|
export function CustomErrors({
|
||||||
|
value,
|
||||||
|
monacoRef,
|
||||||
|
filename
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
monacoRef: RefObject<MonacoRefType | null>;
|
||||||
|
filename: string;
|
||||||
|
}) {
|
||||||
|
const [syntaxErrors, setSyntaxErrors] = useState<SyntaxErrorInterface>();
|
||||||
|
|
||||||
|
const validateCode = () => {
|
||||||
|
if (!monacoRef.current) return;
|
||||||
|
|
||||||
|
monacoRef.current.languages.typescript
|
||||||
|
.getTypeScriptWorker()
|
||||||
|
.then((worker) => {
|
||||||
|
worker(
|
||||||
|
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri,
|
||||||
|
).then((client) => {
|
||||||
|
client
|
||||||
|
.getSemanticDiagnostics(
|
||||||
|
(
|
||||||
|
monacoRef.current?.Uri.parse(`file:///${filename}.ts`) as Uri
|
||||||
|
).toString(),
|
||||||
|
)
|
||||||
|
.then((diags) => {
|
||||||
|
setSyntaxErrors(diags);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
validateCode();
|
||||||
|
|
||||||
|
// biome-ignore lint: L
|
||||||
|
useEffect(validateCode, [value]);
|
||||||
|
|
||||||
|
if (syntaxErrors !== null && syntaxErrors !== undefined)
|
||||||
|
return (
|
||||||
|
<Drawer direction="right">
|
||||||
|
<DrawerTrigger>
|
||||||
|
<Button
|
||||||
|
variant="danger-subtle"
|
||||||
|
size="square-md"
|
||||||
|
className="flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<AlertOctagon />
|
||||||
|
</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent className="p-4 min-w-[400px] overflow-x-hidden max-h-screen overflow-y-auto">
|
||||||
|
<DrawerTitle>Type Errors</DrawerTitle>
|
||||||
|
<div className="p-2">
|
||||||
|
{syntaxErrors.map((c, i) => (
|
||||||
|
<Alert
|
||||||
|
variant={
|
||||||
|
c.category === 1
|
||||||
|
? "error"
|
||||||
|
: c.category === 0
|
||||||
|
? "warning"
|
||||||
|
: "info"
|
||||||
|
}
|
||||||
|
/* biome-ignore lint: No. */
|
||||||
|
key={i}
|
||||||
|
className="gap-1 my-2"
|
||||||
|
>
|
||||||
|
{c.messageText.toString()}{" "}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<small className="flex items-center gap-1 cursor-pointer">
|
||||||
|
(TS{typeof c !== "string" && c.code})
|
||||||
|
<ExternalLink size={16} />
|
||||||
|
</small>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<Link
|
||||||
|
noExtraIcons
|
||||||
|
target="_blank"
|
||||||
|
href={`https://typescript.tv/errors/#ts${c.code}`}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>typescript.tv</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
noExtraIcons
|
||||||
|
target="_blank"
|
||||||
|
href={`https://ts-error-translator.vercel.app/?error=${compressToEncodedURIComponent(c.messageText.toString())}`}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>ts-error-translator</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* MHSF, Minehut Server List
|
||||||
|
* All external content is rather licensed under the ECA Agreement
|
||||||
|
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||||
|
*
|
||||||
|
* All code under MHSF is licensed under the MIT License
|
||||||
|
* by open source contributors
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 dvelo
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { tryCatch } from "@/lib/try-catch";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { SyntaxErrorInterface } from "./custom-errors";
|
||||||
|
|
||||||
|
export function CustomLint({
|
||||||
|
successfullyLinted,
|
||||||
|
setSuccessfullyLinted,
|
||||||
|
value,
|
||||||
|
syntaxErrors,
|
||||||
|
}: {
|
||||||
|
successfullyLinted: boolean;
|
||||||
|
setSuccessfullyLinted: (change: boolean) => void;
|
||||||
|
value: string;
|
||||||
|
syntaxErrors: SyntaxErrorInterface;
|
||||||
|
}) {
|
||||||
|
const lintFile = async () => {
|
||||||
|
toast.info("Transpiling TypeScript...");
|
||||||
|
const { error, data: transpiledCode } = await tryCatch(
|
||||||
|
(async () => transpileTypeScript(value))(),
|
||||||
|
);
|
||||||
|
if (error) {
|
||||||
|
toast.error(`Failed to transpile TypeScript! Error: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const startTime = Date.now();
|
||||||
|
if (transpiledCode === null) {
|
||||||
|
toast.error("Cannot continue.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.info("Generating function...");
|
||||||
|
const functionBody = transpiledCode.match(
|
||||||
|
/function\s+filter\s*\([^)]*\)\s*\{([\s\S]*)\}/,
|
||||||
|
)?.[1];
|
||||||
|
const { error: filterErr, data: filterFunc } = await tryCatch(
|
||||||
|
(async () => new Function("data", functionBody as string))(),
|
||||||
|
);
|
||||||
|
if (filterErr) {
|
||||||
|
toast.error(`Failed to generate function! Error: ${filterErr.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof filterFunc === "function") {
|
||||||
|
toast.success(`Linted in ${Date.now() - startTime}ms`);
|
||||||
|
setSuccessfullyLinted(true);
|
||||||
|
} else {
|
||||||
|
toast.error("Code doesn't have a 'filter' function. Cannot be tested.");
|
||||||
|
toast.error(typeof filterFunc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={lintFile}
|
||||||
|
disabled={syntaxErrors === null || syntaxErrors.length !== 0}
|
||||||
|
variant={successfullyLinted ? "success-subtle" : "secondary"}
|
||||||
|
>
|
||||||
|
Lint
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
apps/www/src/components/feat/server-list/modification/custom-files/custom-test-success.tsx
Normal file
150
apps/www/src/components/feat/server-list/modification/custom-files/custom-test-success.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* MHSF, Minehut Server List
|
||||||
|
* All external content is rather licensed under the ECA Agreement
|
||||||
|
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||||
|
*
|
||||||
|
* All code under MHSF is licensed under the MIT License
|
||||||
|
* by open source contributors
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 dvelo
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { ClerkCustomActivatedModification } from "../modification-file-creation-dialog";
|
||||||
|
import { useUser } from "@clerk/nextjs";
|
||||||
|
import { Alert } from "@/components/ui/alert";
|
||||||
|
import { Material } from "@/components/ui/material";
|
||||||
|
import {
|
||||||
|
Setting,
|
||||||
|
SettingContent,
|
||||||
|
SettingDescription,
|
||||||
|
SettingMeta,
|
||||||
|
SettingTitle,
|
||||||
|
} from "@/components/feat/settings/setting";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
||||||
|
import { useRouter } from "@/lib/useRouter";
|
||||||
|
|
||||||
|
export function CustomTestSuccess({
|
||||||
|
filename,
|
||||||
|
testMode,
|
||||||
|
value,
|
||||||
|
}: { filename: string; testMode: "filter" | "sort" | ""; value: string }) {
|
||||||
|
const { user } = useUser();
|
||||||
|
const [friendlyName, setFriendlyName] = useState("");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="text-sm my-2">
|
||||||
|
You can now activate this custom modification. Please note that the
|
||||||
|
filter and sort versions of your modifications will be different, and
|
||||||
|
the one used will be selected based on what type you tested on.
|
||||||
|
</p>
|
||||||
|
{(
|
||||||
|
(user?.unsafeMetadata
|
||||||
|
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
|
||||||
|
).find(
|
||||||
|
(c) => c.originalFileName === filename && c.testMode === testMode,
|
||||||
|
) !== undefined && (
|
||||||
|
<Alert className="mb-2 gap-2" variant="warning">
|
||||||
|
This modification was already activated! Hitting activate here will
|
||||||
|
just overwrite the contents and the new friendly name.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Material>
|
||||||
|
<Setting>
|
||||||
|
<SettingContent>
|
||||||
|
<SettingMeta>
|
||||||
|
<SettingTitle>Name</SettingTitle>
|
||||||
|
<SettingDescription>
|
||||||
|
Set a friendly name for your modification.
|
||||||
|
</SettingDescription>
|
||||||
|
</SettingMeta>
|
||||||
|
<Input
|
||||||
|
placeholder="My cool mod"
|
||||||
|
value={friendlyName}
|
||||||
|
onChange={(c) => setFriendlyName(c.target.value)}
|
||||||
|
/>
|
||||||
|
</SettingContent>
|
||||||
|
</Setting>
|
||||||
|
</Material>
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button
|
||||||
|
className="w-full my-2"
|
||||||
|
disabled={friendlyName === "" || friendlyName.replace(" ", "") === ""}
|
||||||
|
onClick={async () => {
|
||||||
|
const array =
|
||||||
|
(user?.unsafeMetadata
|
||||||
|
.activatedModifications as ClerkCustomActivatedModification[]) ??
|
||||||
|
[];
|
||||||
|
const index = array.findIndex(
|
||||||
|
(c) => c.originalFileName === filename && c.testMode === testMode,
|
||||||
|
);
|
||||||
|
const color = `#${Math.floor(Math.random() * 16777215).toString(16)}`;
|
||||||
|
const transpiledValue = transpileTypeScript(value);
|
||||||
|
|
||||||
|
if (transpiledValue === null)
|
||||||
|
return toast.error("Error transpiling");
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
// Original already exists
|
||||||
|
array[index] = {
|
||||||
|
originalFileName: filename,
|
||||||
|
friendlyName,
|
||||||
|
transpiledContents: transpiledValue,
|
||||||
|
active: true,
|
||||||
|
testMode: testMode as "filter" | "sort",
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
array.push({
|
||||||
|
originalFileName: filename,
|
||||||
|
friendlyName,
|
||||||
|
transpiledContents: transpiledValue,
|
||||||
|
active: true,
|
||||||
|
testMode: testMode as "filter" | "sort",
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await user?.update({
|
||||||
|
unsafeMetadata: {
|
||||||
|
...user.unsafeMetadata,
|
||||||
|
activatedModifications: array,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Activated!");
|
||||||
|
router.push("/servers/embedded/sl-modification-frame");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Activate
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
* MHSF, Minehut Server List
|
||||||
|
* All external content is rather licensed under the ECA Agreement
|
||||||
|
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||||
|
*
|
||||||
|
* All code under MHSF is licensed under the MIT License
|
||||||
|
* by open source contributors
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 dvelo
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
||||||
|
import { Setting, SettingContent, SettingDescription, SettingMeta, SettingTitle } from "@/components/feat/settings/setting";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Drawer, DrawerContent } from "@/components/ui/drawer";
|
||||||
|
import { Material } from "@/components/ui/material";
|
||||||
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { tryCatch } from "@/lib/try-catch";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { CustomTestSuccess } from "./custom-test-success";
|
||||||
|
|
||||||
|
export function CustomTest({value, successfullyLinted}: {value: string, successfullyLinted: boolean}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [filterEnabled, setFilterEnabled] = useState(true);
|
||||||
|
const [sortEnabled, setSortEnabled] = useState(true);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [fileName, setFileName] = useState("");
|
||||||
|
const [testMode, setTestMode] = useState<"filter" | "sort" | "">("");
|
||||||
|
|
||||||
|
// biome-ignore lint: values needed (but not shown by linter)
|
||||||
|
useEffect(() => setSuccess(false), [value]);
|
||||||
|
|
||||||
|
// biome-ignore lint: values not needed
|
||||||
|
useEffect(() => {
|
||||||
|
setFilterEnabled(true);
|
||||||
|
setSortEnabled(true);
|
||||||
|
setTestMode("");
|
||||||
|
(async () => {
|
||||||
|
const transpiledValue = transpileTypeScript(value);
|
||||||
|
const functionBody = transpiledValue
|
||||||
|
?.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
|
||||||
|
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
|
||||||
|
const { error: filterErr, data: filterFunc } = await tryCatch(
|
||||||
|
(async () =>
|
||||||
|
new Function(
|
||||||
|
"server",
|
||||||
|
`${functionBody}
|
||||||
|
return filter(server)`,
|
||||||
|
))(),
|
||||||
|
);
|
||||||
|
const { error: sortErr, data: sortFunc } = await tryCatch(
|
||||||
|
(async () =>
|
||||||
|
new Function(
|
||||||
|
"serverA",
|
||||||
|
"serverB",
|
||||||
|
`${functionBody}
|
||||||
|
return sort(serverA, serverB)`,
|
||||||
|
))(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filterErr) setFilterEnabled(false);
|
||||||
|
if (sortErr) setSortEnabled(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
filterFunc?.({});
|
||||||
|
} catch (e) {
|
||||||
|
if (String(e).startsWith("ReferenceError: filter is not defined")) {
|
||||||
|
setFilterEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sortFunc?.({}, {});
|
||||||
|
} catch (e) {
|
||||||
|
if (String(e).startsWith("ReferenceError: sort is not defined")) {
|
||||||
|
setSortEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [open, value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button disabled={!successfullyLinted} onClick={() => setOpen(true)}>
|
||||||
|
Test
|
||||||
|
</Button>
|
||||||
|
<Drawer direction="right" open={open} onOpenChange={setOpen}>
|
||||||
|
<DrawerContent className="p-4 min-w-[400px] overflow-x-hidden max-h-screen overflow-y-auto">
|
||||||
|
<p className="text-sm mb-2">
|
||||||
|
You can run an interactive server-list environment with actual
|
||||||
|
online servers to test your modifications.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Material>
|
||||||
|
<Setting>
|
||||||
|
<SettingContent>
|
||||||
|
<SettingMeta>
|
||||||
|
<SettingTitle>Function to test</SettingTitle>
|
||||||
|
<SettingDescription>
|
||||||
|
You can pick to either test a sorting system or a filter.
|
||||||
|
</SettingDescription>
|
||||||
|
</SettingMeta>
|
||||||
|
<Select
|
||||||
|
value={testMode}
|
||||||
|
onValueChange={setTestMode as (change: string) => void}
|
||||||
|
disabled={success}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="filter" disabled={!filterEnabled}>
|
||||||
|
<code>filter</code>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="sort" disabled={!sortEnabled}>
|
||||||
|
<code>sort</code>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</SettingContent>
|
||||||
|
</Setting>
|
||||||
|
</Material>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full mt-2 flex items-center gap-2"
|
||||||
|
disabled={testMode === "" || success}
|
||||||
|
variant={success ? "success-subtle" : "default"}
|
||||||
|
onClick={() => {
|
||||||
|
const t = btoa(value);
|
||||||
|
|
||||||
|
const newTab = window.open(
|
||||||
|
`/servers?tm=${encodeURIComponent(t)}`,
|
||||||
|
);
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
newTab?.dispatchEvent(
|
||||||
|
new Event(`test-mode.enable.${testMode}`),
|
||||||
|
);
|
||||||
|
}, 500);
|
||||||
|
toast.info("Waiting for server tab to pick up thread...");
|
||||||
|
|
||||||
|
newTab?.addEventListener("test-mode.enabled", () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
toast.success("Connected to new tab; continue.");
|
||||||
|
newTab?.addEventListener("test-mode.success", () => {
|
||||||
|
toast.success("Resolved success from thread!");
|
||||||
|
setSuccess(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{success && <Check size={16} />}Test
|
||||||
|
</Button>
|
||||||
|
{success && (
|
||||||
|
<CustomTestSuccess filename={fileName} testMode={testMode} value={value} />
|
||||||
|
)}
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
apps/www/src/components/feat/server-list/modification/modification-custom-modification-row.tsx
Normal file
66
apps/www/src/components/feat/server-list/modification/modification-custom-modification-row.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* 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 { 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";
|
||||||
|
|
||||||
|
export function ModificationCustomModificationRow({category}: {category: string}) {
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
(user?.unsafeMetadata
|
||||||
|
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
|
||||||
|
).map((m) => (
|
||||||
|
<Link
|
||||||
|
href={`/servers/embedded/sl-modification-frame/category/${category}/modification/_custom/${btoa(m.friendlyName)}`}
|
||||||
|
key={m.friendlyName}
|
||||||
|
>
|
||||||
|
<Material
|
||||||
|
elevation="high"
|
||||||
|
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
||||||
|
style={{ backgroundColor: m.color }}
|
||||||
|
>
|
||||||
|
<Binary className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-center w-full flex items-center justify-center">
|
||||||
|
{m.friendlyName}
|
||||||
|
</span>
|
||||||
|
</Material>
|
||||||
|
</Link>
|
||||||
|
));
|
||||||
|
}
|
||||||
@ -63,6 +63,15 @@ export type ClerkCustomModification = {
|
|||||||
testId?: string;
|
testId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ClerkCustomActivatedModification = {
|
||||||
|
originalFileName: string;
|
||||||
|
friendlyName: string;
|
||||||
|
transpiledContents: string;
|
||||||
|
active: boolean;
|
||||||
|
testMode: "filter" | "sort";
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function ModificationFileCreationDialog({
|
export function ModificationFileCreationDialog({
|
||||||
children,
|
children,
|
||||||
type,
|
type,
|
||||||
@ -98,10 +107,11 @@ export function ModificationFileCreationDialog({
|
|||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={(e) => {
|
onClick={async (e) => {
|
||||||
if (!isSignedIn) return toast.error("Please login.");
|
if (!isSignedIn) return toast.error("Please login.");
|
||||||
user?.update({
|
await user?.update({
|
||||||
unsafeMetadata: {
|
unsafeMetadata: {
|
||||||
|
...user.unsafeMetadata,
|
||||||
customFiles: [
|
customFiles: [
|
||||||
...((user.unsafeMetadata
|
...((user.unsafeMetadata
|
||||||
.customFiles as Array<ClerkCustomModification>) ?? []),
|
.customFiles as Array<ClerkCustomModification>) ?? []),
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { AnimatedText } from "@/components/ui/animated-text";
|
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";
|
import { useQueryState } from "nuqs";
|
||||||
|
|
||||||
export function ServerTestModeSelector({
|
export function ServerTestModeSelector({
|
||||||
@ -18,16 +23,27 @@ export function ServerTestModeSelector({
|
|||||||
<div className="pl-5 flex items-center gap-1">
|
<div className="pl-5 flex items-center gap-1">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<span className="relative flex size-2.5 pt-[1px] items-center cursor-pointer">
|
<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
|
||||||
<span className="relative inline-flex size-2.5 rounded-full bg-orange-500" />
|
className={cn(
|
||||||
</span></TooltipTrigger>
|
"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75",
|
||||||
<TooltipContent>Test mode was enabled.</TooltipContent>
|
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>
|
</Tooltip>
|
||||||
<AnimatedText
|
<AnimatedText
|
||||||
className="text-muted-foreground top-[2.5px] left-[6px] min-w-[70vw]"
|
className="text-muted-foreground top-[2.5px] left-[6px] min-w-[70vw]"
|
||||||
text={testModeStatus}
|
text={testModeStatus}
|
||||||
glimmer
|
glimmer={testModeLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
|||||||
import type { RouteParams } from "@/pages/api/v1/server/get/[server]";
|
import type { RouteParams } from "@/pages/api/v1/server/get/[server]";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Setting,
|
Setting,
|
||||||
SettingContent,
|
SettingContent,
|
||||||
SettingDescription,
|
SettingDescription,
|
||||||
SettingMeta,
|
SettingMeta,
|
||||||
SettingTitle,
|
SettingTitle,
|
||||||
} from "../../settings/setting";
|
} from "../../settings/setting";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { codeToHtml } from "shiki";
|
import { codeToHtml } from "shiki";
|
||||||
@ -22,166 +22,178 @@ import { convert } from "../util";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
export function DebugMenu({
|
export function DebugMenu({
|
||||||
debugOptions,
|
debugOptions,
|
||||||
setOpen,
|
setOpen,
|
||||||
open,
|
open,
|
||||||
}: {
|
}: {
|
||||||
debugOptions: {
|
debugOptions: {
|
||||||
serverName: string;
|
serverName: string;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
mhsfData: (MHSFData & RouteParams) | null;
|
mhsfData: (MHSFData & RouteParams) | null;
|
||||||
serverData: ServerResponse | null;
|
serverData: ServerResponse | null;
|
||||||
onlineServerData: OnlineServer | null;
|
onlineServerData: OnlineServer | null;
|
||||||
};
|
};
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (newState: boolean) => void;
|
setOpen: (newState: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const [mhsfShikiParsed, setMHSFShikiParsed] = useState("");
|
const [mhsfShikiParsed, setMHSFShikiParsed] = useState("");
|
||||||
const [mhShikiParsed, setMHShikiParsed] = useState("");
|
const [mhShikiParsed, setMHShikiParsed] = useState("");
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
setMHSFShikiParsed(
|
setMHSFShikiParsed(
|
||||||
await codeToHtml(JSON.stringify(debugOptions.mhsfData, null, 2), {
|
await codeToHtml(JSON.stringify(debugOptions.mhsfData, null, 2), {
|
||||||
lang: "json",
|
lang: "json",
|
||||||
theme: resolvedTheme === "dark" ? "vitesse-dark" : "vitesse-light",
|
theme: resolvedTheme === "dark" ? "vitesse-dark" : "vitesse-light",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
setMHShikiParsed(
|
setMHShikiParsed(
|
||||||
await codeToHtml(JSON.stringify(debugOptions.serverData, null, 2), {
|
await codeToHtml(JSON.stringify(debugOptions.serverData, null, 2), {
|
||||||
lang: "json",
|
lang: "json",
|
||||||
theme: resolvedTheme === "dark" ? "vitesse-dark" : "vitesse-light",
|
theme: resolvedTheme === "dark" ? "vitesse-dark" : "vitesse-light",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
}, [debugOptions]);
|
}, [debugOptions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer onOpenChange={setOpen} open={open} direction="right">
|
<Drawer onOpenChange={setOpen} open={open} direction="right">
|
||||||
<DrawerContent className="p-4 min-w-[600px] overflow-x-hidden max-h-screen overflow-y-auto">
|
<DrawerContent className="p-4 min-w-[600px] overflow-x-hidden max-h-screen overflow-y-auto">
|
||||||
<DrawerTitle className="text-lg mb-3 flex items-center gap-2">
|
<DrawerTitle className="text-lg mb-3 flex items-center gap-2">
|
||||||
<Wrench size={24} /> Debug Options
|
<Wrench size={24} /> Debug Options
|
||||||
</DrawerTitle>
|
</DrawerTitle>
|
||||||
<span className="m-2 mt-1 text-sm">
|
<span className="m-2 mt-1 text-sm">
|
||||||
This data is only designed for developers; it contains every single
|
This data is only designed for developers; it contains every single
|
||||||
piece of information MHSF knows about the server. Could be useful for
|
piece of information MHSF knows about the server. Could be useful for
|
||||||
adding new backend options or endpoints.{" "}
|
adding new backend options or endpoints.{" "}
|
||||||
<strong>
|
<strong>
|
||||||
This only shows up when Debug Mode is enabled. (or when using
|
This only shows up when Debug Mode is enabled. (or when using
|
||||||
Ctrl+Shift+O)
|
Ctrl+Shift+O)
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
<Material className="mb-2">
|
<Material className="mb-2">
|
||||||
<Setting>
|
<Setting>
|
||||||
<SettingContent>
|
<SettingContent>
|
||||||
<SettingMeta>
|
<SettingMeta>
|
||||||
<SettingTitle>Server name</SettingTitle>
|
<SettingTitle>Server name</SettingTitle>
|
||||||
<SettingDescription>
|
<SettingDescription>
|
||||||
Name of server after being parsed through Minehut API (aka
|
Name of server after being parsed through Minehut API (aka
|
||||||
server.name)
|
server.name)
|
||||||
</SettingDescription>
|
</SettingDescription>
|
||||||
</SettingMeta>
|
</SettingMeta>
|
||||||
{debugOptions.serverName}
|
{debugOptions.serverName}
|
||||||
</SettingContent>
|
</SettingContent>
|
||||||
</Setting>
|
</Setting>
|
||||||
</Material>
|
</Material>
|
||||||
<Material className="mb-2">
|
<Material className="mb-2">
|
||||||
<Setting>
|
<Setting>
|
||||||
<SettingContent>
|
<SettingContent>
|
||||||
<SettingMeta>
|
<SettingMeta>
|
||||||
<SettingTitle>Server Id</SettingTitle>
|
<SettingTitle>Server Id</SettingTitle>
|
||||||
<SettingDescription>
|
<SettingDescription>
|
||||||
Passed usually through query
|
Passed usually through query
|
||||||
</SettingDescription>
|
</SettingDescription>
|
||||||
</SettingMeta>
|
</SettingMeta>
|
||||||
{debugOptions.serverId}
|
{debugOptions.serverId}
|
||||||
</SettingContent>
|
</SettingContent>
|
||||||
</Setting>
|
</Setting>
|
||||||
</Material>
|
</Material>
|
||||||
<Material className="mb-2">
|
<Material className="mb-2">
|
||||||
<strong className="flex items-center gap-2">
|
<strong className="flex items-center gap-2">
|
||||||
{debugOptions.serverData === null && <Spinner />} Parsed Minehut
|
{debugOptions.serverData === null && <Spinner />} Parsed Minehut
|
||||||
data
|
data
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
clipboard.writeText(JSON.stringify(debugOptions.serverData))
|
clipboard.writeText(JSON.stringify(debugOptions.serverData))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Copy (no toast!)
|
Copy (no toast!)
|
||||||
</Button>
|
</Button>
|
||||||
</strong>
|
</strong>
|
||||||
<span
|
<span
|
||||||
dangerouslySetInnerHTML={{ __html: mhShikiParsed }}
|
dangerouslySetInnerHTML={{ __html: mhShikiParsed }}
|
||||||
className="break-all max-w-[100px]"
|
className="break-all max-w-[100px]"
|
||||||
/>
|
/>
|
||||||
</Material>
|
</Material>
|
||||||
<Material className="mb-2">
|
<Material className="mb-2">
|
||||||
<strong className="flex items-center gap-2">
|
<strong className="flex items-center gap-2">
|
||||||
{debugOptions.mhsfData === null && <Spinner />} Parsed MHSF data
|
{debugOptions.mhsfData === null && <Spinner />} Parsed MHSF data
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
clipboard.writeText(JSON.stringify(debugOptions.mhsfData))
|
clipboard.writeText(JSON.stringify(debugOptions.mhsfData))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Copy (no toast!)
|
Copy (no toast!)
|
||||||
</Button>
|
</Button>
|
||||||
</strong>
|
</strong>
|
||||||
{debugOptions.mhsfData !== null && (
|
{debugOptions.mhsfData !== null && (
|
||||||
<>
|
<>
|
||||||
<Setting className="py-3">
|
<Setting className="py-3">
|
||||||
<SettingContent>
|
<SettingContent>
|
||||||
<SettingMeta>
|
<SettingMeta>
|
||||||
<SettingTitle>See all data</SettingTitle>
|
<SettingTitle>See all data</SettingTitle>
|
||||||
<SettingDescription>
|
<SettingDescription>
|
||||||
WARNING: this data is MASSIVE. (@keyboard yk what else is
|
WARNING: this data is MASSIVE. (@keyboard yk what else is
|
||||||
massive?)
|
massive?)
|
||||||
</SettingDescription>
|
</SettingDescription>
|
||||||
</SettingMeta>
|
</SettingMeta>
|
||||||
<DebugShikiParsedDrawer shikiParsed={mhsfShikiParsed}>
|
<DebugShikiParsedDrawer shikiParsed={mhsfShikiParsed}>
|
||||||
<Button>Open data</Button>
|
<Button>Open data</Button>
|
||||||
</DebugShikiParsedDrawer>
|
</DebugShikiParsedDrawer>
|
||||||
</SettingContent>
|
</SettingContent>
|
||||||
</Setting>
|
</Setting>
|
||||||
<Setting className="py-3">
|
<Setting className="py-3">
|
||||||
<SettingContent>
|
{debugOptions.mhsfData !== undefined && <SettingContent>
|
||||||
<SettingMeta>
|
<SettingMeta>
|
||||||
<SettingTitle>Total Statistical Data Count</SettingTitle>
|
<SettingTitle>Total Statistical Data Count</SettingTitle>
|
||||||
<SettingDescription>
|
<SettingDescription>
|
||||||
How many times has MHSF grabbed data about this server?
|
How many times has MHSF grabbed data about this server?
|
||||||
</SettingDescription>
|
</SettingDescription>
|
||||||
</SettingMeta>
|
</SettingMeta>
|
||||||
{convert(
|
{convert(
|
||||||
debugOptions.mhsfData.achievements.historically.length +
|
(
|
||||||
debugOptions.mhsfData.playerData.historically.length +
|
debugOptions.mhsfData.achievements ?? {
|
||||||
debugOptions.mhsfData.favoriteData.favoriteHistoricalData
|
historically: { length: 0 },
|
||||||
.length
|
}
|
||||||
)}
|
).historically.length +
|
||||||
</SettingContent>
|
(
|
||||||
</Setting>
|
debugOptions.mhsfData.playerData ?? {
|
||||||
|
historically: { length: 0 },
|
||||||
|
}
|
||||||
|
).historically.length +
|
||||||
|
(
|
||||||
|
debugOptions.mhsfData.favoriteData ?? {
|
||||||
|
favoriteHistoricalData: { length: 0 },
|
||||||
|
}
|
||||||
|
).favoriteHistoricalData.length,
|
||||||
|
)}
|
||||||
|
</SettingContent>}
|
||||||
|
|
||||||
|
</Setting>
|
||||||
|
|
||||||
<Setting className="py-3">
|
<Setting className="py-3">
|
||||||
<SettingContent>
|
<SettingContent>
|
||||||
<SettingMeta>
|
<SettingMeta>
|
||||||
<SettingTitle>
|
<SettingTitle>
|
||||||
Disable image caching on customization images
|
Disable image caching on customization images
|
||||||
</SettingTitle>
|
</SettingTitle>
|
||||||
<SettingDescription>
|
<SettingDescription>
|
||||||
Enabling this could result in being tracked but{" "}
|
Enabling this could result in being tracked but{" "}
|
||||||
<strong>very rarely</strong> could render the image
|
<strong>very rarely</strong> could render the image
|
||||||
faster. (removes wsrv.nl caching)
|
faster. (removes wsrv.nl caching)
|
||||||
</SettingDescription>
|
</SettingDescription>
|
||||||
</SettingMeta>
|
</SettingMeta>
|
||||||
<Switch />
|
<Switch />
|
||||||
</SettingContent>
|
</SettingContent>
|
||||||
</Setting>
|
</Setting>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Material>
|
</Material>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import type { ServerResponse } from "@/lib/types/mh-server";
|
|||||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from "@/components/ui/chart";
|
} from "@/components/ui/chart";
|
||||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@ -15,156 +15,176 @@ import { useQueryState } from "nuqs";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { convert } from "../util";
|
import { convert } from "../util";
|
||||||
import { Material } from "@/components/ui/material";
|
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({
|
export function StatisticsMainRow({
|
||||||
server,
|
server,
|
||||||
mhsfData,
|
mhsfData,
|
||||||
}: {
|
}: {
|
||||||
server: ServerResponse;
|
server: ServerResponse;
|
||||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||||
}) {
|
}) {
|
||||||
const [statisticType, setStatisticType] = useQueryState("st", {
|
const [statisticType, setStatisticType] = useQueryState("st", {
|
||||||
defaultValue: "playerCount",
|
defaultValue: "playerCount",
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Material
|
<Material
|
||||||
className="relative col-span-2 h-[250px] max-lg:mt-3"
|
className="relative col-span-2 h-[250px] max-lg:mt-3"
|
||||||
padding="none"
|
padding="none"
|
||||||
>
|
>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<span className="flex gap-4 mb-2">
|
<span className="flex gap-4 mb-2">
|
||||||
<strong className="text-lg">Statistics</strong>
|
<strong className="text-lg">Statistics</strong>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
|
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
|
||||||
"rounded-xl px-2 flex items-center gap-2",
|
"rounded-xl px-2 flex items-center gap-2",
|
||||||
statisticType === "playerCount" &&
|
statisticType === "playerCount" &&
|
||||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium"
|
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
|
||||||
)}
|
)}
|
||||||
onClick={() => setStatisticType("playerCount")}
|
onClick={() => setStatisticType("playerCount")}
|
||||||
>
|
>
|
||||||
Player Count
|
Player Count
|
||||||
<Badge className="px-1">
|
<Badge className="px-1">
|
||||||
<code>{convert(server.joins)}</code>
|
<code>{convert(server.joins)}</code>
|
||||||
</Badge>
|
</Badge>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
|
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
|
||||||
"rounded-xl px-2 flex items-center gap-2",
|
"rounded-xl px-2 flex items-center gap-2",
|
||||||
statisticType === "favorites" &&
|
statisticType === "favorites" &&
|
||||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium"
|
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
|
||||||
)}
|
)}
|
||||||
onClick={() => setStatisticType("favorites")}
|
onClick={() => setStatisticType("favorites")}
|
||||||
>
|
>
|
||||||
Favorites
|
Favorites
|
||||||
<Badge className="px-1">
|
<Badge className="px-1">
|
||||||
<code>
|
<code>
|
||||||
{convert(
|
{convert(
|
||||||
mhsfData.server?.favoriteData.favoriteNumber as number
|
mhsfData.server?.favoriteData.favoriteNumber as number,
|
||||||
)}
|
)}
|
||||||
</code>
|
</code>
|
||||||
</Badge>
|
</Badge>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{!mhsfData.loading && (
|
{!mhsfData.loading ? (
|
||||||
<StatisticsChart
|
<>
|
||||||
data={
|
{(statisticType === "playerCount"
|
||||||
statisticType === "playerCount"
|
? mhsfData.server?.playerData.historically
|
||||||
? mhsfData.server?.playerData.historically
|
: mhsfData.server?.favoriteData.favoriteHistoricalData
|
||||||
: mhsfData.server?.favoriteData.favoriteHistoricalData
|
)?.length !== 0 ? (
|
||||||
}
|
<StatisticsChart
|
||||||
mainDataPoint={statisticType}
|
data={
|
||||||
/>
|
statisticType === "playerCount"
|
||||||
)}
|
? mhsfData.server?.playerData.historically
|
||||||
</div>
|
: mhsfData.server?.favoriteData.favoriteHistoricalData
|
||||||
</Material>
|
}
|
||||||
);
|
mainDataPoint={statisticType}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="w-full h-full items-center justify-center flex">
|
||||||
|
<Placeholder
|
||||||
|
icon={<CircleSlash />}
|
||||||
|
title="There is no data to be collected"
|
||||||
|
description="This server probably never had any data collected in your choosen timespan."
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Material>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
playerCount: {
|
playerCount: {
|
||||||
label: "Joins",
|
label: "Joins",
|
||||||
color: "hsl(var(--chart-1))",
|
color: "hsl(var(--chart-1))",
|
||||||
},
|
},
|
||||||
favorites: {
|
favorites: {
|
||||||
label: "Favorites",
|
label: "Favorites",
|
||||||
color: "hsl(var(--chart-2))",
|
color: "hsl(var(--chart-2))",
|
||||||
},
|
},
|
||||||
} satisfies ChartConfig;
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export function StatisticsChart({
|
export function StatisticsChart({
|
||||||
data,
|
data,
|
||||||
mainDataPoint,
|
mainDataPoint,
|
||||||
}: {
|
}: {
|
||||||
data: any;
|
data: any;
|
||||||
mainDataPoint: string;
|
mainDataPoint: string;
|
||||||
}) {
|
}) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
return (
|
return (
|
||||||
<ChartContainer config={chartConfig} className="max-h-[202px] min-w-full">
|
<ChartContainer config={chartConfig} className="max-h-[202px] min-w-full">
|
||||||
<AreaChart
|
<AreaChart
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={data.slice(data.length - 30, data.length)}
|
data={data.slice(data.length - 30, data.length)}
|
||||||
margin={{
|
margin={{
|
||||||
top: 30,
|
top: 30,
|
||||||
}}
|
}}
|
||||||
className="rounded-b-xl"
|
className="rounded-b-xl"
|
||||||
>
|
>
|
||||||
<CartesianGrid vertical={false} horizontal={false} />
|
<CartesianGrid vertical={false} horizontal={false} />
|
||||||
<XAxis dataKey="date" tickLine={false} axisLine={false} tick={false} />
|
<XAxis dataKey="date" tickLine={false} axisLine={false} tick={false} />
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
className="w-[150px]"
|
className="w-[150px]"
|
||||||
nameKey={mainDataPoint}
|
nameKey={mainDataPoint}
|
||||||
indicator="line"
|
indicator="line"
|
||||||
labelFormatter={(value) => {
|
labelFormatter={(value) => {
|
||||||
return `${new Date(value).toLocaleDateString("en-US", {
|
return `${new Date(value).toLocaleDateString("en-US", {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
})} ${new Date(value).toLocaleTimeString("en-US", {
|
})} ${new Date(value).toLocaleTimeString("en-US", {
|
||||||
timeStyle: "short",
|
timeStyle: "short",
|
||||||
})}`;
|
})}`;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id={`fill${mainDataPoint}`}
|
id={`fill${mainDataPoint}`}
|
||||||
x1="0"
|
x1="0"
|
||||||
y1="0"
|
y1="0"
|
||||||
x2="0"
|
x2="0"
|
||||||
y2="1"
|
y2="1"
|
||||||
>
|
>
|
||||||
<stop
|
<stop
|
||||||
offset="25%"
|
offset="25%"
|
||||||
stopColor={`var(--color-${mainDataPoint})`}
|
stopColor={`var(--color-${mainDataPoint})`}
|
||||||
stopOpacity={0.8}
|
stopOpacity={0.8}
|
||||||
/>
|
/>
|
||||||
<stop
|
<stop
|
||||||
offset="95%"
|
offset="95%"
|
||||||
stopColor={`var(--color-${mainDataPoint})`}
|
stopColor={`var(--color-${mainDataPoint})`}
|
||||||
stopOpacity={0.1}
|
stopOpacity={0.1}
|
||||||
/>
|
/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<Area
|
<Area
|
||||||
dataKey={mainDataPoint}
|
dataKey={mainDataPoint}
|
||||||
type="natural"
|
type="natural"
|
||||||
fill={`url(#fill${mainDataPoint})`}
|
fill={`url(#fill${mainDataPoint})`}
|
||||||
fillOpacity={0.4}
|
fillOpacity={0.4}
|
||||||
stroke={`var(--color-${mainDataPoint})`}
|
stroke={`var(--color-${mainDataPoint})`}
|
||||||
stackId="a"
|
stackId="a"
|
||||||
/>
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,14 +38,12 @@ import { MultisessionAppSupport } from "@clerk/nextjs/internal";
|
|||||||
export const ClerkProvider = ({ children }: { children: React.ReactNode }) => {
|
export const ClerkProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
if (resolvedTheme === "dark") {
|
if (resolvedTheme === "dark") {
|
||||||
console.log(resolvedTheme);
|
|
||||||
return (
|
return (
|
||||||
<ImportedClerkProvider appearance={{ baseTheme: dark }}>
|
<ImportedClerkProvider appearance={{ baseTheme: dark }}>
|
||||||
<MultisessionAppSupport>{children}</MultisessionAppSupport>
|
<MultisessionAppSupport>{children}</MultisessionAppSupport>
|
||||||
</ImportedClerkProvider>
|
</ImportedClerkProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log("a");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImportedClerkProvider>
|
<ImportedClerkProvider>
|
||||||
|
|||||||
@ -51,8 +51,7 @@ type ModDBCategory = {
|
|||||||
|
|
||||||
export const serverModDB: ModDBCategory[] = [
|
export const serverModDB: ModDBCategory[] = [
|
||||||
{
|
{
|
||||||
displayTitle: "Custom Files",
|
displayTitle: "Create Custom Files",
|
||||||
__custom: true,
|
|
||||||
description:
|
description:
|
||||||
`Create custom TypeScript-based filter or sorting systems, completely from the comfort of your own browser.
|
`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.`,
|
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: []
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@ -112,11 +112,10 @@ export const allTags: Array<{
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
condition: async (c) =>
|
condition: async (c) =>
|
||||||
(c.online === undefined ? c.server?.playerCount : c.online.playerData) ===
|
(c.online === undefined ? c.server?.playerCount: c.online.playerData.playerCount) ===
|
||||||
0,
|
0,
|
||||||
htmlDocs: "Nobody is online this server.",
|
htmlDocs: "Nobody is online this server.",
|
||||||
tooltipDesc: "Nobody is online this server.",
|
tooltipDesc: "Nobody is online this server.",
|
||||||
|
|
||||||
role: "gray-subtle",
|
role: "gray-subtle",
|
||||||
docsName: "Nobody Online",
|
docsName: "Nobody Online",
|
||||||
__filter: true,
|
__filter: true,
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@ -36,111 +36,135 @@ import { tryCatch } from "../try-catch";
|
|||||||
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
||||||
|
|
||||||
export function useFilters(data: OnlineServer[]) {
|
export function useFilters(data: OnlineServer[]) {
|
||||||
const [filteredData, setFilteredData] = useState<OnlineServer[]>(data);
|
const [filteredData, setFilteredData] = useState<OnlineServer[]>(data);
|
||||||
const [t] = useQueryState("tm");
|
const [t] = useQueryState("tm");
|
||||||
const [testModeEnabled, setTestModeEnabled] = useState(false);
|
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(
|
||||||
const [testModeLoading, setTestModeLoading] = useState(true);
|
"Haven't connected thread yet (if stuck, select the other tab, and come back)",
|
||||||
|
);
|
||||||
|
const [testModeLoading, setTestModeLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filteredData.length === 0) setFilteredData(data);
|
if (filteredData.length === 0) setFilteredData(data);
|
||||||
}, [data, filteredData.length]);
|
}, [data, filteredData.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
const testModeInit = (type: "filter" | "sort") => {
|
||||||
if (data.length !== 0)
|
window.dispatchEvent(new Event("test-mode.enabled"));
|
||||||
window.addEventListener("test-mode.enable", (c) => {
|
if (!t) {
|
||||||
window.dispatchEvent(new Event("test-mode.enabled"));
|
toast.error("Couldn't enable test mode; no query variable.");
|
||||||
if (!t) {
|
} else {
|
||||||
toast.error("Couldn't enable test mode; no query variable.");
|
setTestModeEnabled(true);
|
||||||
} else {
|
const code = atob(t);
|
||||||
setTestModeEnabled(true);
|
(async () => {
|
||||||
const code = atob(t);
|
setTestModeStatus("Transpiling TypeScript...");
|
||||||
(async () => {
|
const startTime = Date.now();
|
||||||
setTestModeStatus("Transpiling TypeScript...");
|
const { error, data: transpiledCode } = await tryCatch(
|
||||||
const startTime = Date.now();
|
(async () => transpileTypeScript(code))(),
|
||||||
const { error, data: transpiledCode } = await tryCatch(
|
);
|
||||||
(async () => transpileTypeScript(code))()
|
if (error) {
|
||||||
);
|
setTestModeStatus(
|
||||||
if (error) {
|
"Failed to transpile TypeScript! Error: " + error.message,
|
||||||
setTestModeStatus(
|
);
|
||||||
"Failed to transpile TypeScript! Error: " + error.message
|
setTestModeLoading(false);
|
||||||
);
|
return;
|
||||||
setTestModeLoading(false);
|
}
|
||||||
return;
|
if (transpiledCode === null) {
|
||||||
}
|
setTestModeStatus("Cannot continue.");
|
||||||
if (transpiledCode === null) {
|
setTestModeLoading(false);
|
||||||
setTestModeStatus("Cannot continue.");
|
return;
|
||||||
setTestModeLoading(false);
|
}
|
||||||
return;
|
setTestModeStatus("Generating function...");
|
||||||
}
|
if (
|
||||||
console.log(
|
!transpiledCode.includes("export default") &&
|
||||||
"[MHSF Filters] Transpiled TypeScript:",
|
!transpiledCode.includes("export")
|
||||||
transpiledCode ?? ""
|
) {
|
||||||
);
|
setTestModeStatus(
|
||||||
setTestModeStatus("Generating function...");
|
"Transpiled code does not contain any export statements.",
|
||||||
if (
|
);
|
||||||
!transpiledCode.includes("export default") &&
|
setTestModeLoading(false);
|
||||||
!transpiledCode.includes("export")
|
return;
|
||||||
) {
|
}
|
||||||
setTestModeStatus(
|
const functionBody = transpiledCode
|
||||||
"Transpiled code does not contain any export statements."
|
.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
|
||||||
);
|
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
|
||||||
setTestModeLoading(false);
|
const { error: filterErr, data: filterFunc } = await tryCatch(
|
||||||
return;
|
(async () =>
|
||||||
}
|
type === "filter"
|
||||||
const functionBody = transpiledCode
|
? new Function(
|
||||||
.replace(/export default(?!.*[;])/g, "") // Avoid replacing if followed by a semicolon
|
"server",
|
||||||
.replace(/export(?!.*[;])/g, ""); // Avoid replacing if followed by a semicolon
|
`${functionBody}
|
||||||
const { error: filterErr, data: filterFunc } = await tryCatch(
|
|
||||||
(async () =>
|
return filter(server)`,
|
||||||
new Function(
|
)
|
||||||
"server",
|
: new Function(
|
||||||
`${functionBody}
|
"serverA",
|
||||||
|
"serverB",
|
||||||
return filter(server)`
|
`${functionBody}
|
||||||
))()
|
|
||||||
);
|
return sort(serverA, serverB)`,
|
||||||
if (filterErr) {
|
))(),
|
||||||
setTestModeStatus(
|
);
|
||||||
`Failed to generate function! Error: ${filterErr.message}`
|
if (filterErr) {
|
||||||
);
|
setTestModeStatus(
|
||||||
setTestModeLoading(false);
|
`Failed to generate function! Error: ${filterErr.message}`,
|
||||||
return;
|
);
|
||||||
}
|
setTestModeLoading(false);
|
||||||
if (typeof filterFunc === "function") {
|
return;
|
||||||
setTestModeStatus(
|
}
|
||||||
"Compiled in " + (Date.now() - startTime) + "ms"
|
if (typeof filterFunc === "function") {
|
||||||
);
|
setTestModeStatus("Compiled in " + (Date.now() - startTime) + "ms");
|
||||||
toast.promise(
|
toast.promise(
|
||||||
async () => {
|
async () => {
|
||||||
let newServers = [];
|
let newServers = [];
|
||||||
newServers = data.filter((c) => filterFunc(c));
|
if (type === "filter") {
|
||||||
setTestModeStatus(
|
newServers = data.filter((c) => filterFunc(c));
|
||||||
"Server count " + data.length + " -> " + newServers.length
|
setTestModeStatus(
|
||||||
);
|
"Server count " + data.length + " -> " + newServers.length,
|
||||||
setFilteredData(() => [...newServers]);
|
);
|
||||||
setTestModeLoading(false);
|
if (newServers.length === 0)
|
||||||
},
|
setTestModeStatus(
|
||||||
{
|
"No servers were specified in the criteria; showing all servers instead",
|
||||||
loading: "Manipulating data...",
|
);
|
||||||
success: "Manipulated data; test mode finished!",
|
setFilteredData(() => [...newServers]);
|
||||||
error: (e) =>
|
}
|
||||||
`Error while manipulating data; go back to your editor and run again. ${e}`,
|
if (type === "sort") {
|
||||||
}
|
newServers = data.sort((a, b) => filterFunc(a, b));
|
||||||
);
|
setTestModeStatus("Sorted " + newServers.length + " servers.");
|
||||||
} else {
|
console.log(newServers, data.sort((a, b) => filterFunc(a, b)))
|
||||||
setTestModeStatus(
|
console.log(filterFunc)
|
||||||
"Code doesn't have a 'filter' function. Cannot be tested."
|
setFilteredData(() => [...newServers]);
|
||||||
);
|
}
|
||||||
setTestModeLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [t, data]);
|
|
||||||
|
|
||||||
console.log(filteredData, testModeStatus);
|
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.",
|
||||||
|
);
|
||||||
|
setTestModeLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return { filteredData, testModeEnabled, testModeLoading, 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) => {
|
handle: (key: string, callback: (object: any) => void) => {
|
||||||
window.addEventListener('message', (e) => {
|
window.addEventListener('message', (e) => {
|
||||||
console.log(e);
|
|
||||||
if (e.data.__key === key) {
|
if (e.data.__key === key) {
|
||||||
callback(e.data)
|
callback(e.data)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
} from "@clerk/nextjs/server";
|
} from "@clerk/nextjs/server";
|
||||||
import { type NextRequest, NextResponse } from "next/server";
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
import type { ServerResponse } from "./lib/types/mh-server";
|
import type { ServerResponse } from "./lib/types/mh-server";
|
||||||
|
import { getBackendProcedure } from "./lib/backend-procedure";
|
||||||
|
|
||||||
// Thanks for the router matcher API Clerk <3
|
// Thanks for the router matcher API Clerk <3
|
||||||
const isRootRoute = createRouteMatcher(["/"]);
|
const isRootRoute = createRouteMatcher(["/"]);
|
||||||
@ -57,22 +58,6 @@ export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
|
|||||||
return NextResponse.redirect(new URL("/home", req.url));
|
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)) {
|
if (isOldServerRoute(req)) {
|
||||||
const minehut = await fetch(
|
const minehut = await fetch(
|
||||||
`https://api.minehut.com/server/${req.url.split("/server/")[1].split("/")[0]}?byName=true`,
|
`https://api.minehut.com/server/${req.url.split("/server/")[1].split("/")[0]}?byName=true`,
|
||||||
|
|||||||
@ -28,328 +28,337 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getBackendProcedure } from "@/lib/backend-procedure";
|
||||||
import type { MHSFData } from "@/lib/types/data";
|
import type { MHSFData } from "@/lib/types/data";
|
||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from "mongodb";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
export type RouteParams = {
|
export type RouteParams = {
|
||||||
actions: {
|
actions: {
|
||||||
favorite: string;
|
favorite: string;
|
||||||
customize: string;
|
customize: string;
|
||||||
own: string;
|
own: string;
|
||||||
report: string;
|
report: string;
|
||||||
history: {
|
history: {
|
||||||
dailyData: string;
|
dailyData: string;
|
||||||
monthlyData: string;
|
monthlyData: string;
|
||||||
relativeData: string;
|
relativeData: string;
|
||||||
historicalData: string;
|
historicalData: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse<{ server: (MHSFData & RouteParams) | null }>
|
res: NextApiResponse<
|
||||||
|
{ server: (MHSFData & RouteParams) | null } | { error: string }
|
||||||
|
>,
|
||||||
) {
|
) {
|
||||||
const {
|
const backendProcedure = await getBackendProcedure(req);
|
||||||
server,
|
|
||||||
maxFavoriteEntries,
|
|
||||||
favoriteTimespanStart,
|
|
||||||
favoriteTimespanEnd,
|
|
||||||
maxPlayerEntries,
|
|
||||||
playerTimespanStart,
|
|
||||||
playerTimespanEnd,
|
|
||||||
maxAchievementEntries,
|
|
||||||
achievementTimespanStart,
|
|
||||||
achievementTimespanEnd,
|
|
||||||
} = req.query;
|
|
||||||
if (!server) return res.status(400).send({ server: null });
|
|
||||||
|
|
||||||
const serverData = await findServerData(server as string);
|
if (backendProcedure.status !== "OK")
|
||||||
if (!serverData.exists) return res.status(404).send({ server: null });
|
return res.status(403).json({
|
||||||
|
error: `Backend procedure marked request as '${backendProcedure.status}' instead of required 'OK'`,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
server,
|
||||||
|
maxFavoriteEntries,
|
||||||
|
favoriteTimespanStart,
|
||||||
|
favoriteTimespanEnd,
|
||||||
|
maxPlayerEntries,
|
||||||
|
playerTimespanStart,
|
||||||
|
playerTimespanEnd,
|
||||||
|
maxAchievementEntries,
|
||||||
|
achievementTimespanStart,
|
||||||
|
achievementTimespanEnd,
|
||||||
|
} = req.query;
|
||||||
|
if (!server) return res.status(400).send({ server: null });
|
||||||
|
|
||||||
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
const serverData = await findServerData(server as string);
|
||||||
|
if (!serverData.exists) return res.status(404).send({ server: null });
|
||||||
|
|
||||||
try {
|
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
||||||
await mongo.connect();
|
|
||||||
const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
|
||||||
const userId = req.cookies.userId;
|
|
||||||
|
|
||||||
// Run queries in parallel
|
try {
|
||||||
const [favoriteData, customizationData, playerData, achievements] =
|
await mongo.connect();
|
||||||
await Promise.all([
|
const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||||
findFavoriteData(serverData.name, userId, db, {
|
const userId = req.cookies.userId;
|
||||||
maxFavoriteEntries,
|
|
||||||
favoriteTimespanStart,
|
|
||||||
favoriteTimespanEnd,
|
|
||||||
}),
|
|
||||||
findCustomizationData(serverData.name, userId, db),
|
|
||||||
findPlayerData(serverData.name, db, {
|
|
||||||
maxPlayerEntries,
|
|
||||||
playerTimespanStart,
|
|
||||||
playerTimespanEnd,
|
|
||||||
}),
|
|
||||||
findAchievements(serverData.name, db, {
|
|
||||||
maxAchievementEntries,
|
|
||||||
achievementTimespanStart,
|
|
||||||
achievementTimespanEnd,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
res.send({
|
// Run queries in parallel
|
||||||
server: {
|
const [favoriteData, customizationData, playerData, achievements] =
|
||||||
favoriteData,
|
await Promise.all([
|
||||||
customizationData,
|
findFavoriteData(serverData.name, userId, db, {
|
||||||
playerData,
|
maxFavoriteEntries,
|
||||||
achievements,
|
favoriteTimespanStart,
|
||||||
actions: {
|
favoriteTimespanEnd,
|
||||||
history: {
|
}),
|
||||||
dailyData: `/api/v1/server/get/${server}/history/daily-data`,
|
findCustomizationData(serverData.name, userId, db),
|
||||||
monthlyData: `/api/v1/server/get/${server}/history/monthly-data`,
|
findPlayerData(serverData.name, db, {
|
||||||
relativeData: `/api/v1/server/get/${server}/history/relative-data`,
|
maxPlayerEntries,
|
||||||
historicalData: `/api/v1/server/get/${server}/history/historical-data`,
|
playerTimespanStart,
|
||||||
},
|
playerTimespanEnd,
|
||||||
favorite: `/api/v1/server/get/${server}/favorite-server`,
|
}),
|
||||||
customize: `/api/v1/server/get/${server}/customize`,
|
findAchievements(serverData.name, db, {
|
||||||
own: `/api/v1/server/get/${server}/own-server`,
|
maxAchievementEntries,
|
||||||
report: `/api/v1/server/get/${server}/report-server`,
|
achievementTimespanStart,
|
||||||
},
|
achievementTimespanEnd,
|
||||||
},
|
}),
|
||||||
});
|
]);
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing request:", error);
|
res.send({
|
||||||
res.status(500).send({ server: null });
|
server: {
|
||||||
} finally {
|
favoriteData,
|
||||||
await mongo.close();
|
customizationData,
|
||||||
}
|
playerData,
|
||||||
|
achievements,
|
||||||
|
actions: {
|
||||||
|
history: {
|
||||||
|
dailyData: `/api/v1/server/get/${server}/history/daily-data`,
|
||||||
|
monthlyData: `/api/v1/server/get/${server}/history/monthly-data`,
|
||||||
|
relativeData: `/api/v1/server/get/${server}/history/relative-data`,
|
||||||
|
historicalData: `/api/v1/server/get/${server}/history/historical-data`,
|
||||||
|
},
|
||||||
|
favorite: `/api/v1/server/get/${server}/favorite-server`,
|
||||||
|
customize: `/api/v1/server/get/${server}/customize`,
|
||||||
|
own: `/api/v1/server/get/${server}/own-server`,
|
||||||
|
report: `/api/v1/server/get/${server}/report-server`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing request:", error);
|
||||||
|
res.status(500).send({ server: null });
|
||||||
|
} finally {
|
||||||
|
await mongo.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findCustomizationData(
|
async function findCustomizationData(
|
||||||
serverName: string,
|
serverName: string,
|
||||||
userId: string | undefined,
|
userId: string | undefined,
|
||||||
db: any
|
db: any,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
description: string | undefined;
|
description: string | undefined;
|
||||||
banner: string | undefined;
|
banner: string | undefined;
|
||||||
discord: string | undefined;
|
discord: string | undefined;
|
||||||
colorScheme: string | undefined;
|
colorScheme: string | undefined;
|
||||||
userProfilePicture: string | undefined;
|
userProfilePicture: string | undefined;
|
||||||
isOwned: boolean;
|
isOwned: boolean;
|
||||||
isOwnedByUser: boolean;
|
isOwnedByUser: boolean;
|
||||||
}> {
|
}> {
|
||||||
// Run queries in parallel
|
// Run queries in parallel
|
||||||
const [customizationData, ownedServerData] = await Promise.all([
|
const [customizationData, ownedServerData] = await Promise.all([
|
||||||
db.collection("customization").findOne({ server: serverName }),
|
db.collection("customization").findOne({ server: serverName }),
|
||||||
userId
|
userId
|
||||||
? db.collection("owned-servers").findOne({ server: serverName })
|
? db.collection("owned-servers").findOne({ server: serverName })
|
||||||
: null,
|
: null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (customizationData) {
|
if (customizationData) {
|
||||||
return {
|
return {
|
||||||
...(customizationData as any),
|
...(customizationData as any),
|
||||||
isOwned: true,
|
isOwned: true,
|
||||||
isOwnedByUser: ownedServerData?.author === userId,
|
isOwnedByUser: ownedServerData?.author === userId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOwned: false,
|
isOwned: false,
|
||||||
isOwnedByUser: false,
|
isOwnedByUser: false,
|
||||||
description: undefined,
|
description: undefined,
|
||||||
banner: undefined,
|
banner: undefined,
|
||||||
discord: undefined,
|
discord: undefined,
|
||||||
colorScheme: undefined,
|
colorScheme: undefined,
|
||||||
userProfilePicture: undefined,
|
userProfilePicture: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findFavoriteData(
|
async function findFavoriteData(
|
||||||
serverName: string,
|
serverName: string,
|
||||||
userId: string | undefined,
|
userId: string | undefined,
|
||||||
db: any,
|
db: any,
|
||||||
query: {
|
query: {
|
||||||
maxFavoriteEntries?: string | string[];
|
maxFavoriteEntries?: string | string[];
|
||||||
favoriteTimespanStart?: string | string[];
|
favoriteTimespanStart?: string | string[];
|
||||||
favoriteTimespanEnd?: string | string[];
|
favoriteTimespanEnd?: string | string[];
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
// Run queries in parallel
|
// Run queries in parallel
|
||||||
const [userFavorites, metaData, historyData] = await Promise.all([
|
const [userFavorites, metaData, historyData] = await Promise.all([
|
||||||
userId ? db.collection("favorites").findOne({ user: userId }) : null,
|
userId ? db.collection("favorites").findOne({ user: userId }) : null,
|
||||||
db.collection("meta").findOne({ server: serverName }),
|
db.collection("meta").findOne({ server: serverName }),
|
||||||
fetchHistoryData(db, serverName, query),
|
fetchHistoryData(db, serverName, query),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Process user favorites
|
// Process user favorites
|
||||||
const favoritedByAccount =
|
const favoritedByAccount =
|
||||||
userId && userFavorites
|
userId && userFavorites
|
||||||
? userFavorites.favorites.includes(serverName)
|
? userFavorites.favorites.includes(serverName)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Process favorite count
|
// Process favorite count
|
||||||
const favoriteNumber = metaData?.favorites || 0;
|
const favoriteNumber = metaData?.favorites || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
favoritedByAccount,
|
favoritedByAccount,
|
||||||
favoriteNumber,
|
favoriteNumber,
|
||||||
favoriteHistoricalData: historyData,
|
favoriteHistoricalData: historyData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchHistoryData(
|
async function fetchHistoryData(
|
||||||
db: any,
|
db: any,
|
||||||
serverName: string,
|
serverName: string,
|
||||||
query: {
|
query: {
|
||||||
maxFavoriteEntries?: string | string[];
|
maxFavoriteEntries?: string | string[];
|
||||||
favoriteTimespanStart?: string | string[];
|
favoriteTimespanStart?: string | string[];
|
||||||
favoriteTimespanEnd?: string | string[];
|
favoriteTimespanEnd?: string | string[];
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
// Build query filter
|
// Build query filter
|
||||||
const filter: any = { server: serverName };
|
const filter: any = { server: serverName };
|
||||||
|
|
||||||
// Add date range filter if provided
|
// Add date range filter if provided
|
||||||
if (query.favoriteTimespanStart && query.favoriteTimespanEnd) {
|
if (query.favoriteTimespanStart && query.favoriteTimespanEnd) {
|
||||||
filter.date = {
|
filter.date = {
|
||||||
$gte: new Date(Number(query.favoriteTimespanStart)),
|
$gte: new Date(Number(query.favoriteTimespanStart)),
|
||||||
$lte: new Date(Number(query.favoriteTimespanEnd)),
|
$lte: new Date(Number(query.favoriteTimespanEnd)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine limit
|
// Determine limit
|
||||||
const limit = query.maxFavoriteEntries ? Number(query.maxFavoriteEntries) : 0;
|
const limit = query.maxFavoriteEntries ? Number(query.maxFavoriteEntries) : 0;
|
||||||
|
|
||||||
// Use projection to only fetch needed fields
|
// Use projection to only fetch needed fields
|
||||||
const projection = { favorites: 1, date: 1, _id: 0 };
|
const projection = { favorites: 1, date: 1, _id: 0 };
|
||||||
|
|
||||||
// Execute optimized query
|
// Execute optimized query
|
||||||
const cursor = db.collection("history").find(filter).project(projection);
|
const cursor = db.collection("history").find(filter).project(projection);
|
||||||
|
|
||||||
// Apply limit if specified
|
// Apply limit if specified
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
cursor.limit(limit);
|
cursor.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await cursor.toArray();
|
return await cursor.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findServerData(
|
async function findServerData(
|
||||||
server: string
|
server: string,
|
||||||
): Promise<{ exists: boolean; name: string }> {
|
): Promise<{ exists: boolean; name: string }> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://api.minehut.com/server/" + server);
|
const response = await fetch("https://api.minehut.com/server/" + server);
|
||||||
|
|
||||||
// Check if the response is ok before parsing JSON
|
// Check if the response is ok before parsing JSON
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return { exists: false, name: "" };
|
return { exists: false, name: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverJSON = await response.json();
|
const serverJSON = await response.json();
|
||||||
if (!serverJSON.server) return { exists: false, name: "" };
|
if (!serverJSON.server) return { exists: false, name: "" };
|
||||||
|
|
||||||
return { exists: true, name: serverJSON.server.name };
|
return { exists: true, name: serverJSON.server.name };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching server data:", error);
|
console.error("Error fetching server data:", error);
|
||||||
return { exists: false, name: "" };
|
return { exists: false, name: "" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findPlayerData(
|
async function findPlayerData(
|
||||||
serverName: string,
|
serverName: string,
|
||||||
db: any,
|
db: any,
|
||||||
query: {
|
query: {
|
||||||
maxPlayerEntries?: string | string[];
|
maxPlayerEntries?: string | string[];
|
||||||
playerTimespanStart?: string | string[];
|
playerTimespanStart?: string | string[];
|
||||||
playerTimespanEnd?: string | string[];
|
playerTimespanEnd?: string | string[];
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
// Get historical player data
|
// Get historical player data
|
||||||
const historyCollection = db.collection("history");
|
const historyCollection = db.collection("history");
|
||||||
|
|
||||||
// Build query filter
|
// Build query filter
|
||||||
const filter: any = { server: serverName };
|
const filter: any = { server: serverName };
|
||||||
|
|
||||||
// Add date range filter if provided
|
// Add date range filter if provided
|
||||||
if (query.playerTimespanStart && query.playerTimespanEnd) {
|
if (query.playerTimespanStart && query.playerTimespanEnd) {
|
||||||
filter.date = {
|
filter.date = {
|
||||||
$gte: new Date(Number(query.playerTimespanStart)),
|
$gte: new Date(Number(query.playerTimespanStart)),
|
||||||
$lte: new Date(Number(query.playerTimespanEnd)),
|
$lte: new Date(Number(query.playerTimespanEnd)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use projection to only fetch needed fields
|
// Use projection to only fetch needed fields
|
||||||
const projection = { player_count: 1, date: 1, _id: 0 };
|
const projection = { player_count: 1, date: 1, _id: 0 };
|
||||||
|
|
||||||
// Get max player count in a single query
|
// Get max player count in a single query
|
||||||
const [maxResult, playerHistory] = await Promise.all([
|
const [maxResult, playerHistory] = await Promise.all([
|
||||||
historyCollection
|
historyCollection
|
||||||
.find({ server: serverName })
|
.find({ server: serverName })
|
||||||
.sort({ player_count: -1 })
|
.sort({ player_count: -1 })
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.project({ player_count: 1 })
|
.project({ player_count: 1 })
|
||||||
.toArray(),
|
.toArray(),
|
||||||
|
|
||||||
historyCollection.find(filter).project(projection).toArray(),
|
historyCollection.find(filter).project(projection).toArray(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Apply limit if specified
|
// Apply limit if specified
|
||||||
let historically = playerHistory;
|
let historically = playerHistory;
|
||||||
if (query.maxPlayerEntries) {
|
if (query.maxPlayerEntries) {
|
||||||
historically = historically.slice(0, Number(query.maxPlayerEntries));
|
historically = historically.slice(0, Number(query.maxPlayerEntries));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the data to match the expected structure
|
// Format the data to match the expected structure
|
||||||
const formattedHistory = historically.map(
|
const formattedHistory = historically.map(
|
||||||
(item: { date: string; player_count?: number }) => ({
|
(item: { date: string; player_count?: number }) => ({
|
||||||
date: item.date,
|
date: item.date,
|
||||||
playerCount: item.player_count || 0,
|
playerCount: item.player_count || 0,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const max = maxResult.length > 0 ? maxResult[0].player_count : 0;
|
const max = maxResult.length > 0 ? maxResult[0].player_count : 0;
|
||||||
|
|
||||||
return { historically: formattedHistory, max };
|
return { historically: formattedHistory, max };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findAchievements(
|
async function findAchievements(
|
||||||
serverName: string,
|
serverName: string,
|
||||||
db: any,
|
db: any,
|
||||||
query: {
|
query: {
|
||||||
maxAchievementEntries?: string | string[];
|
maxAchievementEntries?: string | string[];
|
||||||
achievementTimespanStart?: string | string[];
|
achievementTimespanStart?: string | string[];
|
||||||
achievementTimespanEnd?: string | string[];
|
achievementTimespanEnd?: string | string[];
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
// Get achievements data
|
// Get achievements data
|
||||||
const achievementsCollection = db.collection("achievements");
|
const achievementsCollection = db.collection("achievements");
|
||||||
|
|
||||||
// Build query filter
|
// Build query filter
|
||||||
const filter: any = { name: serverName };
|
const filter: any = { name: serverName };
|
||||||
|
|
||||||
// Add date range filter if provided
|
// Add date range filter if provided
|
||||||
if (query.achievementTimespanStart && query.achievementTimespanEnd) {
|
if (query.achievementTimespanStart && query.achievementTimespanEnd) {
|
||||||
// Assuming there's a timestamp or date field in the achievements collection
|
// Assuming there's a timestamp or date field in the achievements collection
|
||||||
// If it's stored in _id, we might need a different approach
|
// If it's stored in _id, we might need a different approach
|
||||||
filter.timestamp = {
|
filter.timestamp = {
|
||||||
$gte: new Date(Number(query.achievementTimespanStart)),
|
$gte: new Date(Number(query.achievementTimespanStart)),
|
||||||
$lte: new Date(Number(query.achievementTimespanEnd)),
|
$lte: new Date(Number(query.achievementTimespanEnd)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get historical achievements
|
// Get historical achievements
|
||||||
let historically = await achievementsCollection.find(filter).toArray();
|
let historically = await achievementsCollection.find(filter).toArray();
|
||||||
|
|
||||||
// Apply limit if specified
|
// Apply limit if specified
|
||||||
if (query.maxAchievementEntries) {
|
if (query.maxAchievementEntries) {
|
||||||
historically = historically.slice(0, Number(query.maxAchievementEntries));
|
historically = historically.slice(0, Number(query.maxAchievementEntries));
|
||||||
}
|
}
|
||||||
|
|
||||||
const currently: any[] = [];
|
const currently: any[] = [];
|
||||||
for (const a of historically)
|
for (const a of historically)
|
||||||
a.achievements.forEach((item: any, interval: number) =>
|
a.achievements.forEach((item: any, interval: number) =>
|
||||||
currently.push({ interval, ...item })
|
currently.push({ interval, ...item }),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { historically, currently };
|
return { historically, currently };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,11 +33,18 @@ import { clerkClient, getAuth } from "@clerk/nextjs/server";
|
|||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from "mongodb";
|
||||||
import { OnlineServer } from "@/lib/types/mh-server";
|
import { OnlineServer } from "@/lib/types/mh-server";
|
||||||
import { waitUntil } from "@vercel/functions";
|
import { waitUntil } from "@vercel/functions";
|
||||||
|
import { getBackendProcedure } from "@/lib/backend-procedure";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
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 { userId } = getAuth(req);
|
||||||
const { server } = req.query;
|
const { server } = req.query;
|
||||||
|
|
||||||
|
|||||||
73
yarn.lock
73
yarn.lock
@ -99,7 +99,7 @@
|
|||||||
"@babel/helper-string-parser" "^7.25.9"
|
"@babel/helper-string-parser" "^7.25.9"
|
||||||
"@babel/helper-validator-identifier" "^7.25.9"
|
"@babel/helper-validator-identifier" "^7.25.9"
|
||||||
|
|
||||||
"@biomejs/biome@^1.8.3":
|
"@biomejs/biome@^1.9.4":
|
||||||
version "1.9.4"
|
version "1.9.4"
|
||||||
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.9.4.tgz#89766281cbc3a0aae865a7ff13d6aaffea2842bf"
|
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.9.4.tgz#89766281cbc3a0aae865a7ff13d6aaffea2842bf"
|
||||||
integrity sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==
|
integrity sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==
|
||||||
@ -3223,11 +3223,43 @@
|
|||||||
lodash.merge "^4.6.2"
|
lodash.merge "^4.6.2"
|
||||||
postcss-selector-parser "6.0.10"
|
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":
|
"@tootallnate/quickjs-emscripten@^0.23.0":
|
||||||
version "0.23.0"
|
version "0.23.0"
|
||||||
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
|
resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c"
|
||||||
integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
|
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":
|
"@types/acorn@^4.0.0":
|
||||||
version "4.0.6"
|
version "4.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
|
||||||
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
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"
|
version "19.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89"
|
||||||
integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==
|
integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==
|
||||||
@ -3503,13 +3535,20 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@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"
|
version "19.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb"
|
||||||
integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==
|
integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype "^3.0.2"
|
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":
|
"@types/resolve@^1.17.1":
|
||||||
version "1.20.6"
|
version "1.20.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8"
|
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"
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
|
||||||
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
|
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:
|
core-util-is@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
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"
|
call-bound "^1.0.3"
|
||||||
get-intrinsic "^1.2.6"
|
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:
|
is-wsl@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
|
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"
|
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
|
||||||
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
|
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:
|
require-directory@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
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"
|
pirates "^4.0.1"
|
||||||
ts-interface-checker "^0.1.9"
|
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:
|
supports-color@^7.1.0:
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
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"
|
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.4.4.tgz#e00c26e3d7fd9a82af90018ad3137f14e5221630"
|
||||||
integrity sha512-403sqp9t5sx6YGEC32IfZTVWkRAixOQomGYB8kEc6ZD+//LirSxzeCHCnM8EmSXw7l57U1G+Fb0kxgTcKPU/Lg==
|
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"
|
version "2.4.4"
|
||||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.4.4.tgz#cec5dbac5850adebdba71fbdf90e6e9a7723c3d6"
|
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.4.4.tgz#cec5dbac5850adebdba71fbdf90e6e9a7723c3d6"
|
||||||
integrity sha512-N9FDOVaY3yz0YCOhYIgOGYad7+m2ptvinXygw27WPLQvcZDl3+0Sa77KGVlLSiuPDChOUEnTKE9VJwLSi9BPGQ==
|
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"
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
|
||||||
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
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"
|
version "3.24.2"
|
||||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
|
||||||
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==
|
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user