mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-09 08:15:00 -05:00
Compare commits
No commits in common. "7f77a4273ca17b708b27cd892457c362e7698da9" and "21d66742c64093462de20985ba7ada2e236ee544" have entirely different histories.
7f77a4273c
...
21d66742c6
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -1,6 +1,3 @@
|
||||
{
|
||||
"copyright-header-injector.copyrightText": "/*\n * MHSF, Minehut Server List\n * All external content is rather licensed under the ECA Agreement\n * located here: https://mhsf.app/docs/legal/external-content-agreement\n *\n * All code under MHSF is licensed under the MIT License\n * by open source contributors\n *\n * Copyright (c) 2025 dvelo\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n */",
|
||||
"cSpell.words": [
|
||||
"MHSF"
|
||||
]
|
||||
"copyright-header-injector.copyrightText": "/*\n * MHSF, Minehut Server List\n * All external content is rather licensed under the ECA Agreement\n * located here: https://mhsf.app/docs/legal/external-content-agreement\n *\n * All code under MHSF is licensed under the MIT License\n * by open source contributors\n *\n * Copyright (c) 2025 dvelo\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n * OTHER DEALINGS IN THE SOFTWARE.\n */"
|
||||
}
|
||||
@ -51,10 +51,6 @@ const nextConfig = {
|
||||
hostname: "exh89c9lva.ufs.sh",
|
||||
pathname: "/f/*",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "api.mineatar.io"
|
||||
}
|
||||
],
|
||||
},
|
||||
async redirects() {
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
"framer-motion": "^12.7.4",
|
||||
"github-slugger": "^2.0.0",
|
||||
"inngest": "^3.21.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"input-otp": "^1.2.4",
|
||||
"json-beautify": "^1.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.487.0",
|
||||
|
||||
@ -40,37 +40,36 @@ import { Footer } from "@/components/feat/footer/footer";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ClerkProvider>
|
||||
<IsScript>
|
||||
<NuqsAdapter>
|
||||
<div vaul-drawer-wrapper="">
|
||||
<FontBoundary>
|
||||
<TooltipProvider>
|
||||
<Toaster richColors position="top-center" />
|
||||
<ClerkProvider>
|
||||
<NavBar />
|
||||
<div className="pt-16 min-h-screen">{children}</div>
|
||||
<Footer />
|
||||
</ClerkProvider>
|
||||
</TooltipProvider>
|
||||
</FontBoundary>
|
||||
</div>
|
||||
</NuqsAdapter>
|
||||
</IsScript>
|
||||
</ClerkProvider>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ClerkProvider>
|
||||
<IsScript>
|
||||
<NuqsAdapter>
|
||||
<FontBoundary>
|
||||
<TooltipProvider>
|
||||
<Toaster richColors position="top-center" />
|
||||
<ClerkProvider>
|
||||
<NavBar />
|
||||
<div className="pt-16 min-h-screen">{children}</div>
|
||||
<Footer />
|
||||
</ClerkProvider>
|
||||
</TooltipProvider>
|
||||
</FontBoundary>
|
||||
</NuqsAdapter>
|
||||
</IsScript>
|
||||
</ClerkProvider>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ export default function RootLayout({
|
||||
<Toaster richColors position="bottom-center" />
|
||||
|
||||
<NextTopLoader showSpinner={false} />
|
||||
<div className="overflow-x-hidden" >{children}</div>
|
||||
<div className="overflow-x-hidden">{children}</div>
|
||||
</TooltipProvider>
|
||||
</IframeProtector>
|
||||
</FontBoundary>
|
||||
|
||||
@ -41,41 +41,39 @@ import Markdown from "react-markdown";
|
||||
import { invertHex } from "../../page";
|
||||
|
||||
export default function ModificationPage({
|
||||
params,
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ category: string; mod: string }>;
|
||||
params: Promise<{ category: string; mod: string }>;
|
||||
}) {
|
||||
const { category, mod } = use(params);
|
||||
const [backRoute] = useQueryState("b", {
|
||||
defaultValue: "/servers/embedded/sl-modification-frame",
|
||||
});
|
||||
const categoryObj = serverModDB.find(
|
||||
(c) => c.displayTitle === atob(decodeURIComponent(category)),
|
||||
);
|
||||
let modObj = null;
|
||||
if (categoryObj !== undefined)
|
||||
modObj = categoryObj?.entries.find(
|
||||
(c) => c.name === atob(decodeURIComponent(mod)),
|
||||
);
|
||||
const { category, mod } = use(params);
|
||||
const [backRoute] = useQueryState("b", {
|
||||
defaultValue: "/servers/embedded/sl-modification-frame",
|
||||
});
|
||||
const categoryObj = serverModDB.find(
|
||||
(c) => c.displayTitle === atob(decodeURIComponent(category))
|
||||
);
|
||||
let modObj = null;
|
||||
if (categoryObj !== undefined)
|
||||
modObj = categoryObj?.entries.find(
|
||||
(c) => c.name === atob(decodeURIComponent(mod))
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="p-4">
|
||||
<div
|
||||
className="h-[150px] w-full rounded-xl p-2"
|
||||
style={{ backgroundColor: modObj?.color }}
|
||||
>
|
||||
<Link href={backRoute}>
|
||||
<ArrowLeft style={{ color: invertHex(modObj?.color ?? "") }} />
|
||||
</Link>
|
||||
</div>
|
||||
return (
|
||||
<main className="p-4">
|
||||
<div
|
||||
className="h-[150px] w-full rounded-xl p-2"
|
||||
style={{ backgroundColor: modObj?.color }}
|
||||
>
|
||||
<Link href={backRoute}>
|
||||
<ArrowLeft style={{color: invertHex(modObj?.color ?? "")}} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<span className="p-4">
|
||||
<h1 className="text-xl font-bold w-full">{modObj?.name}</h1>
|
||||
<div className="text-wrap pt-2">
|
||||
<Markdown>{modObj?.description}</Markdown>
|
||||
</div>
|
||||
<ModificationAction value={modObj?.value} />
|
||||
</span>
|
||||
</main>
|
||||
);
|
||||
<span className="p-4">
|
||||
<h1 className="text-xl font-bold w-full">{modObj?.name}</h1>
|
||||
<Markdown className="text-wrap pt-2">{modObj?.description}</Markdown>
|
||||
<ModificationAction value={modObj?.value} />
|
||||
</span>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -115,12 +115,10 @@ export default function ModificationPage({
|
||||
|
||||
<span className="p-4">
|
||||
<h1 className="text-xl font-bold w-full">{modObj?.friendlyName}</h1>
|
||||
<div className="text-wrap pt-2">
|
||||
<Markdown>
|
||||
This is a custom modification. Enable it! (or not) It's your own!
|
||||
(are you proud?)
|
||||
</Markdown>
|
||||
</div>
|
||||
<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"
|
||||
|
||||
@ -49,9 +49,11 @@ export default function ServerListModificationFrame() {
|
||||
<main className=" p-4">
|
||||
<h1 className="text-xl font-bold w-full">Filters & Sorting</h1>
|
||||
<div className="flex items-center gap-2 my-2">
|
||||
<Button size="sm">Active modifications</Button>
|
||||
<Link href="/servers/embedded/sl-modification-frame/files">
|
||||
<Button size="sm">Custom files</Button>
|
||||
</Link>
|
||||
<Button size="sm">Settings</Button>
|
||||
</div>
|
||||
<span className="text-wrap pt-2">
|
||||
Pick out different filters & sorting systems to customize your server
|
||||
|
||||
@ -208,7 +208,11 @@
|
||||
--crepe-shadow-2:
|
||||
0px 1px 2px 0px rgba(255, 255, 255, 0.3),
|
||||
0px 2px 6px 2px rgba(255, 255, 255, 0.15) !important;
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
@apply border-zinc-800;
|
||||
}
|
||||
.milkdown-icon {
|
||||
fill: white !important;
|
||||
}
|
||||
|
||||
@ -34,7 +34,6 @@ import { Inter } from "next/font/google";
|
||||
import { X } from "lucide-react";
|
||||
import { Link } from "@/components/util/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@ -45,7 +44,7 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={cn(inter.className, "bg-background font-sans")}>
|
||||
<body className={inter.className}>
|
||||
<noscript>
|
||||
<main className="flex justify-center items-center text-center min-h-screen h-max">
|
||||
<Placeholder
|
||||
|
||||
@ -167,7 +167,7 @@ export default function Embed({ params }: { params: { server: string } }) {
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center transition-all duration-300 ease-in-out",
|
||||
staticMode ? "ml-[42px]" : "group-hover:ml-[42px]"
|
||||
staticMode ? "ml-0" : "group-hover:ml-[42px]"
|
||||
)}
|
||||
>
|
||||
{serverObject && (
|
||||
|
||||
@ -15,7 +15,7 @@ export function Footer() {
|
||||
|
||||
if (!hideFooterPages.includes(pathname ?? ""))
|
||||
return (
|
||||
<footer className="w-full my-15 border-t border-neutral-500/20 bg-neutral-100 dark:border-neutral-700/50 dark:bg-neutral-900 text-muted-foreground">
|
||||
<footer className="w-full mt-15 border-t border-neutral-500/20 bg-neutral-100 dark:border-neutral-700/50 dark:bg-neutral-900 text-muted-foreground">
|
||||
<div className="flex justify-between items-start p-[20px]">
|
||||
<span className="flex items-center gap-4">
|
||||
<Link href="Special:Root">
|
||||
@ -89,7 +89,7 @@ export function Footer() {
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<span className="block px-4">
|
||||
<span className="block px-4 lg:-translate-y-12">
|
||||
<small className="text-[0.75rem]">
|
||||
MHSF is an open-source project licensed under the MIT license. MHSF is
|
||||
not officially affiliated with with Minehut, Super League Enterprise,
|
||||
|
||||
@ -103,7 +103,14 @@ export function ServerList() {
|
||||
</h1>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center">
|
||||
<ModificationButton disabled={testModeEnabled} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<ModificationButton disabled={testModeEnabled} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{filterCount} modification(s) enabled
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<ServerTestModeSelector
|
||||
testModeStatus={testModeStatus}
|
||||
testModeEnabled={testModeEnabled}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
import { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
import { ServerResponse } from "@/lib/types/mh-server";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
export function ServerDiscordRow({
|
||||
server,
|
||||
mhsfData,
|
||||
}: { server: ServerResponse; mhsfData: ReturnType<typeof useMHSFServer> }) {
|
||||
const { user, isSignedIn } = useUser();
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<iframe
|
||||
src={`https://discord.com/widget?id=${mhsfData.server?.customizationData.discord}&theme=${resolvedTheme ?? "dark"}${isSignedIn ? `&username=${user.username}` : ""}`}
|
||||
height={250}
|
||||
// @ts-ignore bro idk what react is on :sob:
|
||||
allowtransparency={true}
|
||||
frameBorder={0}
|
||||
title="Discord Embed"
|
||||
className="w-full relative max-lg:mt-3 rounded-lg"
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -126,7 +126,7 @@ export function MOTDRow({
|
||||
code: CodeHighlight,
|
||||
}}
|
||||
>
|
||||
{mhsfData.server?.customizationData.description?.replaceAll("<br />", "\n")}
|
||||
{mhsfData.server?.customizationData.description}
|
||||
</Markdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,115 +0,0 @@
|
||||
/*
|
||||
* 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 { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Material } from "@/components/ui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import ShikiHighlighter from "react-shiki";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function ServerDiscordBox({
|
||||
mhsfServer,
|
||||
defaultDiscord,
|
||||
}: { mhsfServer: string; defaultDiscord: string }) {
|
||||
const [serverId, setServerId] = useState(defaultDiscord);
|
||||
const [allowed, setAllowed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setAllowed(false);
|
||||
if (
|
||||
serverId.length <= 25 &&
|
||||
serverId.length > 3 &&
|
||||
/^\d+$/.test(serverId as string)
|
||||
) {
|
||||
const { ok, body } = await fetch(
|
||||
`https://discord.com/api/guilds/${serverId}/widget.json`,
|
||||
);
|
||||
if (ok) setAllowed(true);
|
||||
}
|
||||
})();
|
||||
}, [serverId]);
|
||||
|
||||
return (
|
||||
<Material className="grid gap-1 max-h-[700px] mt-2">
|
||||
<strong className="flex items-center gap-2">Discord Embed</strong>
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="border-r p-4">
|
||||
<p className="my-2">
|
||||
Enable Discord widgets in your server settings (Settings -{">"}{" "}
|
||||
Engagement -{">"} Enable Server Widget) to use this. <br />
|
||||
Note: We'll handle all of the query variables on the URL for you
|
||||
(like theming and usernames).
|
||||
</p>
|
||||
<Input
|
||||
placeholder="Server ID"
|
||||
value={serverId}
|
||||
onChange={(e) => setServerId(e.target.value)}
|
||||
label="Enter your server ID shown in your engagement tab"
|
||||
/>
|
||||
<Button
|
||||
className="w-full my-2"
|
||||
disabled={!allowed}
|
||||
onClick={async () => {
|
||||
toast.promise(
|
||||
fetch(
|
||||
`/api/v1/server/get/${mhsfServer}/settings/change-discord?discordServerId=${serverId}`,
|
||||
),
|
||||
{
|
||||
success: "Discord widget enabled",
|
||||
error: "An error occurred",
|
||||
loading: "Enabling Discord widget",
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{serverId !== "" && (
|
||||
<iframe
|
||||
src={`https://discord.com/widget?id=${serverId}&theme=dark`}
|
||||
width={350}
|
||||
height={300}
|
||||
// @ts-ignore bro idk what react is on :sob:
|
||||
allowtransparency={true}
|
||||
frameBorder={0}
|
||||
title="Discord Embed"
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Material>
|
||||
);
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
Setting,
|
||||
SettingContent,
|
||||
SettingDescription,
|
||||
SettingMeta,
|
||||
SettingTitle,
|
||||
} from "@/components/feat/settings/setting";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Material } from "@/components/ui/material";
|
||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function ServerUnownBox({
|
||||
mhsfData,
|
||||
serverData,
|
||||
reset,
|
||||
}: {
|
||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||
serverData: ServerResponse;
|
||||
reset: () => void;
|
||||
}) {
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
return (
|
||||
<Material className="flex items-center p-2 mt-2">
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Unlink server</SettingTitle>
|
||||
<SettingDescription>This cannot be undone.</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="danger">Unlink</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<div className="mx-auto w-full max-w-sm">
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
<p className="text-sm">
|
||||
Unlinking a server will remove all customizations of it and
|
||||
you will not be able to customize your server again until you
|
||||
link the server again.
|
||||
</p>
|
||||
<Input
|
||||
label="Server name"
|
||||
className="mt-2 w-full mb-2"
|
||||
placeholder="Type the name of the server"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
/>
|
||||
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
variant="danger"
|
||||
className="w-full mb-4"
|
||||
disabled={
|
||||
input.toLocaleLowerCase() !==
|
||||
serverData.name.toLocaleLowerCase()
|
||||
}
|
||||
onClick={async () => {
|
||||
toast.promise(
|
||||
fetch(
|
||||
`/api/v1/server/get/${serverData._id}/settings/unlink-server`,
|
||||
)
|
||||
.then((c) => c.json())
|
||||
.then(() => setInput(""))
|
||||
.then(() => reset()),
|
||||
{
|
||||
loading: "Unlinking server",
|
||||
success: "Successfully unlinked server",
|
||||
error: "Failed to unlink server",
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
Unlink
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</Material>
|
||||
);
|
||||
}
|
||||
@ -29,9 +29,6 @@ import { ServerDescriptionBox } from "./customizations/server-description-box";
|
||||
import { ServerBannerBox } from "./customizations/server-banner-box";
|
||||
import { ServerMigrationBox } from "./customizations/server-migration-box";
|
||||
import { ServerColorModeBox } from "./customizations/server-color-mode-box";
|
||||
import { MHSFUser, useUser } from "@/lib/hooks/use-user";
|
||||
import { ServerUnownBox } from "./customizations/server-unown-box";
|
||||
import { ServerDiscordBox } from "./customizations/server-discord-box";
|
||||
|
||||
const successClasses =
|
||||
"bg-green-200 border-green-400 dark:bg-green-800 dark:border-green-600";
|
||||
@ -42,16 +39,15 @@ export function ServerEditorProvider({
|
||||
children,
|
||||
serverData,
|
||||
minehutData,
|
||||
mhsfUser
|
||||
}: {
|
||||
children: ReactNode | ReactNode[];
|
||||
serverData: ReturnType<typeof useMHSFServer>;
|
||||
minehutData: ServerResponse;
|
||||
mhsfUser: {user: MHSFUser | null};
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [onlineData, setOnlineData] = useState<OnlineServer>();
|
||||
const { servers, loading } = useServers();
|
||||
const [claimedUser, setClaimedUser] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("open-server-editor", () => {
|
||||
@ -68,19 +64,28 @@ export function ServerEditorProvider({
|
||||
}
|
||||
}, [open, loading, servers, minehutData.name]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await fetch("/api/v1/user/claimed-user");
|
||||
const json = await response.json();
|
||||
|
||||
setClaimedUser(json.player ?? null);
|
||||
})();
|
||||
});
|
||||
|
||||
const requirementOne = minehutData.online;
|
||||
const requirementTwo = onlineData !== null;
|
||||
const requirementThree = mhsfUser !== null && mhsfUser.user?.claimedUser !== null && mhsfUser.user?.claimedUser?.name === onlineData?.author;
|
||||
const requirementFour = mhsfUser !== null && mhsfUser.user?.claimedUser !== null;
|
||||
const requirementThree = claimedUser === onlineData?.author;
|
||||
const requirementFour = claimedUser !== null;
|
||||
const UploadDropzone = generateUploadDropzone<BannerUploaderRouter>({
|
||||
url: `/api/v1/server/get/${minehutData._id}/settings/upload-banner`,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<MilkdownProvider>
|
||||
<Drawer open={open} onOpenChange={(c) => {
|
||||
serverData.refresh();
|
||||
setOpen(c);
|
||||
}}>
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerContent className="p-4 !max-h-[700px] !h-[700px]">
|
||||
<br />
|
||||
{!serverData.server?.customizationData.isOwned ? (
|
||||
@ -202,20 +207,14 @@ export function ServerEditorProvider({
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
toast.promise(
|
||||
async () => {
|
||||
await serverData.ownServer();
|
||||
await serverData.refresh();
|
||||
},
|
||||
{
|
||||
success: "Successfully owned server",
|
||||
error:
|
||||
"There was an error while linking this server. Please contact support.",
|
||||
loading: "Linking server...",
|
||||
},
|
||||
);
|
||||
}}
|
||||
onClick={() =>
|
||||
toast.promise(serverData.ownServer(), {
|
||||
success: "Successfully owned server",
|
||||
error:
|
||||
"There was an error while linking this server. Please contact support.",
|
||||
loading: "Linking server...",
|
||||
})
|
||||
}
|
||||
disabled={
|
||||
!(
|
||||
requirementOne &&
|
||||
@ -251,12 +250,6 @@ export function ServerEditorProvider({
|
||||
serverData={serverData}
|
||||
minehutData={minehutData}
|
||||
/>
|
||||
<ServerDiscordBox mhsfServer={minehutData._id} defaultDiscord={serverData.server.customizationData.discord ?? ""} />
|
||||
<ServerUnownBox
|
||||
mhsfData={serverData}
|
||||
serverData={minehutData}
|
||||
reset={() => {setOpen(false); serverData.refresh();}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ServerMigrationBox
|
||||
|
||||
@ -12,13 +12,11 @@ import { Button } from "@/components/ui/button";
|
||||
import { DebugProvider } from "./debug/debug-provider";
|
||||
import { ReportingProvider } from "./reporting/reporting-provider";
|
||||
import { ServerEditorProvider } from "./server-editor/server-editor-provider";
|
||||
import { useUser } from "@/lib/hooks/use-user";
|
||||
|
||||
export function ServerProvider({ serverId }: { serverId: string }) {
|
||||
const { server, error, loading, onlineServer } = useServer({ id: serverId });
|
||||
const settings = useSettingsStore();
|
||||
const mhsf = useMHSFServer(serverId);
|
||||
const mhsfUser = useUser();
|
||||
|
||||
if (error !== null)
|
||||
return (
|
||||
@ -74,7 +72,7 @@ export function ServerProvider({ serverId }: { serverId: string }) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-10">
|
||||
<ServerEditorProvider serverData={mhsf} minehutData={server as ServerResponse} mhsfUser={mhsfUser}>
|
||||
<ServerEditorProvider serverData={mhsf} minehutData={server as ServerResponse}>
|
||||
<ReportingProvider server={mhsf}>
|
||||
<ServerMainPage
|
||||
server={server as ServerResponse}
|
||||
|
||||
@ -39,7 +39,6 @@ import { IconsRow } from "./icons/icons-row";
|
||||
import { affiliates } from "./util";
|
||||
import { AffiliateRow } from "./afilliate/affilliate-row";
|
||||
import { EmbedCreatorRow } from "./embeds/embed-creator";
|
||||
import { ServerDiscordRow } from "./discord/server-discord-row";
|
||||
|
||||
export function ServerRows({ server, mhsfData }: { server: ServerResponse, mhsfData: ReturnType<typeof useMHSFServer> }) {
|
||||
const clipboard = useClipboard();
|
||||
@ -47,9 +46,8 @@ export function ServerRows({ server, mhsfData }: { server: ServerResponse, mhsfD
|
||||
return (
|
||||
<span className="lg:grid lg:grid-cols-2 w-full gap-3">
|
||||
{affiliates.includes(server.name) && <AffiliateRow />}
|
||||
<MOTDRow server={server} mhsfData={mhsfData} />
|
||||
<MOTDRow server={server} mhsfData={mhsfData}/>
|
||||
<StatisticsMainRow server={server} mhsfData={mhsfData} />
|
||||
{mhsfData.server?.customizationData.discord !== undefined && <ServerDiscordRow server={server} mhsfData={mhsfData} />}
|
||||
<GeneralInfo server={server} mhsfData={mhsfData} />
|
||||
<AchievementsView server={server} mhsfData={mhsfData} />
|
||||
<IconsRow server={server} mhsfData={mhsfData} />
|
||||
|
||||
@ -1,235 +0,0 @@
|
||||
/*
|
||||
* 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 { Material } from "@/components/ui/material";
|
||||
import {
|
||||
Setting,
|
||||
SettingContent,
|
||||
SettingDescription,
|
||||
SettingMeta,
|
||||
SettingTitle,
|
||||
} from "./setting";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { LinkingDialog } from "./linking-dialog";
|
||||
import { useUser } from "@/lib/hooks/use-user";
|
||||
import { toast } from "sonner";
|
||||
import { useMinecraftHead } from "@/lib/hooks/use-minecraft-head";
|
||||
import Image from "next/image";
|
||||
import { Placeholder } from "@/components/ui/placeholder";
|
||||
import { EllipsisVertical, Star, StarOff } from "lucide-react";
|
||||
import { useRouter } from "@/lib/useRouter";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ServerResponse } from "@/lib/types/mh-server";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffectOnce } from "@/lib/useEffectOnce";
|
||||
|
||||
export function AccountSettings() {
|
||||
const { user, refresh } = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
if (user !== null)
|
||||
return (
|
||||
<Material className="mt-6 grid gap-4">
|
||||
<h2 className="text-xl font-semibold text-inherit">Link Account</h2>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Link Account</SettingTitle>
|
||||
<SettingDescription>
|
||||
Link a Minecraft account to confirm you own a server and
|
||||
customize it with certain information to make it look better to
|
||||
other MHSF users.
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<LinkingDialog refresh={refresh}>
|
||||
<Button disabled={user?.claimedUser !== null}>
|
||||
{user?.claimedUser === null
|
||||
? "Link Account"
|
||||
: "(account already linked)"}
|
||||
</Button>
|
||||
</LinkingDialog>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Unlink Account</SettingTitle>
|
||||
</SettingMeta>
|
||||
<Button
|
||||
variant="danger"
|
||||
disabled={user?.claimedUser === null}
|
||||
onClick={() => {
|
||||
toast.promise(
|
||||
fetch(user?.actions.unlinkAccount as string).then(() =>
|
||||
refresh(),
|
||||
),
|
||||
{
|
||||
loading: "Unlinking...",
|
||||
success: "Unlinked!",
|
||||
error: "Failed to unlink",
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
Unlink
|
||||
</Button>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
{user.claimedUser !== null && (
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Account Name</SettingTitle>
|
||||
</SettingMeta>
|
||||
<div className="flex items-center gap-2">
|
||||
<Image
|
||||
src={`https://api.mineatar.io/face/${user.claimedUser?.uuid}`}
|
||||
alt=""
|
||||
className="rounded"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
{user.claimedUser?.name}
|
||||
</div>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
)}
|
||||
<h2 className="text-xl font-semibold text-inherit">Favorite Servers</h2>
|
||||
{user.favorites !== null && user.favorites.favorites.length > 0 ? (
|
||||
<div>
|
||||
{user.favorites.favorites.map((server, i) => (
|
||||
<Setting key={i}>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>{server}</SettingTitle>
|
||||
</SettingMeta>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={() => router.push(`/server/${server}`)}>
|
||||
Open Server
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="flex items-center"
|
||||
size="square-md"
|
||||
variant="secondary"
|
||||
>
|
||||
<EllipsisVertical size={16} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={async () => {
|
||||
const minehut: { server: ServerResponse } =
|
||||
await fetch(
|
||||
`https://api.minehut.com/server/${server}?byName=true`,
|
||||
).then((c) => c.json());
|
||||
toast.promise(
|
||||
fetch(
|
||||
`/api/v1/server/get/${minehut.server._id}/favorite-server`,
|
||||
).then(() => refresh()),
|
||||
{
|
||||
loading: "Removing favorite...",
|
||||
success: "Removed favorite!",
|
||||
error: "Failed to unfavorite",
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Star size={16} />
|
||||
Unfavorite
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Placeholder
|
||||
title="You have no favorite servers"
|
||||
icon={<StarOff />}
|
||||
/>
|
||||
)}
|
||||
<h2 className="text-xl font-semibold text-inherit">Owned Servers</h2>
|
||||
|
||||
{user.ownedServers !== null && user.ownedServers.length > 0 ? (
|
||||
<div>{user.ownedServers.map((server, i) => <OwnedServer server={server} key={i} />)}</div>
|
||||
) : (
|
||||
<Placeholder
|
||||
title="You have no favorite servers"
|
||||
icon={<StarOff />}
|
||||
/>
|
||||
)}
|
||||
</Material>
|
||||
);
|
||||
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const OwnedServer = ({server}: {server: any}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [name, setName] = useState("");
|
||||
const [joins, setJoins] = useState(0);
|
||||
const router = useRouter();
|
||||
|
||||
useEffectOnce(() => {
|
||||
fetch(`https://api.minehut.com/server/${server.serverId}`)
|
||||
.then((c) => c.json())
|
||||
.then((d: {server: ServerResponse}) => {
|
||||
setLoading(false);
|
||||
setName(d.server.name);
|
||||
setJoins(d.server.joins);
|
||||
});
|
||||
});
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
return (
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>{name}</SettingTitle>
|
||||
<SettingDescription>{joins} joins</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Button onClick={() => router.push(`/server/v2/minehut/${server.serverId}`)}>Open Server</Button>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
);
|
||||
};
|
||||
@ -30,139 +30,113 @@
|
||||
|
||||
import { Material } from "@/components/ui/material";
|
||||
import {
|
||||
Setting,
|
||||
SettingContent,
|
||||
SettingDescription,
|
||||
SettingMeta,
|
||||
SettingTitle,
|
||||
Setting,
|
||||
SettingContent,
|
||||
SettingDescription,
|
||||
SettingMeta,
|
||||
SettingTitle,
|
||||
} from "./setting";
|
||||
import { ModeToggle } from "@/components/util/mode-toggle";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Heart } from "lucide-react";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
|
||||
export function BrowserSettings() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const [fontFamily, setFontFamily] = useState("inter");
|
||||
const [mcFont, setMcFont] = useState(true);
|
||||
const [debugMode, setDebugMode] = useState(false);
|
||||
const { resolvedTheme } = useTheme();
|
||||
const settingsStore = useSettingsStore();
|
||||
const [fontFamily, setFontFamily] = useState("inter");
|
||||
const [mcFont, setMcFont] = useState(true);
|
||||
const [debugMode, setDebugMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
||||
setMcFont((settingsStore.get("mc-font") === "true") as boolean);
|
||||
setDebugMode((settingsStore.get("debug-mode") === "true") as boolean);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
||||
setMcFont((settingsStore.get("mc-font") === "true") as boolean);
|
||||
setDebugMode((settingsStore.get("debug-mode") === "true") as boolean);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Material className="mt-6 grid gap-4">
|
||||
<h2 className="text-xl font-semibold text-inherit">Support</h2>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Donate</SettingTitle>
|
||||
<SettingDescription>
|
||||
Please consider supporting me if you think this project is useful
|
||||
to you, this project is completely open-source and I do not get
|
||||
any money from it.
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<a
|
||||
className="px-3 py-1.5 rounded-lg shadow-none flex items-center gap-2 text-sm transition-all font-medium cursor-pointer duration-75 disabled:opacity-50 disabled:pointer-events-none origin-center dark:bg-pink-400 bg-pink-600 text-white dark:text-black"
|
||||
href="https://buymeacoffee.com/dvelo"
|
||||
>
|
||||
<Heart fill={resolvedTheme === "dark" ? "black" : "white"} size={16} /> Donate
|
||||
</a>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Separator />
|
||||
<h2 className="text-xl font-semibold text-inherit">Appearance</h2>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Color Scheme</SettingTitle>
|
||||
<SettingDescription>
|
||||
Change the MHSF color scheme
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<ModeToggle />
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Use Minecraft font</SettingTitle>
|
||||
<SettingDescription>
|
||||
Use Minecraft font for MOTD. Turning this off restores font
|
||||
settings for MOTD's to a v1-like state.
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Switch
|
||||
checked={mcFont}
|
||||
onCheckedChange={(c) => {
|
||||
settingsStore.set("mc-font", c, false);
|
||||
setMcFont(c);
|
||||
}}
|
||||
/>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Font</SettingTitle>
|
||||
<SettingDescription>
|
||||
Change the default font used in the interface.
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Select
|
||||
defaultValue="inter"
|
||||
value={fontFamily}
|
||||
onValueChange={(c) => {
|
||||
settingsStore.set("font-family", c, false);
|
||||
window.dispatchEvent(new Event("font-family-change"));
|
||||
setFontFamily(c);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="max-w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="inter">Inter</SelectItem>
|
||||
<SelectItem value="geist-sans">Geist Sans</SelectItem>
|
||||
<SelectItem value="system-ui">System UI</SelectItem>
|
||||
<SelectItem value="roboto">Roboto</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Debug Mode</SettingTitle>
|
||||
<SettingDescription>
|
||||
Enable debug mode to show debug options
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Switch
|
||||
checked={debugMode}
|
||||
onCheckedChange={(c) => {
|
||||
settingsStore.set("debug-mode", c, false);
|
||||
window.dispatchEvent(new Event("debug-mode-change"));
|
||||
setDebugMode(c);
|
||||
}}
|
||||
/>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</Material>
|
||||
);
|
||||
return (
|
||||
<Material className="mt-6 grid gap-4">
|
||||
<h2 className="text-xl font-semibold text-inherit">Appearance</h2>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Color Scheme</SettingTitle>
|
||||
<SettingDescription>
|
||||
Change the MHSF color scheme
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<ModeToggle />
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Use Minecraft font</SettingTitle>
|
||||
<SettingDescription>
|
||||
Use Minecraft font for MOTD. Turning this off restores font
|
||||
settings for MOTD's to a v1-like state.
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Switch
|
||||
checked={mcFont}
|
||||
onCheckedChange={(c) => {
|
||||
settingsStore.set("mc-font", c, false);
|
||||
setMcFont(c);
|
||||
}}
|
||||
/>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Font</SettingTitle>
|
||||
<SettingDescription>
|
||||
Change the default font used in the interface.
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Select
|
||||
defaultValue="inter"
|
||||
value={fontFamily}
|
||||
onValueChange={(c) => {
|
||||
settingsStore.set("font-family", c, false);
|
||||
window.dispatchEvent(new Event("font-family-change"));
|
||||
setFontFamily(c);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="max-w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="inter">Inter</SelectItem>
|
||||
<SelectItem value="geist-sans">Geist Sans</SelectItem>
|
||||
<SelectItem value="system-ui">System UI</SelectItem>
|
||||
<SelectItem value="roboto">Roboto</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Debug Mode</SettingTitle>
|
||||
<SettingDescription>Enable debug mode to show debug options</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Switch
|
||||
checked={debugMode}
|
||||
onCheckedChange={(c) => {
|
||||
settingsStore.set("debug-mode", c, false);
|
||||
window.dispatchEvent(new Event("debug-mode-change"));
|
||||
setDebugMode(c);
|
||||
}}
|
||||
/>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</Material>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,248 +0,0 @@
|
||||
/*
|
||||
* 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 Link from "next/link";
|
||||
import { ChevronLeft, ServerOff } from "lucide-react";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { DialogTitle } from "@radix-ui/react-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import Image from "next/image";
|
||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FormSpinner } from "@/components/ui/form-spinner";
|
||||
|
||||
export function LinkingDialog({ children, refresh }: { children: ReactNode, refresh: () => Promise<void> }) {
|
||||
const [step, setStep] = useState(0);
|
||||
const { user } = useUser();
|
||||
const [error, setError] = useState(false);
|
||||
const [code, setCode] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [username, setUserName] = useState("");
|
||||
|
||||
const onSubmit = async (code: string) => {
|
||||
setLoading(true);
|
||||
const fetchRes = await fetch(
|
||||
`/api/v1/user/claim-account-code?code=${code}`,
|
||||
);
|
||||
const json = await fetchRes.json();
|
||||
|
||||
if (!fetchRes.ok) {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setUserName(json.player);
|
||||
setStep(2);
|
||||
setLoading(false);
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onOpenChange={(c) => {
|
||||
if (c) setStep(0);
|
||||
if (c) setLoading(false);
|
||||
if (c) setError(false);
|
||||
if (c) setCode("");
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="absolute h-[427px]">
|
||||
<div className="relative overflow-hidden min-h-full">
|
||||
<div
|
||||
className={`transition-transform duration-300 ease-in-out ${step === 0 ? "translate-x-0" : "-translate-x-full"}`}
|
||||
>
|
||||
<div className="flex flex-col h-[427px]">
|
||||
<div className="flex-grow">
|
||||
<DialogTitle className="font-bold text-2xl">
|
||||
Linking your account
|
||||
</DialogTitle>
|
||||
<p className="my-1 text-sm">
|
||||
In order to link your account, you must have a Minecraft: Java
|
||||
Edition account and be logged into your MHSF account.{" "}
|
||||
<strong>
|
||||
Linking MHSF does NOT involve Microsoft authentication.
|
||||
</strong>
|
||||
</p>
|
||||
<p className="my-1 mb-4 text-sm">
|
||||
<Link href="/server/CoreBoxx" className="underline">
|
||||
CoreBoxx
|
||||
</Link>{" "}
|
||||
has partnered with us to have an integrated account linking
|
||||
feature, which is also open all day.
|
||||
</p>
|
||||
<p className="py-1">
|
||||
<code className="border rounded-full bg-muted h-[1.75rem] w-[1.75rem] absolute inline-flex items-center justify-center">
|
||||
1
|
||||
</code>
|
||||
<span className="ml-[2.25rem] pt-0.5 grid grid-rows-2">
|
||||
<span>Join CoreBoxx</span>
|
||||
|
||||
<code className="border rounded p-2">
|
||||
CoreBoxx.minehut.gg
|
||||
</code>
|
||||
</span>
|
||||
</p>
|
||||
<p className="py-1">
|
||||
<code className="border rounded-full bg-muted h-[1.75rem] w-[1.75rem] absolute inline-flex items-center justify-center">
|
||||
2
|
||||
</code>
|
||||
<span className="ml-[2.25rem] pt-0.5 grid">
|
||||
<span>
|
||||
Link your account using <code>/mhsf</code>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<p className="py-1">
|
||||
<code className="border rounded-full bg-muted h-[1.75rem] w-[1.75rem] absolute inline-flex items-center justify-center">
|
||||
3
|
||||
</code>
|
||||
<span className="ml-[2.25rem] pt-0.5 grid">
|
||||
<span>Input the code returned</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full flex items-center mb-12"
|
||||
onClick={() => setStep(1)}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute h-full top-0 left-0 w-full transition-transform duration-300 ease-in-out ${step === 1 ? "translate-x-0" : "translate-x-full"}`}
|
||||
>
|
||||
<span className="flex p-4 items-center gap-2 text-sm w-full justify-center">
|
||||
<Image
|
||||
alt="Clerk Image"
|
||||
src={
|
||||
user?.imageUrl === undefined
|
||||
? "https://img.clerk.com/preview.png?size=144&seed=seed&initials=AD&isSquare=true&bgType=marble&bgColor=6c47ff&fgType=silhouette&fgColor=FFFFFF&type=user&w=48&q=75"
|
||||
: user?.imageUrl
|
||||
}
|
||||
width={16}
|
||||
height={16}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<p>Signed in as @{user?.username}</p>
|
||||
</span>
|
||||
<strong className="text-center w-full flex items-center justify-center">
|
||||
Enter the code provided on the server:
|
||||
</strong>
|
||||
<div className="p-4 w-full flex items-center justify-center">
|
||||
<div>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
pattern={REGEXP_ONLY_DIGITS}
|
||||
onComplete={onSubmit}
|
||||
value={code}
|
||||
onChange={(c) => {
|
||||
setCode(c);
|
||||
setError(false);
|
||||
}}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
<span className="text-red-400 text-sm">
|
||||
We couldn't find that code being used. Please try again later or
|
||||
double check the code provided to you.
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 w-full",
|
||||
error ? "pt-auto h-[calc(100%-3rem)]" : "mt-6.5 pt-auto h-full",
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className="h-[34px]"
|
||||
onClick={() => setStep(0)}
|
||||
disabled={loading}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => onSubmit(code)}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <FormSpinner /> : "Continue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`absolute h-full top-0 left-0 w-full transition-transform duration-300 ease-in-out ${step === 2 ? "translate-x-0" : "translate-x-full"}`}
|
||||
>
|
||||
<div className="flex flex-col h-[427px]">
|
||||
<div className="flex-grow">
|
||||
<DialogTitle className="font-bold text-2xl">
|
||||
You've linked your account!
|
||||
</DialogTitle>
|
||||
<p className="my-1 text-sm">
|
||||
Congratulations! You've successfully linked your account to{" "}
|
||||
{username}!
|
||||
</p>
|
||||
</div>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="w-full flex items-center mb-12"
|
||||
onClick={() => setStep(1)}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -40,7 +40,6 @@ import { BrowserSettings } from "./browser-settings";
|
||||
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||
import { useEffect, useState } from "react";
|
||||
import { DebugSettings } from "./debug-settings";
|
||||
import { AccountSettings } from "./account-settings";
|
||||
|
||||
export function Settings() {
|
||||
const settingsStore = useSettingsStore();
|
||||
@ -102,7 +101,6 @@ export function Settings() {
|
||||
<DebugSettings />
|
||||
</TabsContent>
|
||||
<TabsContent value="user-settings">
|
||||
<SignedIn><AccountSettings /></SignedIn>
|
||||
<SignedOut>
|
||||
<Material className="mt-6 grid gap-4 py-6">
|
||||
<h3
|
||||
|
||||
@ -53,7 +53,7 @@ export function WaitlistPage() {
|
||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
||||
v2 private beta
|
||||
</h1>
|
||||
<p className="mb-3 text-sm">
|
||||
<p className="mb-3">
|
||||
Hello there! MHSF has an exclusive beta that you may have been invited{" "}
|
||||
<br /> to. Please sign into your account below or follow the
|
||||
instructions.
|
||||
@ -67,8 +67,8 @@ export function WaitlistPage() {
|
||||
<SignedOut>
|
||||
<p>
|
||||
You must be signed in to check for eligibility for this beta. Please
|
||||
make sure you use the Discord connection so we can check if you are
|
||||
eligible for the beta.
|
||||
make sure you use the Discord connection so we can check if you
|
||||
eligibile for the beta.
|
||||
</p>
|
||||
<span className="flex items-center gap-2">
|
||||
<Button onClick={() => clerk.openSignIn()} variant="secondary">
|
||||
@ -131,7 +131,7 @@ export function UserInformation({ discordPage }: { discordPage?: boolean }) {
|
||||
|
||||
{discordData !== undefined && discordData !== null && (
|
||||
<p className="group cursor-pointer flex items-center gap-1">
|
||||
Discord linked as {discordData.global_name ?? discordData.username}
|
||||
Discord linked as {discordData.global_name}
|
||||
<span className="text-muted-foreground hidden group-hover:block">
|
||||
@{discordData.username}
|
||||
</span>
|
||||
|
||||
@ -100,7 +100,7 @@ const DialogContent = React.forwardRef<
|
||||
"dark:border-zinc-900 dark:border-t-zinc-800 dark:border-b-zinc-900",
|
||||
"rounded-2xl max-w-lg box-border mx-auto overscroll-contain shadow-lg overflow-auto",
|
||||
"p-5 flex flex-col gap-2 dark:bg-zinc-950 rounded-xl",
|
||||
"bg-white fixed z-100",
|
||||
"bg-white fixed z-9",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { MinusIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
containerClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof OTPInput> & {
|
||||
containerClassName?: string
|
||||
}) {
|
||||
return (
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
containerClassName
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
index: number
|
||||
}) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
@ -77,8 +77,6 @@ export function FontBoundary({
|
||||
})() as string,
|
||||
"overflow-x-hidden",
|
||||
className,
|
||||
"bg-background",
|
||||
"font-sans"
|
||||
] as string[];
|
||||
|
||||
document.body.classList.add(...classes);
|
||||
|
||||
@ -29,4 +29,4 @@
|
||||
*/
|
||||
|
||||
"use client";
|
||||
export const version = "pb1-2.0";
|
||||
export const version = "2.0";
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
*/
|
||||
//
|
||||
|
||||
import type { Achievement } from "./types/achievement";
|
||||
import { Achievement } from "./types/achievement";
|
||||
|
||||
const connector = (
|
||||
endpoint: string,
|
||||
@ -168,7 +168,7 @@ export async function isFavorited(server: string): Promise<boolean> {
|
||||
export async function getAccountFavorites(): Promise<Array<string>> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
connector(`/user/get`, { version: 1 }),
|
||||
connector(`/user/favorites`, { version: 1 }),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -177,7 +177,7 @@ export async function getAccountFavorites(): Promise<Array<string>> {
|
||||
}
|
||||
);
|
||||
|
||||
return (await response.json()).favorites.favorites;
|
||||
return (await response.json()).result;
|
||||
} catch {
|
||||
throw Error("Not authenticated with a user.");
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ export async function checkOwnedServerMetadata(
|
||||
{ server: serverSelector.name ?? serverData.name },
|
||||
],
|
||||
},
|
||||
{ $set: { serverId: serverSelector.id, customizationVersion: 2, ...changes } },
|
||||
{ $set: changes },
|
||||
{ upsert: true },
|
||||
);
|
||||
},
|
||||
|
||||
@ -36,10 +36,10 @@ import { tryCatch } from "../try-catch";
|
||||
import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import type { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||
import type { ClerkEmbeddedFilter } from "@/components/feat/server-list/modification/modification-action";
|
||||
import { ClerkEmbeddedFilter } from "@/components/feat/server-list/modification/modification-action";
|
||||
import { supportedFilters } from "../types/supportedFilters";
|
||||
|
||||
export type EmbeddedFilter = {
|
||||
type EmbeddedFilter = {
|
||||
identifier: string;
|
||||
functionFilter: (server: OnlineServer) => (boolean | Promise<boolean>);
|
||||
};
|
||||
@ -77,13 +77,13 @@ export function useFilters(data: OnlineServer[]) {
|
||||
if (filteredData.length === 0 || data.length === 0) {
|
||||
window.dispatchEvent(new Event("update-modification-stack"));
|
||||
} else setLoading(false);
|
||||
}, [data, filteredData]);
|
||||
}, [data, filteredData, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length === 0) {
|
||||
window.dispatchEvent(new Event("update-modification-stack"));
|
||||
} else setLoading(false);
|
||||
}, [data, filteredData]);
|
||||
}, [data, filteredData, loading]);
|
||||
|
||||
const testModeInit = (type: "filter" | "sort") => {
|
||||
window.dispatchEvent(new Event("test-mode.enabled"));
|
||||
|
||||
@ -28,16 +28,17 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getAuth, clerkClient } from "@clerk/nextjs/server";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const { userId } = getAuth(req);
|
||||
const { code } = req.query;
|
||||
const { code } = req.body;
|
||||
|
||||
if (code == null) {
|
||||
res.status(400).send({ message: "Couldn't find data" });
|
||||
@ -58,7 +59,7 @@ export default async function handler(
|
||||
res.status(400).send({ message: "Couldn't find code" });
|
||||
return;
|
||||
}
|
||||
await collection.findOneAndDelete({ code });
|
||||
collection.findOneAndDelete({ code });
|
||||
const users = db.collection("claimed-users");
|
||||
await users.insertOne({ player: entry.player, userId });
|
||||
|
||||
@ -68,5 +69,9 @@ export default async function handler(
|
||||
},
|
||||
});
|
||||
|
||||
// Close the database, but don't close this
|
||||
// serverless instance until it happens
|
||||
waitUntil(client.close());
|
||||
|
||||
res.send({ player: entry.player });
|
||||
}
|
||||
@ -28,48 +28,43 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { checkOwnedServerMetadata } from "@/lib/check-owned-server";
|
||||
import { getAuth } from "@clerk/nextjs/server";
|
||||
import { MongoClient } from "mongodb";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { clerkClient, getAuth } from "@clerk/nextjs/server";
|
||||
import z from "zod";
|
||||
|
||||
const obj = z.object({
|
||||
// Use padding on the sides of only the servers, not the whole server list
|
||||
srv: z.boolean(),
|
||||
// Items per row (4-6 rows)
|
||||
ipr: z.number().min(4).max(6),
|
||||
// Padding of server list (0-120px)
|
||||
pad: z.number().min(0).max(120),
|
||||
});
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
try {
|
||||
const { server: serverId, discordServerId } = req.query;
|
||||
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
||||
if (!discordServerId)
|
||||
return res.status(400).send({ error: "No description provided" });
|
||||
if (
|
||||
!(
|
||||
discordServerId.length <= 25 &&
|
||||
discordServerId.length > 3 &&
|
||||
/^\d+$/.test(discordServerId as string)
|
||||
)
|
||||
)
|
||||
return res.status(400).send({ error: "Invalid value" });
|
||||
const { userId } = getAuth(req);
|
||||
|
||||
const { ok } = await fetch(
|
||||
`https://discord.com/api/guilds/${discordServerId}/widget.json`,
|
||||
);
|
||||
|
||||
if (!ok) return res.status(400).send({ error: "Invalid value" });
|
||||
|
||||
const { changeServer } = await checkOwnedServerMetadata(
|
||||
getAuth(req).userId ?? null,
|
||||
mongo,
|
||||
{
|
||||
id: serverId as string,
|
||||
},
|
||||
);
|
||||
|
||||
await changeServer({
|
||||
discord: discordServerId as string,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(400).send({ error: error });
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
return res.send({ message: "Success" });
|
||||
const { data } = req.body;
|
||||
|
||||
if (data === undefined) {
|
||||
res.status(400).send({ message: "Couldn't find data" });
|
||||
return;
|
||||
}
|
||||
|
||||
const v = obj.parse(data);
|
||||
for (const [key, value] of Object.entries(v)) {
|
||||
(await clerkClient()).users.updateUserMetadata(userId, {
|
||||
publicMetadata: {
|
||||
[key]: typeof value === "number" ? value.toString() : value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send({ message: "Success" });
|
||||
}
|
||||
@ -28,28 +28,27 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
|
||||
export function useMinecraftHead(username: string) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const [uuid, setUUID] = useState<string | null>(null);
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
||||
await client.connect();
|
||||
|
||||
useEffect(() => {
|
||||
if (username !== "")
|
||||
fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`)
|
||||
.then((c) => c.json())
|
||||
.then((d) => {
|
||||
setUUID(d.id);
|
||||
});
|
||||
}, [username]);
|
||||
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const collection = db.collection("meta");
|
||||
|
||||
useEffect(() => {
|
||||
if (uuid !== null) {
|
||||
setImageUrl(`https://api.mineatar.io/face/${uuid}`);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [uuid])
|
||||
const all = await collection.find().toArray();
|
||||
const sorted = all.sort((a, b) => a.favorites - b.favorites);
|
||||
sorted.reverse();
|
||||
|
||||
return { loading, imageUrl, uuid };
|
||||
}
|
||||
// Close the database, but don't close this
|
||||
// serverless instance until it happens
|
||||
waitUntil(client.close());
|
||||
|
||||
res.send({ results: sorted });
|
||||
}
|
||||
@ -29,10 +29,9 @@
|
||||
*/
|
||||
|
||||
import { getBackendProcedure } from "@/lib/backend-procedure";
|
||||
import type { Achievement } from "@/lib/types/achievement";
|
||||
import type { ActualCustomization, MHSFData } from "@/lib/types/data";
|
||||
import { clerkClient, getAuth, type User } from "@clerk/nextjs/server";
|
||||
import { type Db, type Filter, type Document, MongoClient, type WithId } from "mongodb";
|
||||
import type { MHSFData } from "@/lib/types/data";
|
||||
import { clerkClient, getAuth, User } from "@clerk/nextjs/server";
|
||||
import { MongoClient } from "mongodb";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export type RouteParams = {
|
||||
@ -115,7 +114,6 @@ export default async function handler(
|
||||
res.send({
|
||||
server: {
|
||||
favoriteData,
|
||||
// @ts-ignore Also don't care what you think.
|
||||
customizationData,
|
||||
playerData,
|
||||
achievements,
|
||||
@ -145,18 +143,15 @@ async function findCustomizationData(
|
||||
serverName: string,
|
||||
serverId: string,
|
||||
userId: string | undefined,
|
||||
db: Db,
|
||||
db: any,
|
||||
): Promise<{
|
||||
description: string | undefined;
|
||||
banner: string | undefined;
|
||||
discord: string | undefined;
|
||||
colorScheme: string | undefined;
|
||||
colorMode: "dark" | "light" | null;
|
||||
customizationVersion: number | undefined;
|
||||
userProfilePicture: string | undefined;
|
||||
isOwned: boolean;
|
||||
isOwnedByUser: boolean;
|
||||
banner: string | undefined;
|
||||
_deletionId: string | undefined;
|
||||
}> {
|
||||
const clerk = await clerkClient();
|
||||
// Run queries in parallel
|
||||
@ -172,33 +167,18 @@ async function findCustomizationData(
|
||||
]);
|
||||
let user: User | undefined = undefined;
|
||||
if (ownedServerData) {
|
||||
|
||||
try {
|
||||
user = await clerk.users.getUser(ownedServerData?.author);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
if (customizationData || ownedServerData) {
|
||||
const baseData: {
|
||||
description?: string;
|
||||
discord?: string;
|
||||
colorScheme?: string;
|
||||
colorMode: "dark" | "light" | null;
|
||||
customizationVersion?: number;
|
||||
banner?: string;
|
||||
_deletionId?: string | undefined;
|
||||
isOwned: boolean;
|
||||
isOwnedByUser: boolean;
|
||||
userProfilePicture: string | undefined;
|
||||
} = {
|
||||
...(customizationData as WithId<ActualCustomization> | null) ?? {},
|
||||
return {
|
||||
...(customizationData as any),
|
||||
isOwned: true,
|
||||
isOwnedByUser: ownedServerData?.author === userId,
|
||||
userProfilePicture: undefined,
|
||||
colorMode: null,
|
||||
customizationVersion: undefined,
|
||||
_deletionId: undefined,
|
||||
userProfilePicture: null,
|
||||
};
|
||||
// @ts-ignore L
|
||||
return baseData
|
||||
}
|
||||
return {
|
||||
isOwned: false,
|
||||
@ -207,51 +187,17 @@ async function findCustomizationData(
|
||||
banner: undefined,
|
||||
discord: undefined,
|
||||
colorScheme: undefined,
|
||||
colorMode: null,
|
||||
customizationVersion: undefined,
|
||||
userProfilePicture: undefined,
|
||||
_deletionId: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (customizationData || ownedServerData) {
|
||||
const baseData: {
|
||||
description?: string;
|
||||
discord?: string;
|
||||
colorScheme?: string;
|
||||
colorMode: "dark" | "light" | null;
|
||||
customizationVersion?: number;
|
||||
banner?: string;
|
||||
_deletionId?: string | undefined;
|
||||
isOwned: boolean;
|
||||
isOwnedByUser: boolean;
|
||||
userProfilePicture: string | undefined;
|
||||
} = {
|
||||
...(customizationData as WithId<ActualCustomization> | null) ?? {},
|
||||
return {
|
||||
...(customizationData as any),
|
||||
isOwned: true,
|
||||
isOwnedByUser: ownedServerData?.author === userId,
|
||||
customizationVersion: customizationData === null ? 2 : customizationData?.customizationVersion,
|
||||
userProfilePicture: undefined,
|
||||
colorMode: customizationData?.colorMode === undefined ? null : customizationData?.colorMode,
|
||||
_deletionId: undefined,
|
||||
};
|
||||
// @ts-ignore L
|
||||
return {
|
||||
description: baseData.description,
|
||||
discord: baseData.discord,
|
||||
colorScheme: baseData.colorScheme,
|
||||
colorMode: baseData.colorMode,
|
||||
customizationVersion: baseData.customizationVersion,
|
||||
userProfilePicture: baseData.userProfilePicture,
|
||||
isOwned: baseData.isOwned,
|
||||
isOwnedByUser: baseData.isOwnedByUser,
|
||||
...(baseData.banner ? {
|
||||
banner: baseData.banner as string,
|
||||
_deletionId: (baseData._deletionId || "") as string,
|
||||
} : {
|
||||
banner: undefined,
|
||||
}),
|
||||
userProfilePicture: userId ? user?.imageUrl : "no user",
|
||||
};
|
||||
}
|
||||
|
||||
@ -262,17 +208,14 @@ async function findCustomizationData(
|
||||
banner: undefined,
|
||||
discord: undefined,
|
||||
colorScheme: undefined,
|
||||
colorMode: null,
|
||||
customizationVersion: undefined,
|
||||
userProfilePicture: undefined,
|
||||
_deletionId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function findFavoriteData(
|
||||
serverName: string,
|
||||
userId: string | undefined,
|
||||
db: Db,
|
||||
db: any,
|
||||
query: {
|
||||
maxFavoriteEntries?: string | string[];
|
||||
favoriteTimespanStart?: string | string[];
|
||||
@ -303,16 +246,16 @@ async function findFavoriteData(
|
||||
}
|
||||
|
||||
async function fetchHistoryData(
|
||||
db: Db,
|
||||
db: any,
|
||||
serverName: string,
|
||||
query: {
|
||||
maxFavoriteEntries?: string | string[];
|
||||
favoriteTimespanStart?: string | string[];
|
||||
favoriteTimespanEnd?: string | string[];
|
||||
},
|
||||
): Promise<{ date: string; favorites: number }[]> {
|
||||
) {
|
||||
// Build query filter
|
||||
const filter: { server: string; date?: { $gte: Date; $lte: Date } } = { server: serverName };
|
||||
const filter: any = { server: serverName };
|
||||
|
||||
// Add date range filter if provided
|
||||
if (query.favoriteTimespanStart && query.favoriteTimespanEnd) {
|
||||
@ -336,18 +279,14 @@ async function fetchHistoryData(
|
||||
cursor.limit(limit);
|
||||
}
|
||||
|
||||
const results = await cursor.toArray();
|
||||
return results.map(doc => ({
|
||||
date: doc.date.toISOString(),
|
||||
favorites: doc.favorites || 0
|
||||
}));
|
||||
return await cursor.toArray();
|
||||
}
|
||||
|
||||
export async function findServerData(
|
||||
server: string,
|
||||
): Promise<{ exists: boolean; name: string }> {
|
||||
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
|
||||
if (!response.ok) {
|
||||
@ -366,7 +305,7 @@ export async function findServerData(
|
||||
|
||||
async function findPlayerData(
|
||||
serverName: string,
|
||||
db: Db,
|
||||
db: any,
|
||||
query: {
|
||||
maxPlayerEntries?: string | string[];
|
||||
playerTimespanStart?: string | string[];
|
||||
@ -377,7 +316,7 @@ async function findPlayerData(
|
||||
const historyCollection = db.collection("history");
|
||||
|
||||
// Build query filter
|
||||
const filter: Filter<Document> = { server: serverName };
|
||||
const filter: any = { server: serverName };
|
||||
|
||||
// Add date range filter if provided
|
||||
if (query.playerTimespanStart && query.playerTimespanEnd) {
|
||||
@ -409,11 +348,10 @@ async function findPlayerData(
|
||||
}
|
||||
|
||||
// Format the data to match the expected structure
|
||||
type HistoryDocument = { date: Date; player_count?: number };
|
||||
const formattedHistory = historically.map(
|
||||
(item) => ({
|
||||
date: (item as HistoryDocument).date.toISOString(),
|
||||
playerCount: (item as HistoryDocument).player_count || 0,
|
||||
(item: { date: string; player_count?: number }) => ({
|
||||
date: item.date,
|
||||
playerCount: item.player_count || 0,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -424,7 +362,7 @@ async function findPlayerData(
|
||||
|
||||
async function findAchievements(
|
||||
serverName: string,
|
||||
db: Db,
|
||||
db: any,
|
||||
query: {
|
||||
maxAchievementEntries?: string | string[];
|
||||
achievementTimespanStart?: string | string[];
|
||||
@ -435,10 +373,12 @@ async function findAchievements(
|
||||
const achievementsCollection = db.collection("achievements");
|
||||
|
||||
// Build query filter
|
||||
const filter: Filter<Document> = { name: serverName };
|
||||
const filter: any = { name: serverName };
|
||||
|
||||
// Add date range filter if provided
|
||||
if (query.achievementTimespanStart && query.achievementTimespanEnd) {
|
||||
// Assuming there's a timestamp or date field in the achievements collection
|
||||
// If it's stored in _id, we might need a different approach
|
||||
filter.timestamp = {
|
||||
$gte: new Date(Number(query.achievementTimespanStart)),
|
||||
$lte: new Date(Number(query.achievementTimespanEnd)),
|
||||
@ -453,17 +393,11 @@ async function findAchievements(
|
||||
historically = historically.slice(0, Number(query.maxAchievementEntries));
|
||||
}
|
||||
|
||||
// Transform the data to match the expected shape
|
||||
const transformedHistorically = historically.map(doc => ({
|
||||
_id: doc._id.toString(),
|
||||
name: doc.name,
|
||||
achievements: doc.achievements || []
|
||||
}));
|
||||
|
||||
const currently: Achievement[] = [];
|
||||
const currently: any[] = [];
|
||||
for (const a of historically)
|
||||
for (const item of a.achievements)
|
||||
currently.push(item);
|
||||
a.achievements.forEach((item: any, interval: number) =>
|
||||
currently.push({ interval, ...item }),
|
||||
);
|
||||
|
||||
return { historically: transformedHistorically, currently };
|
||||
return { historically, currently };
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ export default async function handler(
|
||||
await collection.insertOne({ serverId: server, author: userId });
|
||||
|
||||
// Close the database, but don't close this
|
||||
// serverless instance until it happens
|
||||
// serverless instance until it happens
|
||||
waitUntil(client.close());
|
||||
|
||||
res.send({ message: "Successfully owned server!" });
|
||||
|
||||
@ -28,34 +28,25 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { checkOwnedServerMetadata } from "@/lib/check-owned-server";
|
||||
import { getAuth } from "@clerk/nextjs/server";
|
||||
import { MongoClient } from "mongodb";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getAuth, clerkClient } from "@clerk/nextjs/server";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
try {
|
||||
const { server: serverId } = req.query;
|
||||
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
||||
const { userId } = getAuth(req);
|
||||
|
||||
const { ownedServer, customizedServer, changeServer } =
|
||||
await checkOwnedServerMetadata(getAuth(req).userId ?? null, mongo, {
|
||||
id: serverId as string,
|
||||
});
|
||||
const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
|
||||
await db.collection("customization").findOneAndDelete({
|
||||
$or: [{ serverId: serverId }],
|
||||
});
|
||||
await db.collection("owned-servers").findOneAndDelete({
|
||||
$or: [{ serverId: serverId }],
|
||||
});
|
||||
|
||||
return res.send({ message: "Success" });
|
||||
} catch (error) {
|
||||
return res.status(400).send({ error: error });
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
||||
await client.connect();
|
||||
|
||||
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const users = db.collection("claimed-users");
|
||||
|
||||
return res.send((await users.findOne({ userId })) ?? {player: null});
|
||||
}
|
||||
@ -28,51 +28,34 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { WithId } from "mongodb";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NullLiteral } from "typescript";
|
||||
import type { NextApiResponse, NextApiRequest } from "next";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { getAuth } from "@clerk/nextjs/server";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
|
||||
export type MHSFUser = {
|
||||
favorites: WithId<{
|
||||
/** @note Not important */
|
||||
user: string;
|
||||
/** TODO: should be as a Id */
|
||||
favorites: string[];
|
||||
}> | null;
|
||||
ownedServers: {
|
||||
serverId: string;
|
||||
/** @deprecated use `serverId` instead */
|
||||
server: string;
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { userId } = getAuth(req);
|
||||
|
||||
author: string;
|
||||
}[];
|
||||
claimedUser: { uuid: string; name: string } | null;
|
||||
actions: {
|
||||
linkAccount: string;
|
||||
unlinkAccount: string;
|
||||
};
|
||||
};
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
||||
await client.connect();
|
||||
|
||||
export function useUser(): {
|
||||
user: MHSFUser | null;
|
||||
refresh: () => Promise<void>;
|
||||
} {
|
||||
const [user, setUser] = useState<MHSFUser | null>(null);
|
||||
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const collection = db.collection("favorites");
|
||||
const find = await collection.find({ user: userId }).toArray();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const user = await fetch("/api/v1/user/get");
|
||||
const json = await user.json();
|
||||
setUser(json);
|
||||
})();
|
||||
}, []);
|
||||
// Close the database, but don't close this
|
||||
// serverless instance until it happens
|
||||
waitUntil(client.close());
|
||||
|
||||
return {
|
||||
user,
|
||||
refresh: async () => {
|
||||
const user = await fetch("/api/v1/user/get");
|
||||
const json = await user.json();
|
||||
setUser(json);
|
||||
},
|
||||
};
|
||||
if (find.length == 0) {
|
||||
res.send({ favorites: [] });
|
||||
} else {
|
||||
res.send({ favorites: find[0].favorites });
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
/*
|
||||
* 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 type { MHSFUser } from "@/lib/hooks/use-user";
|
||||
import { getAuth } from "@clerk/nextjs/server";
|
||||
import { MongoClient, type WithId } from "mongodb";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<MHSFUser | { error: string }>,
|
||||
) {
|
||||
const { userId } = getAuth(req);
|
||||
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
||||
await client.connect();
|
||||
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
|
||||
const favoriteCollection = db.collection("favorites");
|
||||
const favorites = (await favoriteCollection.findOne({
|
||||
user: userId,
|
||||
})) as WithId<{ user: string; favorites: string[] }> | null;
|
||||
|
||||
const ownedServersCollection = db.collection("owned-servers");
|
||||
const ownedServers = (await ownedServersCollection
|
||||
.find({ author: userId })
|
||||
.toArray()) as WithId<{
|
||||
serverId: string;
|
||||
author: string;
|
||||
server: string;
|
||||
}>[];
|
||||
|
||||
const claimedUsers = db.collection("claimed-users");
|
||||
const claimedUser = await claimedUsers.findOne({ userId });
|
||||
|
||||
let uuid = "";
|
||||
|
||||
if (claimedUser?.player !== undefined)
|
||||
uuid = await fetch(
|
||||
`https://api.mojang.com/users/profiles/minecraft/${claimedUser?.player ?? ""}`,
|
||||
)
|
||||
.then((c) => c.json())
|
||||
.then((d) => d.id);
|
||||
|
||||
return res.send({
|
||||
favorites,
|
||||
ownedServers,
|
||||
claimedUser:
|
||||
claimedUser === null
|
||||
? null
|
||||
: { name: (claimedUser ?? { player: undefined }).player, uuid },
|
||||
actions: {
|
||||
unlinkAccount: "/api/v1/user/unlink-account",
|
||||
linkAccount: "/api/v1/user/claim-account-code",
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -9380,9 +9380,9 @@ inngest@^3.21.2:
|
||||
ulidx "^2.4.1"
|
||||
zod "~3.22.3"
|
||||
|
||||
input-otp@^1.4.2:
|
||||
input-otp@^1.2.4, input-otp@^1.4.2:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/input-otp/-/input-otp-1.4.2.tgz#f4d3d587d0f641729e55029b3b8c4870847f4f07"
|
||||
resolved "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz"
|
||||
integrity sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==
|
||||
|
||||
inquirer@^12.3.0:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user