fix: minor fixes

This commit is contained in:
dvelo 2025-05-10 13:07:01 -05:00
parent 3a727aad98
commit fa421252c1
13 changed files with 545 additions and 138 deletions

1
apps/www/.gitignore vendored

@ -35,6 +35,7 @@ yarn-error.log*
# local env files # local env files
.env*.local .env*.local
.env*.prod
# vercel # vercel
.vercel .vercel

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://mhsf.app/settings</loc><lastmod>2025-05-05T04:09:03.452Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> <url><loc>https://mhsf.app/waitlist</loc><lastmod>2025-05-10T18:03:30.182Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://mhsf.app/support</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> <url><loc>https://mhsf.app/home</loc><lastmod>2025-05-10T18:03:30.186Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://mhsf.app/waitlist/oauth-need-discord</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> <url><loc>https://mhsf.app/waitlist/oauth-need-discord</loc><lastmod>2025-05-10T18:03:30.186Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://mhsf.app/waitlist/ref</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> <url><loc>https://mhsf.app/servers/embedded/sl-modification-frame/files</loc><lastmod>2025-05-10T18:03:30.186Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://mhsf.app/servers/embedded/sl-modification-frame</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> <url><loc>https://mhsf.app/support</loc><lastmod>2025-05-10T18:03:30.186Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://mhsf.app/home</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> <url><loc>https://mhsf.app/servers/embedded/sl-modification-frame</loc><lastmod>2025-05-10T18:03:30.186Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://mhsf.app/servers/embedded/sl-modification-frame/files</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> <url><loc>https://mhsf.app/settings</loc><lastmod>2025-05-10T18:03:30.186Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://mhsf.app/servers</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> <url><loc>https://mhsf.app/waitlist/ref</loc><lastmod>2025-05-10T18:03:30.186Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://mhsf.app/waitlist</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url> <url><loc>https://mhsf.app/servers</loc><lastmod>2025-05-10T18:03:30.186Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
</urlset> </urlset>

@ -29,6 +29,7 @@
*/ */
@import "tailwindcss"; @import "tailwindcss";
@import url('https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,100..900;1,100..900&display=swap');
@plugin 'tailwindcss-animate'; @plugin 'tailwindcss-animate';
@config '../../tailwind-hero.config.ts'; @config '../../tailwind-hero.config.ts';
@ -36,6 +37,22 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
.milkdown {
--crepe-font-title: "Inter Tight", "Roboto" !important;
.milkdown-slash-menu {
position: fixed !important;
}
.dark .milkdown-icon {
fill: white !important;
}
.dark {
@import "@milkdown/crepe/theme/nord-dark.css";
}
}
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--border: 214.3 31.8% 91.4%; --border: 214.3 31.8% 91.4%;

@ -22,6 +22,18 @@ export function ModificationAction({ value }: { value?: Action }) {
if (!(value !== undefined && "customAction" in value)) { if (!(value !== undefined && "customAction" in value)) {
const filter = value as Filter; const filter = value as Filter;
let existing = -1; let existing = -1;
console.log(
(
(user?.unsafeMetadata.filters as Array<
ClerkEmbeddedFilter<unknown>
>) ?? []
).findIndex((c) => {
return (
JSON.stringify(c.metadata, replacer) === JSON.stringify(filter.toIdentifier(), replacer) &&
c.type === filter.getSpecificFilterId()
);
}),
);
if (isSignedIn) if (isSignedIn)
existing = ( existing = (
(user.unsafeMetadata.filters as Array< (user.unsafeMetadata.filters as Array<
@ -29,8 +41,7 @@ export function ModificationAction({ value }: { value?: Action }) {
>) ?? [] >) ?? []
).findIndex( ).findIndex(
(c) => (c) =>
JSON.stringify(c.metadata) === JSON.stringify(c.metadata, replacer) === JSON.stringify(filter.toIdentifier(), replacer) &&
JSON.stringify(filter.toIdentifier()) &&
c.type === filter.getSpecificFilterId(), c.type === filter.getSpecificFilterId(),
); );
else else
@ -40,8 +51,7 @@ export function ModificationAction({ value }: { value?: Action }) {
>) ?? [] >) ?? []
).findIndex( ).findIndex(
(c) => (c) =>
JSON.stringify(c.metadata) === JSON.stringify(c.metadata, replacer) === JSON.stringify(filter.toIdentifier(), replacer) &&
JSON.stringify(filter.toIdentifier()) &&
c.type === filter.getSpecificFilterId(), c.type === filter.getSpecificFilterId(),
); );
return existing; return existing;
@ -120,12 +130,12 @@ export function ModificationAction({ value }: { value?: Action }) {
type: filter.getSpecificFilterId(), type: filter.getSpecificFilterId(),
metadata: filter.toIdentifier(), metadata: filter.toIdentifier(),
}, },
]), ], replacer),
); );
else else
localStorage.setItem( localStorage.setItem(
"mhsf__filters", "mhsf__filters",
JSON.stringify(existingArray), JSON.stringify(existingArray, replacer),
); );
} }
@ -140,3 +150,12 @@ export function ModificationAction({ value }: { value?: Action }) {
</> </>
); );
} }
const replacer = (key, value) =>
value instanceof Object && !(Array.isArray(value)) ?
Object.keys(value)
.sort()
.reduce((sorted, key) => {
sorted[key] = value[key];
return sorted
}, {}) :
value;

@ -0,0 +1,36 @@
/*
* 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 '@milkdown/crepe/theme/nord-dark.css';
import type { ReactNode } from 'react';
export function ServerDescriptionDarkProvider({children}: {children: ReactNode}) {
return children;
}

@ -28,29 +28,58 @@
* OTHER DEALINGS IN THE SOFTWARE. * OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { listenerCtx } from '@milkdown/kit/plugin/listener'; import { listenerCtx } from "@milkdown/kit/plugin/listener";
import { Crepe } from '@milkdown/crepe'; import { Crepe } from "@milkdown/crepe";
import { Milkdown, useEditor } from '@milkdown/react'; import { useEditor, type EditorInfoCtx, Milkdown } from "@milkdown/react";
import '@milkdown/crepe/theme/common/style.css'; import "@milkdown/crepe/theme/common/style.css";
import '@milkdown/crepe/theme/nord-dark.css'; import { createContext } from "react";
import { Spinner } from "@/components/ui/spinner";
import { ServerDescriptionLightProvider } from "./server-light-provider";
import { useTheme } from "@/lib/hooks/use-theme";
import { ServerDescriptionDarkProvider } from "./server-dark-provider";
export function ServerEditorDescription({ defaultMarkdown, onUpdate }: { defaultMarkdown: string, onUpdate?: (update: string) => void }) { export function ServerEditorDescription({
useEditor((root) => { defaultMarkdown,
onUpdate,
}: { defaultMarkdown: string; onUpdate?: (update: string) => void }) {
const { resolvedTheme } = useTheme();
const { loading } = useEditor((root) => {
const crepe = new Crepe({ const crepe = new Crepe({
root, root,
defaultValue: defaultMarkdown, defaultValue: defaultMarkdown,
}); });
crepe.editor.config(async (ctx) => { crepe.editor.config(async (ctx) => {
ctx.get(listenerCtx).markdownUpdated((_, markdown) => { ctx.get(listenerCtx).markdownUpdated((_, markdown) => {
if (onUpdate) if (onUpdate) onUpdate(markdown);
onUpdate(markdown);
}); });
}); });
return crepe; return crepe;
}, []); }, []);
return ( return (
<Milkdown /> <div className="h-[500px] max-h-[500px]">
{loading && (
<div>
<span className="flex items-center justify-center w-full">
<Spinner />
</span>
<span className="flex items-center justify-center w-full mt-2">
Loading Milkdown
</span>
</div>
)}
{resolvedTheme === "dark" ? (
<ServerDescriptionLightProvider>
<Milkdown />
</ServerDescriptionLightProvider>
) : (
<ServerDescriptionDarkProvider>
<Milkdown />
</ServerDescriptionDarkProvider>
)}
</div>
); );
} }
const editorInfoContext = createContext<EditorInfoCtx>({} as EditorInfoCtx);

@ -1,61 +1,257 @@
import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Drawer, DrawerContent, DrawerTitle } from "@/components/ui/drawer"; import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerTitle,
} from "@/components/ui/drawer";
import { type ReactNode, useEffect, useState } from "react"; import { type ReactNode, useEffect, useState } from "react";
import { ServerEditorDescription } from "./server-editor-description"; import { ServerEditorDescription } from "./server-editor-description";
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server"; import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
import { MilkdownProvider } from "@milkdown/react"; import { MilkdownProvider } from "@milkdown/react";
import { Material } from "@/components/ui/material"; import { Material } from "@/components/ui/material";
import { Placeholder } from "@/components/ui/placeholder"; import { Placeholder } from "@/components/ui/placeholder";
import { X } from "lucide-react"; import { Check, X } from "lucide-react";
import { Link } from "@/components/util/link";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
import { useServers } from "@/lib/hooks/use-servers";
import { Alert } from "@/components/ui/alert";
import { toast } from "sonner";
const successClasses =
"bg-green-200 border-green-400 dark:bg-green-800 dark:border-green-600";
const errorClasses =
"bg-red-200 border-red-400 dark:bg-red-800 dark:border-red-600";
export function ServerEditorProvider({ export function ServerEditorProvider({
children, children,
serverData, serverData,
minehutData,
}: { }: {
children: ReactNode | ReactNode[]; children: ReactNode | ReactNode[];
serverData: ReturnType<typeof useMHSFServer>; serverData: ReturnType<typeof useMHSFServer>;
minehutData: ServerResponse;
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [onlineData, setOnlineData] = useState<OnlineServer>();
const { servers, loading } = useServers();
const [claimedUser, setClaimedUser] = useState<string>();
useEffect(() => { useEffect(() => {
window.addEventListener("open-server-editor", () => setOpen(true)); window.addEventListener("open-server-editor", () => {
setOpen(true);
});
}, []); }, []);
useEffect(() => {
if (open && !loading) {
const server = servers.find((c) => c.name === minehutData.name);
if (server !== null) {
setOnlineData(server);
}
}
}, [open, loading]);
useEffect(() => {
(async () => {
const response = await fetch("/api/v1/user/claimed-user");
const json = await response.json();
setClaimedUser(json.player ?? null);
})();
});
const requirementOne = minehutData.online;
const requirementTwo = onlineData !== null;
const requirementThree = claimedUser === onlineData?.author;
const requirementFour = claimedUser !== null;
return ( return (
<> <>
{children} {children}
<MilkdownProvider> <MilkdownProvider>
<Drawer open={open} onOpenChange={setOpen}> <Drawer open={open} onOpenChange={setOpen}>
<DrawerContent className="p-4 "> <DrawerContent className="p-4 ">
{serverData.server?.customizationData.isOwnedByUser ? ( <br />
{!serverData.server?.customizationData.isOwned ? (
<div className="h-full overflow-y-scroll"> <div className="h-full overflow-y-scroll">
<DrawerTitle className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3"> <DrawerTitle className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
Server Settings Own your server.
</DrawerTitle> </DrawerTitle>
<Material className="grid gap-1 max-h-[700px]"> <DrawerDescription>
<strong>Server Description</strong> Own your server to completely control your server and how it
<p className="mb-3"> appears on MHSF.{" "}
A markdown enabled, fancy description for your server! <Link
Describe what players will expect from your server and why href="https://mhsf.mintlify.app/guides/owning-a-server"
they should join; don't worry, you have more space than className="text-black dark:text-white"
MOTD's. >
Learn more about server owning.
</Link>
</DrawerDescription>
{onlineData?.author === undefined && (
<Alert variant="error">
This server cannot be automatically linked as it doesn't
have an author. You must either contact support to link this
server or link your Minehut account to your Minecraft: Java
Edition one.
</Alert>
)}
<div className="mt-2 text-sm">
<p className="pb-2">
<code
className={cn(
"border rounded-full h-[1.75rem] w-[1.75rem] absolute inline-flex items-center justify-center",
requirementOne ? successClasses : errorClasses,
)}
>
{requirementOne ? <Check size={16} /> : <X size={16} />}
</code>
<span className="ml-[2.25rem] pt-0.5 grid grid-rows-2">
<strong>Your server must be online.</strong> This is
required as this is the only way to ensure you are the
owner of the server.
</span>
</p> </p>
{!serverData.loading && ( {requirementOne && (
<ServerEditorDescription <>
defaultMarkdown={ <p className="pb-2">
serverData.server?.customizationData.description ?? "" <code
} className={cn(
/> "border rounded-full h-[1.75rem] w-[1.75rem] absolute inline-flex items-center justify-center",
requirementTwo !== null
? successClasses
: errorClasses,
)}
>
{requirementTwo !== null ? (
<Check size={16} />
) : (
<X size={16} />
)}
</code>
<span className="ml-[2.25rem] pt-0.5 grid grid-rows-2">
<strong>
Your server must be on the server list.
</strong>
This sometimes can't be achieved if the server isn't
visible or another issue appears.
</span>
</p>
{requirementTwo && (
<>
<p className="pb-2">
<code
className={cn(
"border rounded-full h-[1.75rem] w-[1.75rem] absolute inline-flex items-center justify-center",
requirementThree
? successClasses
: errorClasses,
)}
>
{requirementThree ? (
<Check size={16} />
) : (
<X size={16} />
)}
</code>
<span className="ml-[2.25rem] pt-0.5 grid grid-rows-2">
<strong>
Your account must be linked to{" "}
<i>{onlineData?.author}</i>
</strong>
You must link your MHSF account to your Minecraft:
Java Edition account that is the owner of this
server to be able to link this server.
</span>
</p>
<p className="pb-2">
<code
className={cn(
"border rounded-full h-[1.75rem] w-[1.75rem] absolute inline-flex items-center justify-center",
requirementFour ? successClasses : errorClasses,
)}
>
{requirementFour ? (
<Check size={16} />
) : (
<X size={16} />
)}
</code>
<span className="ml-[2.25rem] pt-0.5 grid grid-rows-2">
<strong>
Your account must be linked on MHSF
</strong>
You must link your account to your Minecraft: Java
Edition account to be able to link this server.
</span>
</p>
</>
)}
</>
)} )}
</Material> </div>
<DrawerFooter>
<Button
onClick={() =>
toast.promise(serverData.ownServer(), {
success: "Successfully owned server",
error:
"There was an error while linking this server. Please contact support.",
loading: "Linking server...",
})
}
disabled={
!(
requirementOne &&
requirementTwo &&
requirementThree &&
requirementFour
)
}
>
Own Server
</Button>
</DrawerFooter>
</div> </div>
) : ( ) : (
<Placeholder <>
icon={<X />} {serverData.server?.customizationData.isOwnedByUser ? (
className="h-full justify-center flex items-center" <div className="max-h-[400px] overflow-y-scroll">
title="You don't own this server" <DrawerTitle className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
description="Unfortunately, that one ain't gonna work. Atleast not on my watch." Server Settings
/> </DrawerTitle>
<Material className="grid gap-1 max-h-[700px]">
<strong>Server Description</strong>
<p className="mb-3">
A markdown enabled, fancy description for your server!
Describe what players will expect from your server and
why they should join; don't worry, you have more space
than MOTD's.
</p>
{!serverData.loading && (
<ServerEditorDescription
defaultMarkdown={
serverData.server?.customizationData.description ??
""
}
onUpdate={(content) => console.log(content)}
/>
)}
</Material>
</div>
) : (
<Placeholder
icon={<X />}
className="h-full justify-center flex items-center"
title="You don't own this server"
description="Unfortunately, that one ain't gonna work. Atleast not on my watch."
/>
)}
</>
)} )}
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>

@ -0,0 +1,36 @@
/*
* 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 '@milkdown/crepe/theme/frame.css';
import type { ReactNode } from 'react';
export function ServerDescriptionLightProvider({children}: {children: ReactNode}) {
return children;
}

@ -31,7 +31,7 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ServerResponse } from "@/lib/types/mh-server"; import { ServerResponse } from "@/lib/types/mh-server";
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs"; import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
import { EllipsisVertical, Flag, Heart, Share, Star } from "lucide-react"; import { EllipsisVertical, Flag, Heart, Pencil, Share, Star } from "lucide-react";
import { useFavoriteStore } from "@/lib/hooks/use-favorite-store"; import { useFavoriteStore } from "@/lib/hooks/use-favorite-store";
import { useState } from "react"; import { useState } from "react";
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server"; import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
@ -105,6 +105,10 @@ export function ServerPageButtons({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuSeparator>Server</DropdownMenuSeparator> <DropdownMenuSeparator>Server</DropdownMenuSeparator>
<DropdownMenuItem className="flex items-center gap-2" onClick={() => window.dispatchEvent(new Event("open-server-editor"))}>
<Pencil size={16} />
Edit Server
</DropdownMenuItem>
<DropdownMenuItem className="flex items-center gap-2"> <DropdownMenuItem className="flex items-center gap-2">
<Share size={16} /> <Share size={16} />
Share Share

@ -72,7 +72,7 @@ export function ServerProvider({ serverId }: { serverId: string }) {
</div> </div>
) : ( ) : (
<div className="px-10"> <div className="px-10">
<ServerEditorProvider serverData={mhsf}> <ServerEditorProvider serverData={mhsf} minehutData={server as ServerResponse}>
<ReportingProvider server={mhsf}> <ReportingProvider server={mhsf}>
<ServerMainPage <ServerMainPage
server={server as ServerResponse} server={server as ServerResponse}

@ -30,7 +30,7 @@
import { getBackendProcedure } from "@/lib/backend-procedure"; import { getBackendProcedure } from "@/lib/backend-procedure";
import type { MHSFData } from "@/lib/types/data"; import type { MHSFData } from "@/lib/types/data";
import { clerkClient, getAuth } from "@clerk/nextjs/server"; import { clerkClient, getAuth, User } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
@ -78,12 +78,12 @@ export default async function handler(
const serverData = await findServerData(server as string); const serverData = await findServerData(server as string);
if (!serverData.exists) return res.status(404).send({ server: null }); if (!serverData.exists) return res.status(404).send({ server: null });
const mongo = new MongoClient(process.env.MONGO_DB as string); const mongo = new MongoClient(process.env.MONGO_DB as string);
try { try {
await mongo.connect(); await mongo.connect();
const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const {userId} = getAuth(req); const { userId } = getAuth(req);
// Run queries in parallel // Run queries in parallel
const [favoriteData, customizationData, playerData, achievements] = const [favoriteData, customizationData, playerData, achievements] =
@ -93,7 +93,12 @@ export default async function handler(
favoriteTimespanStart, favoriteTimespanStart,
favoriteTimespanEnd, favoriteTimespanEnd,
}), }),
findCustomizationData(serverData.name, userId ?? undefined, db), findCustomizationData(
serverData.name,
server as string,
userId ?? undefined,
db,
),
findPlayerData(serverData.name, db, { findPlayerData(serverData.name, db, {
maxPlayerEntries, maxPlayerEntries,
playerTimespanStart, playerTimespanStart,
@ -136,6 +141,7 @@ export default async function handler(
async function findCustomizationData( async function findCustomizationData(
serverName: string, serverName: string,
serverId: string,
userId: string | undefined, userId: string | undefined,
db: any, db: any,
): Promise<{ ): Promise<{
@ -150,18 +156,40 @@ async function findCustomizationData(
const clerk = await clerkClient(); const clerk = await clerkClient();
// Run queries in parallel // Run queries in parallel
const [customizationData, ownedServerData] = await Promise.all([ const [customizationData, ownedServerData] = await Promise.all([
db.collection("customization").findOne({ server: serverName }), db.collection("customization").findOne({ server: serverId }),
userId userId
? db.collection("owned-servers").findOne({ server: serverName }) ? db.collection("owned-servers").findOne({ server: serverId })
: null, : null,
]); ]);
let user: User | undefined = undefined;
try {
user = await clerk.users.getUser(ownedServerData?.author);
} catch (e) {
return {
isOwned: false,
isOwnedByUser: false,
description: undefined,
banner: undefined,
discord: undefined,
colorScheme: undefined,
userProfilePicture: undefined,
};
}
if (customizationData) { console.log(
ownedServerData?.author === userId,
userId,
ownedServerData?.author,
);
if (customizationData || ownedServerData) {
return { return {
...(customizationData as any), ...(customizationData as any),
isOwned: true, isOwned: true,
isOwnedByUser: ownedServerData?.author === userId, isOwnedByUser: ownedServerData?.author === userId,
userProfilePicture: userId ? (await clerk.users.getUser(ownedServerData?.author)).imageUrl : 'no user' userProfilePicture: userId
? user.imageUrl
: "no user",
}; };
} }

@ -36,8 +36,8 @@ import { waitUntil } from "@vercel/functions";
import { getBackendProcedure } from "@/lib/backend-procedure"; import { getBackendProcedure } from "@/lib/backend-procedure";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse,
) { ) {
const backendProcedure = await getBackendProcedure(req); const backendProcedure = await getBackendProcedure(req);
@ -45,90 +45,79 @@ export default async function handler(
return res.status(403).json({ return res.status(403).json({
error: `Backend procedure marked request as '${backendProcedure.status}' instead of required 'OK'`, error: `Backend procedure marked request as '${backendProcedure.status}' instead of required 'OK'`,
}); });
const { userId } = getAuth(req); const { userId } = getAuth(req);
const { server } = req.query; const { server } = req.query;
if (server == null) { if (server == null) {
res.status(400).send({ message: "Couldn't find data" }); res.status(400).send({ message: "Couldn't find data" });
return; return;
} }
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Unauthorized" }); return res.status(401).json({ error: "Unauthorized" });
} }
if ( const client = new MongoClient(process.env.MONGO_DB as string);
(await (await clerkClient()).users.getUser(userId)).publicMetadata.player === await client.connect();
undefined
) {
return res.status(401).json({ error: "Account not linked" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers"); const collection = db.collection("owned-servers");
const users = db.collection("claimed-users");
if ((await collection.findOne({ server: server })) === undefined) { if ((await users.findOne({ userId })) === null) {
const mh = await fetch( return res.status(401).json({ error: "Account not linked" });
process.env.MHSF_BACKEND_API_LOCATION ?? }
"https://api.minehut.com/servers",
{
headers: {
accept: "*/*",
"accept-language": Math.random().toString(),
priority: "u=1, i",
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
Referer: "http://localhost:3000/",
"Referrer-Policy": "strict-origin-when-cross-origin",
Authentication: `MHSF-Backend-Server ${process.env.MHSF_BACKEND_API_LOCATION ? process.env.MHSF_BACKEND_SECRET : "Sorry Minehut Devs."}`,
},
body: null,
method: "GET",
}
);
const servers: Array<OnlineServer> = (await mh.json()).servers;
servers.forEach(async (c, i) => { const minecraftUsername = (await users.findOne({ userId }))?.player;
if (c.name === server) {
const MCUsername = (await (await clerkClient()).users.getUser(userId))
.publicMetadata.player;
if (MCUsername === c.author) { if ((await collection.findOne({ server })) === null) {
await collection.insertOne({ server, author: userId }); const mh = await fetch(
process.env.MHSF_BACKEND_API_LOCATION ??
"https://api.minehut.com/servers",
{
headers: {
accept: "*/*",
"accept-language": Math.random().toString(),
priority: "u=1, i",
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
Referer: "http://localhost:3000/",
"Referrer-Policy": "strict-origin-when-cross-origin",
Authentication: `MHSF-Backend-Server ${process.env.MHSF_BACKEND_API_LOCATION ? process.env.MHSF_BACKEND_SECRET : "Sorry Minehut Devs."}`,
},
body: null,
method: "GET",
},
);
const servers: Array<OnlineServer> = (await mh.json()).servers;
const serverObj = servers.find((c) => c.staticInfo._id === server);
// Close the database, but don't close this if (serverObj === undefined)
// serverless instance until it happens return res
waitUntil(client.close()); .status(400)
.send({ message: "The server needs to be online." });
res.send({ message: "Successfully owned server!" }); if (minecraftUsername === serverObj.author) {
} else { await collection.insertOne({ server, author: userId });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res // Close the database, but don't close this
.status(400) // serverless instance until it happens
.send({ message: "The linked account doesn't own the server." }); waitUntil(client.close());
}
}
if (i === servers.length) {
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.status(400).send({ message: "The server needs to be online." }); res.send({ message: "Successfully owned server!" });
} } else {
}); res
} else { .status(400)
// Close the database, but don't close this .send({ message: "The linked account doesn't own the server." });
// serverless instance until it happens }
waitUntil(client.close()); } else {
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.status(400).send({ message: "This server has already been owned." }); res.status(400).send({ message: "This server has already been owned." });
} }
} }

@ -0,0 +1,52 @@
/*
* 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 { NextApiRequest, NextApiResponse } from "next";
import { getAuth, clerkClient } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const users = db.collection("claimed-users");
return res.send((await users.findOne({ userId })) ?? {player: null});
}