mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-09 07:14:59 -05:00
Compare commits
3 Commits
21d66742c6
...
7f77a4273c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f77a4273c | ||
|
|
166cca6931 | ||
|
|
3e1f94bf78 |
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"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 */"
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@ -51,6 +51,10 @@ const nextConfig = {
|
|||||||
hostname: "exh89c9lva.ufs.sh",
|
hostname: "exh89c9lva.ufs.sh",
|
||||||
pathname: "/f/*",
|
pathname: "/f/*",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "api.mineatar.io"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
|
|||||||
@ -68,7 +68,7 @@
|
|||||||
"framer-motion": "^12.7.4",
|
"framer-motion": "^12.7.4",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"inngest": "^3.21.2",
|
"inngest": "^3.21.2",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.4.2",
|
||||||
"json-beautify": "^1.1.1",
|
"json-beautify": "^1.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.487.0",
|
"lucide-react": "^0.487.0",
|
||||||
|
|||||||
@ -40,36 +40,37 @@ import { Footer } from "@/components/feat/footer/footer";
|
|||||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
return (
|
||||||
return (
|
<>
|
||||||
<>
|
<ThemeProvider
|
||||||
<ThemeProvider
|
attribute="class"
|
||||||
attribute="class"
|
defaultTheme="system"
|
||||||
defaultTheme="system"
|
enableSystem
|
||||||
enableSystem
|
disableTransitionOnChange
|
||||||
disableTransitionOnChange
|
>
|
||||||
>
|
<ClerkProvider>
|
||||||
<ClerkProvider>
|
<IsScript>
|
||||||
<IsScript>
|
<NuqsAdapter>
|
||||||
<NuqsAdapter>
|
<div vaul-drawer-wrapper="">
|
||||||
<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>
|
</div>
|
||||||
</IsScript>
|
</NuqsAdapter>
|
||||||
</ClerkProvider>
|
</IsScript>
|
||||||
</ThemeProvider>
|
</ClerkProvider>
|
||||||
</>
|
</ThemeProvider>
|
||||||
);
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export default function RootLayout({
|
|||||||
<Toaster richColors position="bottom-center" />
|
<Toaster richColors position="bottom-center" />
|
||||||
|
|
||||||
<NextTopLoader showSpinner={false} />
|
<NextTopLoader showSpinner={false} />
|
||||||
<div className="overflow-x-hidden">{children}</div>
|
<div className="overflow-x-hidden" >{children}</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</IframeProtector>
|
</IframeProtector>
|
||||||
</FontBoundary>
|
</FontBoundary>
|
||||||
|
|||||||
@ -41,39 +41,41 @@ import Markdown from "react-markdown";
|
|||||||
import { invertHex } from "../../page";
|
import { invertHex } from "../../page";
|
||||||
|
|
||||||
export default function ModificationPage({
|
export default function ModificationPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ category: string; mod: string }>;
|
params: Promise<{ category: string; mod: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { category, mod } = use(params);
|
const { category, mod } = use(params);
|
||||||
const [backRoute] = useQueryState("b", {
|
const [backRoute] = useQueryState("b", {
|
||||||
defaultValue: "/servers/embedded/sl-modification-frame",
|
defaultValue: "/servers/embedded/sl-modification-frame",
|
||||||
});
|
});
|
||||||
const categoryObj = serverModDB.find(
|
const categoryObj = serverModDB.find(
|
||||||
(c) => c.displayTitle === atob(decodeURIComponent(category))
|
(c) => c.displayTitle === atob(decodeURIComponent(category)),
|
||||||
);
|
);
|
||||||
let modObj = null;
|
let modObj = null;
|
||||||
if (categoryObj !== undefined)
|
if (categoryObj !== undefined)
|
||||||
modObj = categoryObj?.entries.find(
|
modObj = categoryObj?.entries.find(
|
||||||
(c) => c.name === atob(decodeURIComponent(mod))
|
(c) => c.name === atob(decodeURIComponent(mod)),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="p-4">
|
<main className="p-4">
|
||||||
<div
|
<div
|
||||||
className="h-[150px] w-full rounded-xl p-2"
|
className="h-[150px] w-full rounded-xl p-2"
|
||||||
style={{ backgroundColor: modObj?.color }}
|
style={{ backgroundColor: modObj?.color }}
|
||||||
>
|
>
|
||||||
<Link href={backRoute}>
|
<Link href={backRoute}>
|
||||||
<ArrowLeft style={{color: invertHex(modObj?.color ?? "")}} />
|
<ArrowLeft style={{ color: invertHex(modObj?.color ?? "") }} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="p-4">
|
<span className="p-4">
|
||||||
<h1 className="text-xl font-bold w-full">{modObj?.name}</h1>
|
<h1 className="text-xl font-bold w-full">{modObj?.name}</h1>
|
||||||
<Markdown className="text-wrap pt-2">{modObj?.description}</Markdown>
|
<div className="text-wrap pt-2">
|
||||||
<ModificationAction value={modObj?.value} />
|
<Markdown>{modObj?.description}</Markdown>
|
||||||
</span>
|
</div>
|
||||||
</main>
|
<ModificationAction value={modObj?.value} />
|
||||||
);
|
</span>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,10 +115,12 @@ export default function ModificationPage({
|
|||||||
|
|
||||||
<span className="p-4">
|
<span className="p-4">
|
||||||
<h1 className="text-xl font-bold w-full">{modObj?.friendlyName}</h1>
|
<h1 className="text-xl font-bold w-full">{modObj?.friendlyName}</h1>
|
||||||
<Markdown className="text-wrap pt-2">
|
<div className="text-wrap pt-2">
|
||||||
This is a custom modification. Enable it! (or not) It's your own! (are
|
<Markdown>
|
||||||
you proud?)
|
This is a custom modification. Enable it! (or not) It's your own!
|
||||||
</Markdown>
|
(are you proud?)
|
||||||
|
</Markdown>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
|
|||||||
@ -49,11 +49,9 @@ export default function ServerListModificationFrame() {
|
|||||||
<main className=" p-4">
|
<main className=" 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>
|
|
||||||
<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>
|
|
||||||
</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
|
||||||
|
|||||||
@ -208,11 +208,7 @@
|
|||||||
--crepe-shadow-2:
|
--crepe-shadow-2:
|
||||||
0px 1px 2px 0px rgba(255, 255, 255, 0.3),
|
0px 1px 2px 0px rgba(255, 255, 255, 0.3),
|
||||||
0px 2px 6px 2px rgba(255, 255, 255, 0.15) !important;
|
0px 2px 6px 2px rgba(255, 255, 255, 0.15) !important;
|
||||||
*,
|
|
||||||
::before,
|
|
||||||
::after {
|
|
||||||
@apply border-zinc-800;
|
|
||||||
}
|
|
||||||
.milkdown-icon {
|
.milkdown-icon {
|
||||||
fill: white !important;
|
fill: white !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { Inter } from "next/font/google";
|
|||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { Link } from "@/components/util/link";
|
import { Link } from "@/components/util/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body className={cn(inter.className, "bg-background font-sans")}>
|
||||||
<noscript>
|
<noscript>
|
||||||
<main className="flex justify-center items-center text-center min-h-screen h-max">
|
<main className="flex justify-center items-center text-center min-h-screen h-max">
|
||||||
<Placeholder
|
<Placeholder
|
||||||
|
|||||||
@ -167,7 +167,7 @@ export default function Embed({ params }: { params: { server: string } }) {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center transition-all duration-300 ease-in-out",
|
"flex items-center transition-all duration-300 ease-in-out",
|
||||||
staticMode ? "ml-0" : "group-hover:ml-[42px]"
|
staticMode ? "ml-[42px]" : "group-hover:ml-[42px]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{serverObject && (
|
{serverObject && (
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export function Footer() {
|
|||||||
|
|
||||||
if (!hideFooterPages.includes(pathname ?? ""))
|
if (!hideFooterPages.includes(pathname ?? ""))
|
||||||
return (
|
return (
|
||||||
<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">
|
<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">
|
||||||
<div className="flex justify-between items-start p-[20px]">
|
<div className="flex justify-between items-start p-[20px]">
|
||||||
<span className="flex items-center gap-4">
|
<span className="flex items-center gap-4">
|
||||||
<Link href="Special:Root">
|
<Link href="Special:Root">
|
||||||
@ -89,7 +89,7 @@ export function Footer() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="block px-4 lg:-translate-y-12">
|
<span className="block px-4">
|
||||||
<small className="text-[0.75rem]">
|
<small className="text-[0.75rem]">
|
||||||
MHSF is an open-source project licensed under the MIT license. MHSF is
|
MHSF is an open-source project licensed under the MIT license. MHSF is
|
||||||
not officially affiliated with with Minehut, Super League Enterprise,
|
not officially affiliated with with Minehut, Super League Enterprise,
|
||||||
|
|||||||
@ -103,14 +103,7 @@ export function ServerList() {
|
|||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<Tooltip>
|
<ModificationButton disabled={testModeEnabled} />
|
||||||
<TooltipTrigger>
|
|
||||||
<ModificationButton disabled={testModeEnabled} />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">
|
|
||||||
{filterCount} modification(s) enabled
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<ServerTestModeSelector
|
<ServerTestModeSelector
|
||||||
testModeStatus={testModeStatus}
|
testModeStatus={testModeStatus}
|
||||||
testModeEnabled={testModeEnabled}
|
testModeEnabled={testModeEnabled}
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
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,
|
code: CodeHighlight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{mhsfData.server?.customizationData.description}
|
{mhsfData.server?.customizationData.description?.replaceAll("<br />", "\n")}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
115
apps/www/src/components/feat/server-page/server-editor/customizations/server-discord-box.tsx
Normal file
115
apps/www/src/components/feat/server-page/server-editor/customizations/server-discord-box.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
apps/www/src/components/feat/server-page/server-editor/customizations/server-unown-box.tsx
Normal file
126
apps/www/src/components/feat/server-page/server-editor/customizations/server-unown-box.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* 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,6 +29,9 @@ import { ServerDescriptionBox } from "./customizations/server-description-box";
|
|||||||
import { ServerBannerBox } from "./customizations/server-banner-box";
|
import { ServerBannerBox } from "./customizations/server-banner-box";
|
||||||
import { ServerMigrationBox } from "./customizations/server-migration-box";
|
import { ServerMigrationBox } from "./customizations/server-migration-box";
|
||||||
import { ServerColorModeBox } from "./customizations/server-color-mode-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 =
|
const successClasses =
|
||||||
"bg-green-200 border-green-400 dark:bg-green-800 dark:border-green-600";
|
"bg-green-200 border-green-400 dark:bg-green-800 dark:border-green-600";
|
||||||
@ -39,15 +42,16 @@ export function ServerEditorProvider({
|
|||||||
children,
|
children,
|
||||||
serverData,
|
serverData,
|
||||||
minehutData,
|
minehutData,
|
||||||
|
mhsfUser
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode | ReactNode[];
|
children: ReactNode | ReactNode[];
|
||||||
serverData: ReturnType<typeof useMHSFServer>;
|
serverData: ReturnType<typeof useMHSFServer>;
|
||||||
minehutData: ServerResponse;
|
minehutData: ServerResponse;
|
||||||
|
mhsfUser: {user: MHSFUser | null};
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [onlineData, setOnlineData] = useState<OnlineServer>();
|
const [onlineData, setOnlineData] = useState<OnlineServer>();
|
||||||
const { servers, loading } = useServers();
|
const { servers, loading } = useServers();
|
||||||
const [claimedUser, setClaimedUser] = useState<string>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("open-server-editor", () => {
|
window.addEventListener("open-server-editor", () => {
|
||||||
@ -64,28 +68,19 @@ export function ServerEditorProvider({
|
|||||||
}
|
}
|
||||||
}, [open, loading, servers, minehutData.name]);
|
}, [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 requirementOne = minehutData.online;
|
||||||
const requirementTwo = onlineData !== null;
|
const requirementTwo = onlineData !== null;
|
||||||
const requirementThree = claimedUser === onlineData?.author;
|
const requirementThree = mhsfUser !== null && mhsfUser.user?.claimedUser !== null && mhsfUser.user?.claimedUser?.name === onlineData?.author;
|
||||||
const requirementFour = claimedUser !== null;
|
const requirementFour = mhsfUser !== null && mhsfUser.user?.claimedUser !== null;
|
||||||
const UploadDropzone = generateUploadDropzone<BannerUploaderRouter>({
|
|
||||||
url: `/api/v1/server/get/${minehutData._id}/settings/upload-banner`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
<MilkdownProvider>
|
<MilkdownProvider>
|
||||||
<Drawer open={open} onOpenChange={setOpen}>
|
<Drawer open={open} onOpenChange={(c) => {
|
||||||
|
serverData.refresh();
|
||||||
|
setOpen(c);
|
||||||
|
}}>
|
||||||
<DrawerContent className="p-4 !max-h-[700px] !h-[700px]">
|
<DrawerContent className="p-4 !max-h-[700px] !h-[700px]">
|
||||||
<br />
|
<br />
|
||||||
{!serverData.server?.customizationData.isOwned ? (
|
{!serverData.server?.customizationData.isOwned ? (
|
||||||
@ -207,14 +202,20 @@ export function ServerEditorProvider({
|
|||||||
</div>
|
</div>
|
||||||
<DrawerFooter>
|
<DrawerFooter>
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={async () => {
|
||||||
toast.promise(serverData.ownServer(), {
|
toast.promise(
|
||||||
success: "Successfully owned server",
|
async () => {
|
||||||
error:
|
await serverData.ownServer();
|
||||||
"There was an error while linking this server. Please contact support.",
|
await serverData.refresh();
|
||||||
loading: "Linking server...",
|
},
|
||||||
})
|
{
|
||||||
}
|
success: "Successfully owned server",
|
||||||
|
error:
|
||||||
|
"There was an error while linking this server. Please contact support.",
|
||||||
|
loading: "Linking server...",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
!(
|
!(
|
||||||
requirementOne &&
|
requirementOne &&
|
||||||
@ -250,6 +251,12 @@ export function ServerEditorProvider({
|
|||||||
serverData={serverData}
|
serverData={serverData}
|
||||||
minehutData={minehutData}
|
minehutData={minehutData}
|
||||||
/>
|
/>
|
||||||
|
<ServerDiscordBox mhsfServer={minehutData._id} defaultDiscord={serverData.server.customizationData.discord ?? ""} />
|
||||||
|
<ServerUnownBox
|
||||||
|
mhsfData={serverData}
|
||||||
|
serverData={minehutData}
|
||||||
|
reset={() => {setOpen(false); serverData.refresh();}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ServerMigrationBox
|
<ServerMigrationBox
|
||||||
|
|||||||
@ -12,11 +12,13 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { DebugProvider } from "./debug/debug-provider";
|
import { DebugProvider } from "./debug/debug-provider";
|
||||||
import { ReportingProvider } from "./reporting/reporting-provider";
|
import { ReportingProvider } from "./reporting/reporting-provider";
|
||||||
import { ServerEditorProvider } from "./server-editor/server-editor-provider";
|
import { ServerEditorProvider } from "./server-editor/server-editor-provider";
|
||||||
|
import { useUser } from "@/lib/hooks/use-user";
|
||||||
|
|
||||||
export function ServerProvider({ serverId }: { serverId: string }) {
|
export function ServerProvider({ serverId }: { serverId: string }) {
|
||||||
const { server, error, loading, onlineServer } = useServer({ id: serverId });
|
const { server, error, loading, onlineServer } = useServer({ id: serverId });
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
const mhsf = useMHSFServer(serverId);
|
const mhsf = useMHSFServer(serverId);
|
||||||
|
const mhsfUser = useUser();
|
||||||
|
|
||||||
if (error !== null)
|
if (error !== null)
|
||||||
return (
|
return (
|
||||||
@ -72,7 +74,7 @@ export function ServerProvider({ serverId }: { serverId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="px-10">
|
<div className="px-10">
|
||||||
<ServerEditorProvider serverData={mhsf} minehutData={server as ServerResponse}>
|
<ServerEditorProvider serverData={mhsf} minehutData={server as ServerResponse} mhsfUser={mhsfUser}>
|
||||||
<ReportingProvider server={mhsf}>
|
<ReportingProvider server={mhsf}>
|
||||||
<ServerMainPage
|
<ServerMainPage
|
||||||
server={server as ServerResponse}
|
server={server as ServerResponse}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import { IconsRow } from "./icons/icons-row";
|
|||||||
import { affiliates } from "./util";
|
import { affiliates } from "./util";
|
||||||
import { AffiliateRow } from "./afilliate/affilliate-row";
|
import { AffiliateRow } from "./afilliate/affilliate-row";
|
||||||
import { EmbedCreatorRow } from "./embeds/embed-creator";
|
import { EmbedCreatorRow } from "./embeds/embed-creator";
|
||||||
|
import { ServerDiscordRow } from "./discord/server-discord-row";
|
||||||
|
|
||||||
export function ServerRows({ server, mhsfData }: { server: ServerResponse, mhsfData: ReturnType<typeof useMHSFServer> }) {
|
export function ServerRows({ server, mhsfData }: { server: ServerResponse, mhsfData: ReturnType<typeof useMHSFServer> }) {
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
@ -46,8 +47,9 @@ export function ServerRows({ server, mhsfData }: { server: ServerResponse, mhsfD
|
|||||||
return (
|
return (
|
||||||
<span className="lg:grid lg:grid-cols-2 w-full gap-3">
|
<span className="lg:grid lg:grid-cols-2 w-full gap-3">
|
||||||
{affiliates.includes(server.name) && <AffiliateRow />}
|
{affiliates.includes(server.name) && <AffiliateRow />}
|
||||||
<MOTDRow server={server} mhsfData={mhsfData}/>
|
<MOTDRow server={server} mhsfData={mhsfData} />
|
||||||
<StatisticsMainRow server={server} mhsfData={mhsfData} />
|
<StatisticsMainRow server={server} mhsfData={mhsfData} />
|
||||||
|
{mhsfData.server?.customizationData.discord !== undefined && <ServerDiscordRow server={server} mhsfData={mhsfData} />}
|
||||||
<GeneralInfo server={server} mhsfData={mhsfData} />
|
<GeneralInfo server={server} mhsfData={mhsfData} />
|
||||||
<AchievementsView server={server} mhsfData={mhsfData} />
|
<AchievementsView server={server} mhsfData={mhsfData} />
|
||||||
<IconsRow server={server} mhsfData={mhsfData} />
|
<IconsRow server={server} mhsfData={mhsfData} />
|
||||||
|
|||||||
235
apps/www/src/components/feat/settings/account-settings.tsx
Normal file
235
apps/www/src/components/feat/settings/account-settings.tsx
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
/*
|
||||||
|
* 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,113 +30,139 @@
|
|||||||
|
|
||||||
import { Material } from "@/components/ui/material";
|
import { Material } from "@/components/ui/material";
|
||||||
import {
|
import {
|
||||||
Setting,
|
Setting,
|
||||||
SettingContent,
|
SettingContent,
|
||||||
SettingDescription,
|
SettingDescription,
|
||||||
SettingMeta,
|
SettingMeta,
|
||||||
SettingTitle,
|
SettingTitle,
|
||||||
} from "./setting";
|
} from "./setting";
|
||||||
import { ModeToggle } from "@/components/util/mode-toggle";
|
import { ModeToggle } from "@/components/util/mode-toggle";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||||
import { Switch } from "@/components/ui/switch";
|
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() {
|
export function BrowserSettings() {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const [fontFamily, setFontFamily] = useState("inter");
|
const [fontFamily, setFontFamily] = useState("inter");
|
||||||
const [mcFont, setMcFont] = useState(true);
|
const [mcFont, setMcFont] = useState(true);
|
||||||
const [debugMode, setDebugMode] = useState(false);
|
const [debugMode, setDebugMode] = useState(false);
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
||||||
setMcFont((settingsStore.get("mc-font") === "true") as boolean);
|
setMcFont((settingsStore.get("mc-font") === "true") as boolean);
|
||||||
setDebugMode((settingsStore.get("debug-mode") === "true") as boolean);
|
setDebugMode((settingsStore.get("debug-mode") === "true") as boolean);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Material className="mt-6 grid gap-4">
|
<Material className="mt-6 grid gap-4">
|
||||||
<h2 className="text-xl font-semibold text-inherit">Appearance</h2>
|
<h2 className="text-xl font-semibold text-inherit">Support</h2>
|
||||||
<Setting>
|
<Setting>
|
||||||
<SettingContent>
|
<SettingContent>
|
||||||
<SettingMeta>
|
<SettingMeta>
|
||||||
<SettingTitle>Color Scheme</SettingTitle>
|
<SettingTitle>Donate</SettingTitle>
|
||||||
<SettingDescription>
|
<SettingDescription>
|
||||||
Change the MHSF color scheme
|
Please consider supporting me if you think this project is useful
|
||||||
</SettingDescription>
|
to you, this project is completely open-source and I do not get
|
||||||
</SettingMeta>
|
any money from it.
|
||||||
<ModeToggle />
|
</SettingDescription>
|
||||||
</SettingContent>
|
</SettingMeta>
|
||||||
</Setting>
|
<a
|
||||||
<Setting>
|
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"
|
||||||
<SettingContent>
|
href="https://buymeacoffee.com/dvelo"
|
||||||
<SettingMeta>
|
>
|
||||||
<SettingTitle>Use Minecraft font</SettingTitle>
|
<Heart fill={resolvedTheme === "dark" ? "black" : "white"} size={16} /> Donate
|
||||||
<SettingDescription>
|
</a>
|
||||||
Use Minecraft font for MOTD. Turning this off restores font
|
</SettingContent>
|
||||||
settings for MOTD's to a v1-like state.
|
</Setting>
|
||||||
</SettingDescription>
|
<Separator />
|
||||||
</SettingMeta>
|
<h2 className="text-xl font-semibold text-inherit">Appearance</h2>
|
||||||
<Switch
|
<Setting>
|
||||||
checked={mcFont}
|
<SettingContent>
|
||||||
onCheckedChange={(c) => {
|
<SettingMeta>
|
||||||
settingsStore.set("mc-font", c, false);
|
<SettingTitle>Color Scheme</SettingTitle>
|
||||||
setMcFont(c);
|
<SettingDescription>
|
||||||
}}
|
Change the MHSF color scheme
|
||||||
/>
|
</SettingDescription>
|
||||||
</SettingContent>
|
</SettingMeta>
|
||||||
</Setting>
|
<ModeToggle />
|
||||||
<Setting>
|
</SettingContent>
|
||||||
<SettingContent>
|
</Setting>
|
||||||
<SettingMeta>
|
<Setting>
|
||||||
<SettingTitle>Font</SettingTitle>
|
<SettingContent>
|
||||||
<SettingDescription>
|
<SettingMeta>
|
||||||
Change the default font used in the interface.
|
<SettingTitle>Use Minecraft font</SettingTitle>
|
||||||
</SettingDescription>
|
<SettingDescription>
|
||||||
</SettingMeta>
|
Use Minecraft font for MOTD. Turning this off restores font
|
||||||
<Select
|
settings for MOTD's to a v1-like state.
|
||||||
defaultValue="inter"
|
</SettingDescription>
|
||||||
value={fontFamily}
|
</SettingMeta>
|
||||||
onValueChange={(c) => {
|
<Switch
|
||||||
settingsStore.set("font-family", c, false);
|
checked={mcFont}
|
||||||
window.dispatchEvent(new Event("font-family-change"));
|
onCheckedChange={(c) => {
|
||||||
setFontFamily(c);
|
settingsStore.set("mc-font", c, false);
|
||||||
}}
|
setMcFont(c);
|
||||||
>
|
}}
|
||||||
<SelectTrigger className="max-w-[180px]">
|
/>
|
||||||
<SelectValue />
|
</SettingContent>
|
||||||
</SelectTrigger>
|
</Setting>
|
||||||
<SelectContent>
|
<Setting>
|
||||||
<SelectItem value="inter">Inter</SelectItem>
|
<SettingContent>
|
||||||
<SelectItem value="geist-sans">Geist Sans</SelectItem>
|
<SettingMeta>
|
||||||
<SelectItem value="system-ui">System UI</SelectItem>
|
<SettingTitle>Font</SettingTitle>
|
||||||
<SelectItem value="roboto">Roboto</SelectItem>
|
<SettingDescription>
|
||||||
</SelectContent>
|
Change the default font used in the interface.
|
||||||
</Select>
|
</SettingDescription>
|
||||||
</SettingContent>
|
</SettingMeta>
|
||||||
</Setting>
|
<Select
|
||||||
<Setting>
|
defaultValue="inter"
|
||||||
<SettingContent>
|
value={fontFamily}
|
||||||
<SettingMeta>
|
onValueChange={(c) => {
|
||||||
<SettingTitle>Debug Mode</SettingTitle>
|
settingsStore.set("font-family", c, false);
|
||||||
<SettingDescription>Enable debug mode to show debug options</SettingDescription>
|
window.dispatchEvent(new Event("font-family-change"));
|
||||||
</SettingMeta>
|
setFontFamily(c);
|
||||||
<Switch
|
}}
|
||||||
checked={debugMode}
|
>
|
||||||
onCheckedChange={(c) => {
|
<SelectTrigger className="max-w-[180px]">
|
||||||
settingsStore.set("debug-mode", c, false);
|
<SelectValue />
|
||||||
window.dispatchEvent(new Event("debug-mode-change"));
|
</SelectTrigger>
|
||||||
setDebugMode(c);
|
<SelectContent>
|
||||||
}}
|
<SelectItem value="inter">Inter</SelectItem>
|
||||||
/>
|
<SelectItem value="geist-sans">Geist Sans</SelectItem>
|
||||||
</SettingContent>
|
<SelectItem value="system-ui">System UI</SelectItem>
|
||||||
</Setting>
|
<SelectItem value="roboto">Roboto</SelectItem>
|
||||||
</Material>
|
</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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
248
apps/www/src/components/feat/settings/linking-dialog.tsx
Normal file
248
apps/www/src/components/feat/settings/linking-dialog.tsx
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
* 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,6 +40,7 @@ import { BrowserSettings } from "./browser-settings";
|
|||||||
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { DebugSettings } from "./debug-settings";
|
import { DebugSettings } from "./debug-settings";
|
||||||
|
import { AccountSettings } from "./account-settings";
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
@ -101,6 +102,7 @@ export function Settings() {
|
|||||||
<DebugSettings />
|
<DebugSettings />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="user-settings">
|
<TabsContent value="user-settings">
|
||||||
|
<SignedIn><AccountSettings /></SignedIn>
|
||||||
<SignedOut>
|
<SignedOut>
|
||||||
<Material className="mt-6 grid gap-4 py-6">
|
<Material className="mt-6 grid gap-4 py-6">
|
||||||
<h3
|
<h3
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export function WaitlistPage() {
|
|||||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
||||||
v2 private beta
|
v2 private beta
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mb-3">
|
<p className="mb-3 text-sm">
|
||||||
Hello there! MHSF has an exclusive beta that you may have been invited{" "}
|
Hello there! MHSF has an exclusive beta that you may have been invited{" "}
|
||||||
<br /> to. Please sign into your account below or follow the
|
<br /> to. Please sign into your account below or follow the
|
||||||
instructions.
|
instructions.
|
||||||
@ -67,8 +67,8 @@ export function WaitlistPage() {
|
|||||||
<SignedOut>
|
<SignedOut>
|
||||||
<p>
|
<p>
|
||||||
You must be signed in to check for eligibility for this beta. Please
|
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
|
make sure you use the Discord connection so we can check if you are
|
||||||
eligibile for the beta.
|
eligible for the beta.
|
||||||
</p>
|
</p>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Button onClick={() => clerk.openSignIn()} variant="secondary">
|
<Button onClick={() => clerk.openSignIn()} variant="secondary">
|
||||||
@ -131,7 +131,7 @@ export function UserInformation({ discordPage }: { discordPage?: boolean }) {
|
|||||||
|
|
||||||
{discordData !== undefined && discordData !== null && (
|
{discordData !== undefined && discordData !== null && (
|
||||||
<p className="group cursor-pointer flex items-center gap-1">
|
<p className="group cursor-pointer flex items-center gap-1">
|
||||||
Discord linked as {discordData.global_name}
|
Discord linked as {discordData.global_name ?? discordData.username}
|
||||||
<span className="text-muted-foreground hidden group-hover:block">
|
<span className="text-muted-foreground hidden group-hover:block">
|
||||||
@{discordData.username}
|
@{discordData.username}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -100,7 +100,7 @@ const DialogContent = React.forwardRef<
|
|||||||
"dark:border-zinc-900 dark:border-t-zinc-800 dark:border-b-zinc-900",
|
"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",
|
"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",
|
"p-5 flex flex-col gap-2 dark:bg-zinc-950 rounded-xl",
|
||||||
"bg-white fixed z-9",
|
"bg-white fixed z-100",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
77
apps/www/src/components/ui/input-otp.tsx
Normal file
77
apps/www/src/components/ui/input-otp.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"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,6 +77,8 @@ export function FontBoundary({
|
|||||||
})() as string,
|
})() as string,
|
||||||
"overflow-x-hidden",
|
"overflow-x-hidden",
|
||||||
className,
|
className,
|
||||||
|
"bg-background",
|
||||||
|
"font-sans"
|
||||||
] as string[];
|
] as string[];
|
||||||
|
|
||||||
document.body.classList.add(...classes);
|
document.body.classList.add(...classes);
|
||||||
|
|||||||
@ -29,4 +29,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
export const version = "2.0";
|
export const version = "pb1-2.0";
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
*/
|
*/
|
||||||
//
|
//
|
||||||
|
|
||||||
import { Achievement } from "./types/achievement";
|
import type { Achievement } from "./types/achievement";
|
||||||
|
|
||||||
const connector = (
|
const connector = (
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
@ -168,7 +168,7 @@ export async function isFavorited(server: string): Promise<boolean> {
|
|||||||
export async function getAccountFavorites(): Promise<Array<string>> {
|
export async function getAccountFavorites(): Promise<Array<string>> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
connector(`/user/favorites`, { version: 1 }),
|
connector(`/user/get`, { version: 1 }),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -177,7 +177,7 @@ export async function getAccountFavorites(): Promise<Array<string>> {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (await response.json()).result;
|
return (await response.json()).favorites.favorites;
|
||||||
} catch {
|
} catch {
|
||||||
throw Error("Not authenticated with a user.");
|
throw Error("Not authenticated with a user.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -110,7 +110,7 @@ export async function checkOwnedServerMetadata(
|
|||||||
{ server: serverSelector.name ?? serverData.name },
|
{ server: serverSelector.name ?? serverData.name },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ $set: changes },
|
{ $set: { serverId: serverSelector.id, customizationVersion: 2, ...changes } },
|
||||||
{ upsert: true },
|
{ 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 { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embedded/sl-modification-frame/file/[filename]/page";
|
||||||
import { useUser } from "@clerk/nextjs";
|
import { useUser } from "@clerk/nextjs";
|
||||||
import type { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
import type { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||||
import { ClerkEmbeddedFilter } from "@/components/feat/server-list/modification/modification-action";
|
import type { ClerkEmbeddedFilter } from "@/components/feat/server-list/modification/modification-action";
|
||||||
import { supportedFilters } from "../types/supportedFilters";
|
import { supportedFilters } from "../types/supportedFilters";
|
||||||
|
|
||||||
type EmbeddedFilter = {
|
export type EmbeddedFilter = {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
functionFilter: (server: OnlineServer) => (boolean | Promise<boolean>);
|
functionFilter: (server: OnlineServer) => (boolean | Promise<boolean>);
|
||||||
};
|
};
|
||||||
@ -77,13 +77,13 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
if (filteredData.length === 0 || data.length === 0) {
|
if (filteredData.length === 0 || data.length === 0) {
|
||||||
window.dispatchEvent(new Event("update-modification-stack"));
|
window.dispatchEvent(new Event("update-modification-stack"));
|
||||||
} else setLoading(false);
|
} else setLoading(false);
|
||||||
}, [data, filteredData, loading]);
|
}, [data, filteredData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
window.dispatchEvent(new Event("update-modification-stack"));
|
window.dispatchEvent(new Event("update-modification-stack"));
|
||||||
} else setLoading(false);
|
} else setLoading(false);
|
||||||
}, [data, filteredData, loading]);
|
}, [data, filteredData]);
|
||||||
|
|
||||||
const testModeInit = (type: "filter" | "sort") => {
|
const testModeInit = (type: "filter" | "sort") => {
|
||||||
window.dispatchEvent(new Event("test-mode.enabled"));
|
window.dispatchEvent(new Event("test-mode.enabled"));
|
||||||
|
|||||||
@ -28,27 +28,28 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { useEffect, useState } from "react";
|
||||||
import { MongoClient } from "mongodb";
|
|
||||||
import { waitUntil } from "@vercel/functions";
|
|
||||||
|
|
||||||
export default async function handler(
|
export function useMinecraftHead(username: string) {
|
||||||
req: NextApiRequest,
|
const [loading, setLoading] = useState(true);
|
||||||
res: NextApiResponse,
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||||
) {
|
const [uuid, setUUID] = useState<string | null>(null);
|
||||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
|
||||||
await client.connect();
|
|
||||||
|
|
||||||
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
useEffect(() => {
|
||||||
const collection = db.collection("meta");
|
if (username !== "")
|
||||||
|
fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`)
|
||||||
|
.then((c) => c.json())
|
||||||
|
.then((d) => {
|
||||||
|
setUUID(d.id);
|
||||||
|
});
|
||||||
|
}, [username]);
|
||||||
|
|
||||||
const all = await collection.find().toArray();
|
useEffect(() => {
|
||||||
const sorted = all.sort((a, b) => a.favorites - b.favorites);
|
if (uuid !== null) {
|
||||||
sorted.reverse();
|
setImageUrl(`https://api.mineatar.io/face/${uuid}`);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [uuid])
|
||||||
|
|
||||||
// Close the database, but don't close this
|
return { loading, imageUrl, uuid };
|
||||||
// serverless instance until it happens
|
}
|
||||||
waitUntil(client.close());
|
|
||||||
|
|
||||||
res.send({ results: sorted });
|
|
||||||
}
|
|
||||||
@ -28,34 +28,51 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiResponse, NextApiRequest } from "next";
|
import type { WithId } from "mongodb";
|
||||||
import { MongoClient } from "mongodb";
|
import { useEffect, useState } from "react";
|
||||||
import { getAuth } from "@clerk/nextjs/server";
|
import { NullLiteral } from "typescript";
|
||||||
import { waitUntil } from "@vercel/functions";
|
|
||||||
|
|
||||||
export default async function handler(
|
export type MHSFUser = {
|
||||||
req: NextApiRequest,
|
favorites: WithId<{
|
||||||
res: NextApiResponse
|
/** @note Not important */
|
||||||
) {
|
user: string;
|
||||||
const { userId } = getAuth(req);
|
/** TODO: should be as a Id */
|
||||||
|
favorites: string[];
|
||||||
|
}> | null;
|
||||||
|
ownedServers: {
|
||||||
|
serverId: string;
|
||||||
|
/** @deprecated use `serverId` instead */
|
||||||
|
server: string;
|
||||||
|
|
||||||
if (!userId) {
|
author: string;
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
}[];
|
||||||
}
|
claimedUser: { uuid: string; name: string } | null;
|
||||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
actions: {
|
||||||
await client.connect();
|
linkAccount: string;
|
||||||
|
unlinkAccount: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
export function useUser(): {
|
||||||
const collection = db.collection("favorites");
|
user: MHSFUser | null;
|
||||||
const find = await collection.find({ user: userId }).toArray();
|
refresh: () => Promise<void>;
|
||||||
|
} {
|
||||||
|
const [user, setUser] = useState<MHSFUser | null>(null);
|
||||||
|
|
||||||
// Close the database, but don't close this
|
useEffect(() => {
|
||||||
// serverless instance until it happens
|
(async () => {
|
||||||
waitUntil(client.close());
|
const user = await fetch("/api/v1/user/get");
|
||||||
|
const json = await user.json();
|
||||||
|
setUser(json);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (find.length == 0) {
|
return {
|
||||||
res.send({ favorites: [] });
|
user,
|
||||||
} else {
|
refresh: async () => {
|
||||||
res.send({ favorites: find[0].favorites });
|
const user = await fetch("/api/v1/user/get");
|
||||||
}
|
const json = await user.json();
|
||||||
|
setUser(json);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@ -29,9 +29,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getBackendProcedure } from "@/lib/backend-procedure";
|
import { getBackendProcedure } from "@/lib/backend-procedure";
|
||||||
import type { MHSFData } from "@/lib/types/data";
|
import type { Achievement } from "@/lib/types/achievement";
|
||||||
import { clerkClient, getAuth, User } from "@clerk/nextjs/server";
|
import type { ActualCustomization, MHSFData } from "@/lib/types/data";
|
||||||
import { MongoClient } from "mongodb";
|
import { clerkClient, getAuth, type User } from "@clerk/nextjs/server";
|
||||||
|
import { type Db, type Filter, type Document, MongoClient, type WithId } from "mongodb";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
export type RouteParams = {
|
export type RouteParams = {
|
||||||
@ -114,6 +115,7 @@ export default async function handler(
|
|||||||
res.send({
|
res.send({
|
||||||
server: {
|
server: {
|
||||||
favoriteData,
|
favoriteData,
|
||||||
|
// @ts-ignore Also don't care what you think.
|
||||||
customizationData,
|
customizationData,
|
||||||
playerData,
|
playerData,
|
||||||
achievements,
|
achievements,
|
||||||
@ -143,15 +145,18 @@ async function findCustomizationData(
|
|||||||
serverName: string,
|
serverName: string,
|
||||||
serverId: string,
|
serverId: string,
|
||||||
userId: string | undefined,
|
userId: string | undefined,
|
||||||
db: any,
|
db: Db,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
description: string | undefined;
|
description: string | undefined;
|
||||||
banner: string | undefined;
|
|
||||||
discord: string | undefined;
|
discord: string | undefined;
|
||||||
colorScheme: string | undefined;
|
colorScheme: string | undefined;
|
||||||
|
colorMode: "dark" | "light" | null;
|
||||||
|
customizationVersion: number | undefined;
|
||||||
userProfilePicture: string | undefined;
|
userProfilePicture: string | undefined;
|
||||||
isOwned: boolean;
|
isOwned: boolean;
|
||||||
isOwnedByUser: boolean;
|
isOwnedByUser: boolean;
|
||||||
|
banner: string | undefined;
|
||||||
|
_deletionId: string | undefined;
|
||||||
}> {
|
}> {
|
||||||
const clerk = await clerkClient();
|
const clerk = await clerkClient();
|
||||||
// Run queries in parallel
|
// Run queries in parallel
|
||||||
@ -167,18 +172,33 @@ async function findCustomizationData(
|
|||||||
]);
|
]);
|
||||||
let user: User | undefined = undefined;
|
let user: User | undefined = undefined;
|
||||||
if (ownedServerData) {
|
if (ownedServerData) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
user = await clerk.users.getUser(ownedServerData?.author);
|
user = await clerk.users.getUser(ownedServerData?.author);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
if (customizationData || ownedServerData) {
|
if (customizationData || ownedServerData) {
|
||||||
return {
|
const baseData: {
|
||||||
...(customizationData as any),
|
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) ?? {},
|
||||||
isOwned: true,
|
isOwned: true,
|
||||||
isOwnedByUser: ownedServerData?.author === userId,
|
isOwnedByUser: ownedServerData?.author === userId,
|
||||||
userProfilePicture: null,
|
userProfilePicture: undefined,
|
||||||
|
colorMode: null,
|
||||||
|
customizationVersion: undefined,
|
||||||
|
_deletionId: undefined,
|
||||||
};
|
};
|
||||||
|
// @ts-ignore L
|
||||||
|
return baseData
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isOwned: false,
|
isOwned: false,
|
||||||
@ -187,17 +207,51 @@ async function findCustomizationData(
|
|||||||
banner: undefined,
|
banner: undefined,
|
||||||
discord: undefined,
|
discord: undefined,
|
||||||
colorScheme: undefined,
|
colorScheme: undefined,
|
||||||
|
colorMode: null,
|
||||||
|
customizationVersion: undefined,
|
||||||
userProfilePicture: undefined,
|
userProfilePicture: undefined,
|
||||||
|
_deletionId: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customizationData || ownedServerData) {
|
if (customizationData || ownedServerData) {
|
||||||
return {
|
const baseData: {
|
||||||
...(customizationData as any),
|
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) ?? {},
|
||||||
isOwned: true,
|
isOwned: true,
|
||||||
isOwnedByUser: ownedServerData?.author === userId,
|
isOwnedByUser: ownedServerData?.author === userId,
|
||||||
userProfilePicture: userId ? user?.imageUrl : "no user",
|
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,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,14 +262,17 @@ async function findCustomizationData(
|
|||||||
banner: undefined,
|
banner: undefined,
|
||||||
discord: undefined,
|
discord: undefined,
|
||||||
colorScheme: undefined,
|
colorScheme: undefined,
|
||||||
|
colorMode: null,
|
||||||
|
customizationVersion: undefined,
|
||||||
userProfilePicture: undefined,
|
userProfilePicture: undefined,
|
||||||
|
_deletionId: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findFavoriteData(
|
async function findFavoriteData(
|
||||||
serverName: string,
|
serverName: string,
|
||||||
userId: string | undefined,
|
userId: string | undefined,
|
||||||
db: any,
|
db: Db,
|
||||||
query: {
|
query: {
|
||||||
maxFavoriteEntries?: string | string[];
|
maxFavoriteEntries?: string | string[];
|
||||||
favoriteTimespanStart?: string | string[];
|
favoriteTimespanStart?: string | string[];
|
||||||
@ -246,16 +303,16 @@ async function findFavoriteData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchHistoryData(
|
async function fetchHistoryData(
|
||||||
db: any,
|
db: Db,
|
||||||
serverName: string,
|
serverName: string,
|
||||||
query: {
|
query: {
|
||||||
maxFavoriteEntries?: string | string[];
|
maxFavoriteEntries?: string | string[];
|
||||||
favoriteTimespanStart?: string | string[];
|
favoriteTimespanStart?: string | string[];
|
||||||
favoriteTimespanEnd?: string | string[];
|
favoriteTimespanEnd?: string | string[];
|
||||||
},
|
},
|
||||||
) {
|
): Promise<{ date: string; favorites: number }[]> {
|
||||||
// Build query filter
|
// Build query filter
|
||||||
const filter: any = { server: serverName };
|
const filter: { server: string; date?: { $gte: Date; $lte: Date } } = { server: serverName };
|
||||||
|
|
||||||
// Add date range filter if provided
|
// Add date range filter if provided
|
||||||
if (query.favoriteTimespanStart && query.favoriteTimespanEnd) {
|
if (query.favoriteTimespanStart && query.favoriteTimespanEnd) {
|
||||||
@ -279,14 +336,18 @@ async function fetchHistoryData(
|
|||||||
cursor.limit(limit);
|
cursor.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await cursor.toArray();
|
const results = await cursor.toArray();
|
||||||
|
return results.map(doc => ({
|
||||||
|
date: doc.date.toISOString(),
|
||||||
|
favorites: doc.favorites || 0
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findServerData(
|
export 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) {
|
||||||
@ -305,7 +366,7 @@ export async function findServerData(
|
|||||||
|
|
||||||
async function findPlayerData(
|
async function findPlayerData(
|
||||||
serverName: string,
|
serverName: string,
|
||||||
db: any,
|
db: Db,
|
||||||
query: {
|
query: {
|
||||||
maxPlayerEntries?: string | string[];
|
maxPlayerEntries?: string | string[];
|
||||||
playerTimespanStart?: string | string[];
|
playerTimespanStart?: string | string[];
|
||||||
@ -316,7 +377,7 @@ async function findPlayerData(
|
|||||||
const historyCollection = db.collection("history");
|
const historyCollection = db.collection("history");
|
||||||
|
|
||||||
// Build query filter
|
// Build query filter
|
||||||
const filter: any = { server: serverName };
|
const filter: Filter<Document> = { server: serverName };
|
||||||
|
|
||||||
// Add date range filter if provided
|
// Add date range filter if provided
|
||||||
if (query.playerTimespanStart && query.playerTimespanEnd) {
|
if (query.playerTimespanStart && query.playerTimespanEnd) {
|
||||||
@ -348,10 +409,11 @@ async function findPlayerData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Format the data to match the expected structure
|
// Format the data to match the expected structure
|
||||||
|
type HistoryDocument = { date: Date; player_count?: number };
|
||||||
const formattedHistory = historically.map(
|
const formattedHistory = historically.map(
|
||||||
(item: { date: string; player_count?: number }) => ({
|
(item) => ({
|
||||||
date: item.date,
|
date: (item as HistoryDocument).date.toISOString(),
|
||||||
playerCount: item.player_count || 0,
|
playerCount: (item as HistoryDocument).player_count || 0,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -362,7 +424,7 @@ async function findPlayerData(
|
|||||||
|
|
||||||
async function findAchievements(
|
async function findAchievements(
|
||||||
serverName: string,
|
serverName: string,
|
||||||
db: any,
|
db: Db,
|
||||||
query: {
|
query: {
|
||||||
maxAchievementEntries?: string | string[];
|
maxAchievementEntries?: string | string[];
|
||||||
achievementTimespanStart?: string | string[];
|
achievementTimespanStart?: string | string[];
|
||||||
@ -373,12 +435,10 @@ async function findAchievements(
|
|||||||
const achievementsCollection = db.collection("achievements");
|
const achievementsCollection = db.collection("achievements");
|
||||||
|
|
||||||
// Build query filter
|
// Build query filter
|
||||||
const filter: any = { name: serverName };
|
const filter: Filter<Document> = { 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
|
|
||||||
// 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)),
|
||||||
@ -393,11 +453,17 @@ async function findAchievements(
|
|||||||
historically = historically.slice(0, Number(query.maxAchievementEntries));
|
historically = historically.slice(0, Number(query.maxAchievementEntries));
|
||||||
}
|
}
|
||||||
|
|
||||||
const currently: any[] = [];
|
// Transform the data to match the expected shape
|
||||||
for (const a of historically)
|
const transformedHistorically = historically.map(doc => ({
|
||||||
a.achievements.forEach((item: any, interval: number) =>
|
_id: doc._id.toString(),
|
||||||
currently.push({ interval, ...item }),
|
name: doc.name,
|
||||||
);
|
achievements: doc.achievements || []
|
||||||
|
}));
|
||||||
|
|
||||||
return { historically, currently };
|
const currently: Achievement[] = [];
|
||||||
|
for (const a of historically)
|
||||||
|
for (const item of a.achievements)
|
||||||
|
currently.push(item);
|
||||||
|
|
||||||
|
return { historically: transformedHistorically, currently };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,7 +104,7 @@ export default async function handler(
|
|||||||
await collection.insertOne({ serverId: server, author: userId });
|
await collection.insertOne({ serverId: server, author: userId });
|
||||||
|
|
||||||
// Close the database, but don't close this
|
// Close the database, but don't close this
|
||||||
// serverless instance until it happens
|
// serverless instance until it happens
|
||||||
waitUntil(client.close());
|
waitUntil(client.close());
|
||||||
|
|
||||||
res.send({ message: "Successfully owned server!" });
|
res.send({ message: "Successfully owned server!" });
|
||||||
|
|||||||
@ -28,43 +28,48 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* 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 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(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
const { userId } = getAuth(req);
|
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" });
|
||||||
|
|
||||||
if (!userId) {
|
const { ok } = await fetch(
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
`https://discord.com/api/guilds/${discordServerId}/widget.json`,
|
||||||
}
|
);
|
||||||
const { data } = req.body;
|
|
||||||
|
|
||||||
if (data === undefined) {
|
if (!ok) return res.status(400).send({ error: "Invalid value" });
|
||||||
res.status(400).send({ message: "Couldn't find data" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const v = obj.parse(data);
|
const { changeServer } = await checkOwnedServerMetadata(
|
||||||
for (const [key, value] of Object.entries(v)) {
|
getAuth(req).userId ?? null,
|
||||||
(await clerkClient()).users.updateUserMetadata(userId, {
|
mongo,
|
||||||
publicMetadata: {
|
{
|
||||||
[key]: typeof value === "number" ? value.toString() : value,
|
id: serverId as string,
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).send({ message: "Success" });
|
await changeServer({
|
||||||
|
discord: discordServerId as string,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(400).send({ error: error });
|
||||||
|
}
|
||||||
|
return res.send({ message: "Success" });
|
||||||
}
|
}
|
||||||
@ -28,25 +28,34 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import { checkOwnedServerMetadata } from "@/lib/check-owned-server";
|
||||||
import { getAuth, clerkClient } from "@clerk/nextjs/server";
|
import { getAuth } from "@clerk/nextjs/server";
|
||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from "mongodb";
|
||||||
import { waitUntil } from "@vercel/functions";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
const { userId } = getAuth(req);
|
try {
|
||||||
|
const { server: serverId } = req.query;
|
||||||
|
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
||||||
|
|
||||||
if (!userId) {
|
const { ownedServer, customizedServer, changeServer } =
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
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 });
|
||||||
}
|
}
|
||||||
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,17 +28,16 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getAuth, clerkClient } from "@clerk/nextjs/server";
|
import { getAuth, clerkClient } from "@clerk/nextjs/server";
|
||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from "mongodb";
|
||||||
import { waitUntil } from "@vercel/functions";
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
const { userId } = getAuth(req);
|
const { userId } = getAuth(req);
|
||||||
const { code } = req.body;
|
const { code } = req.query;
|
||||||
|
|
||||||
if (code == null) {
|
if (code == null) {
|
||||||
res.status(400).send({ message: "Couldn't find data" });
|
res.status(400).send({ message: "Couldn't find data" });
|
||||||
@ -59,7 +58,7 @@ export default async function handler(
|
|||||||
res.status(400).send({ message: "Couldn't find code" });
|
res.status(400).send({ message: "Couldn't find code" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
collection.findOneAndDelete({ code });
|
await collection.findOneAndDelete({ code });
|
||||||
const users = db.collection("claimed-users");
|
const users = db.collection("claimed-users");
|
||||||
await users.insertOne({ player: entry.player, userId });
|
await users.insertOne({ player: entry.player, userId });
|
||||||
|
|
||||||
@ -69,9 +68,5 @@ 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 });
|
res.send({ player: entry.player });
|
||||||
}
|
}
|
||||||
88
apps/www/src/pages/api/v1/user/get.ts
Normal file
88
apps/www/src/pages/api/v1/user/get.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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"
|
ulidx "^2.4.1"
|
||||||
zod "~3.22.3"
|
zod "~3.22.3"
|
||||||
|
|
||||||
input-otp@^1.2.4, input-otp@^1.4.2:
|
input-otp@^1.4.2:
|
||||||
version "1.4.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz"
|
resolved "https://registry.yarnpkg.com/input-otp/-/input-otp-1.4.2.tgz#f4d3d587d0f641729e55029b3b8c4870847f4f07"
|
||||||
integrity sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==
|
integrity sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==
|
||||||
|
|
||||||
inquirer@^12.3.0:
|
inquirer@^12.3.0:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user