mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-09 09:04:59 -05:00
Compare commits
No commits in common. "21d66742c64093462de20985ba7ada2e236ee544" and "20afc2de8e1c793891658d13e5aa9c9cd7e25ebd" have entirely different histories.
21d66742c6
...
20afc2de8e
@ -45,12 +45,7 @@ const nextConfig = {
|
|||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "cdn.discordapp.com"
|
hostname: "cdn.discordapp.com"
|
||||||
},
|
}
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "exh89c9lva.ufs.sh",
|
|
||||||
pathname: "/f/*",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
|
|||||||
@ -43,7 +43,6 @@
|
|||||||
"@radix-ui/react-switch": "1.1.0",
|
"@radix-ui/react-switch": "1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.3",
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@shikijs/rehype": "^3.4.0",
|
|
||||||
"@tanstack/react-query": "^5.69.0",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
"@trpc/client": "^11.0.0",
|
"@trpc/client": "^11.0.0",
|
||||||
"@trpc/next": "^11.0.0",
|
"@trpc/next": "^11.0.0",
|
||||||
@ -94,15 +93,10 @@
|
|||||||
"react-fade-in": "^2.0.1",
|
"react-fade-in": "^2.0.1",
|
||||||
"react-fast-marquee": "^1.6.5",
|
"react-fast-marquee": "^1.6.5",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"react-qr-code": "^2.0.15",
|
"react-qr-code": "^2.0.15",
|
||||||
"react-shiki": "^0.6.0",
|
|
||||||
"react-snowfall": "^2.2.0",
|
"react-snowfall": "^2.2.0",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"rehype-pretty-code": "^0.14.1",
|
|
||||||
"rehype-react": "^8.0.0",
|
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark-breaks": "^4.0.0",
|
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"request-ip": "^3.3.0",
|
"request-ip": "^3.3.0",
|
||||||
"sonner": "^1.7.0",
|
"sonner": "^1.7.0",
|
||||||
@ -161,6 +155,7 @@
|
|||||||
"react-hook-form": "^7.52.2",
|
"react-hook-form": "^7.52.2",
|
||||||
"react-hotkeys-hook": "^4.5.0",
|
"react-hotkeys-hook": "^4.5.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
"react-resizable-panels": "^2.0.23",
|
"react-resizable-panels": "^2.0.23",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"shiki": "^1.23.0",
|
"shiki": "^1.23.0",
|
||||||
|
|||||||
@ -37,26 +37,15 @@ import { toast } from "sonner";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Material } from "@/components/ui/material";
|
import { Material } from "@/components/ui/material";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
import { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import ShikiHighlighter, {
|
|
||||||
type Element,
|
|
||||||
isInlineCode,
|
|
||||||
rehypeInlineCodeProperty,
|
|
||||||
} from "react-shiki";
|
|
||||||
|
|
||||||
export function MOTDRow({
|
export function MOTDRow({
|
||||||
server,
|
server,
|
||||||
mhsfData,
|
mhsfData,
|
||||||
}: { server: ServerResponse; mhsfData: ReturnType<typeof useMHSFServer> }) {
|
}: { server: ServerResponse; mhsfData: ReturnType<typeof useMHSFServer> }) {
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const [tab, setTab] = useState(
|
const [tab, setTab] = useState(mhsfData.server?.customizationData.description !== undefined ? "description" : "motd");
|
||||||
mhsfData.server?.customizationData.description !== undefined
|
|
||||||
? "description"
|
|
||||||
: "motd",
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Material className="p-4 relative h-[250px]">
|
<Material className="p-4 relative h-[250px]">
|
||||||
@ -83,7 +72,7 @@ export function MOTDRow({
|
|||||||
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
|
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
|
||||||
"rounded-xl px-2 flex items-center gap-2",
|
"rounded-xl px-2 flex items-center gap-2",
|
||||||
tab === "description" &&
|
tab === "description" &&
|
||||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
|
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
|
||||||
)}
|
)}
|
||||||
onClick={() => setTab("description")}
|
onClick={() => setTab("description")}
|
||||||
>
|
>
|
||||||
@ -121,37 +110,9 @@ export function MOTDRow({
|
|||||||
)}
|
)}
|
||||||
{tab === "description" && (
|
{tab === "description" && (
|
||||||
<div className="prose mt-2 break-words overflow-y-auto max-h-[175px] min-w-full dark:prose-invert">
|
<div className="prose mt-2 break-words overflow-y-auto max-h-[175px] min-w-full dark:prose-invert">
|
||||||
<Markdown
|
<Markdown className="min-w-full">{mhsfData.server?.customizationData.description}</Markdown>
|
||||||
components={{
|
|
||||||
code: CodeHighlight,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{mhsfData.server?.customizationData.description}
|
|
||||||
</Markdown>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Material>
|
</Material>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodeHighlightProps {
|
|
||||||
className?: string | undefined;
|
|
||||||
children?: ReactNode | undefined;
|
|
||||||
node?: Element | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CodeHighlight = ({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
node,
|
|
||||||
...props
|
|
||||||
}: CodeHighlightProps): ReactNode => {
|
|
||||||
const match = className?.match(/language-(\w+)/);
|
|
||||||
const language = match ? match[1] : undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ShikiHighlighter language={language?.toLocaleLowerCase()} theme="poimandres" {...props}>
|
|
||||||
{String(children)}
|
|
||||||
</ShikiHighlighter>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
|
||||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
|
||||||
import type { BannerUploaderRouter } from "@/pages/api/v1/server/get/[server]/settings/upload-banner";
|
|
||||||
import { generateUploadDropzone } from "@uploadthing/react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function ServerBannerBox({
|
|
||||||
serverData,
|
|
||||||
minehutData
|
|
||||||
}: {
|
|
||||||
serverData: ReturnType<typeof useMHSFServer>;
|
|
||||||
minehutData: ServerResponse;
|
|
||||||
}) {
|
|
||||||
const UploadDropzone = generateUploadDropzone<BannerUploaderRouter>({
|
|
||||||
url: `/api/v1/server/get/${minehutData._id}/settings/upload-banner`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Material className="grid gap-1 mt-2 max-h-[700px]">
|
|
||||||
<strong className="flex items-center gap-2">Server Banner </strong>
|
|
||||||
<p className="mb-3">
|
|
||||||
Pick out whatever represents your server best! Images have a limit of
|
|
||||||
4.5MB, and the prefered aspect ratio for the banner should be 19:11 to
|
|
||||||
look the best on MHSF. Powered by UploadThing.
|
|
||||||
</p>
|
|
||||||
<UploadDropzone
|
|
||||||
endpoint="imageUploader"
|
|
||||||
className="uploadthing-dropzone"
|
|
||||||
onClientUploadComplete={(res) => {
|
|
||||||
console.log("Upload complete response:", res);
|
|
||||||
// Refresh the server data
|
|
||||||
serverData.refresh();
|
|
||||||
toast.success("Banner uploaded successfully!");
|
|
||||||
}}
|
|
||||||
onUploadError={(error: Error) => {
|
|
||||||
console.error("Upload error:", error);
|
|
||||||
toast.error(`Upload failed: ${error.message}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Material>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
|
||||||
import { ServerResponse } from "@/lib/types/mh-server";
|
|
||||||
import { debounce } from "lodash";
|
|
||||||
import { Moon, Sun } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export function ServerColorModeBox({
|
|
||||||
serverData,
|
|
||||||
minehutData,
|
|
||||||
}: {
|
|
||||||
serverData: ReturnType<typeof useMHSFServer>;
|
|
||||||
minehutData: ServerResponse;
|
|
||||||
}) {
|
|
||||||
const [colorModeEnabled, setColorModeEnabled] = useState(false);
|
|
||||||
const [colorMode, setColorMode] = useState("light");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setColorModeEnabled(
|
|
||||||
serverData.server?.customizationData.colorMode !== null,
|
|
||||||
);
|
|
||||||
setColorMode(serverData.server?.customizationData.colorMode ?? "light");
|
|
||||||
}, [serverData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (colorMode === "idc") {
|
|
||||||
setColorModeEnabled(false);
|
|
||||||
setColorMode("light");
|
|
||||||
}
|
|
||||||
if (colorModeEnabled) {
|
|
||||||
if (colorMode === "dark")
|
|
||||||
window.dispatchEvent(new Event("force-dark-mode"));
|
|
||||||
|
|
||||||
if (colorMode === "light")
|
|
||||||
window.dispatchEvent(new Event("force-light-mode"));
|
|
||||||
} else window.dispatchEvent(new Event("force-no-mode"));
|
|
||||||
}, [colorMode, colorModeEnabled]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
update();
|
|
||||||
}, [colorMode, colorModeEnabled]);
|
|
||||||
|
|
||||||
const update = debounce(async () => {
|
|
||||||
await fetch(
|
|
||||||
`/api/v1/server/get/${minehutData._id}/settings/change-color-mode${colorModeEnabled !== false ? `?colorMode=${colorMode}` : ""}`,
|
|
||||||
);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Material className="flex justify-between items-center p-2 mt-2">
|
|
||||||
<div className="flex items-center font-bold gap-4">
|
|
||||||
<Switch
|
|
||||||
checked={colorModeEnabled}
|
|
||||||
onCheckedChange={setColorModeEnabled}
|
|
||||||
/>{" "}
|
|
||||||
Enforce color mode
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
disabled={!colorModeEnabled}
|
|
||||||
onValueChange={setColorMode}
|
|
||||||
value={colorMode}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[180px] disabled:hidden">
|
|
||||||
<SelectValue placeholder="Select mode" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectItem value="dark">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Moon size={16} />
|
|
||||||
Dark
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="light">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Sun size={16} />
|
|
||||||
Light
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="idc">I couldn't care less</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</Material>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
|
||||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
|
||||||
import { ServerEditorDescription } from "../server-editor-description";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { debounce } from "lodash";
|
|
||||||
|
|
||||||
export function ServerDescriptionBox({
|
|
||||||
serverData,
|
|
||||||
minehutData,
|
|
||||||
}: {
|
|
||||||
serverData: ReturnType<typeof useMHSFServer>;
|
|
||||||
minehutData: ServerResponse;
|
|
||||||
}) {
|
|
||||||
const [descriptionSaved, setDescriptionSaved] = useState(true);
|
|
||||||
|
|
||||||
const save = debounce(async (content) => {
|
|
||||||
await fetch(`/api/v1/server/get/${minehutData._id}/settings/change-description?description=${encodeURIComponent(btoa(content))}`);
|
|
||||||
setDescriptionSaved(true);
|
|
||||||
}, 250);
|
|
||||||
const reload = debounce(async () => {
|
|
||||||
await serverData.refresh()
|
|
||||||
}, 1000)
|
|
||||||
return (
|
|
||||||
<Material className="grid gap-1 max-h-[700px]">
|
|
||||||
<strong className="flex items-center gap-2">Server Description <Badge className="ml-2">{descriptionSaved ? "Saved" : "Not Saved"}</Badge></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 ??
|
|
||||||
`# ${minehutData.name}`
|
|
||||||
}
|
|
||||||
onUpdate={async (content) => {
|
|
||||||
setDescriptionSaved(false);
|
|
||||||
save(content);
|
|
||||||
reload();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Material>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DrawerTitle } from "@/components/ui/drawer";
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function ServerMigrationBox({
|
|
||||||
oldVersion,
|
|
||||||
reupdate,id
|
|
||||||
}: { oldVersion: number | undefined, reupdate: () => void, id: string }) {
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies:
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
const response = await fetch(`/api/v1/server/get/${id}/settings/migrate`);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
reupdate();
|
|
||||||
} else {
|
|
||||||
const json = await response.json()
|
|
||||||
toast.error(json.error)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DrawerTitle>Migrate Server</DrawerTitle>
|
|
||||||
<Material className="grid gap-1 max-h-[700px]">
|
|
||||||
<p className="mb-3">
|
|
||||||
This server must be migrated from version {oldVersion ?? 1} of MHSF.
|
|
||||||
This is an automatic process.
|
|
||||||
</p>
|
|
||||||
<div className="w-full justify-center flex items-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
</Material>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -20,15 +20,11 @@ import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
|||||||
import { useServers } from "@/lib/hooks/use-servers";
|
import { useServers } from "@/lib/hooks/use-servers";
|
||||||
import { Alert } from "@/components/ui/alert";
|
import { Alert } from "@/components/ui/alert";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { BannerUploaderRouter } from "@/pages/api/v1/server/get/[server]/settings/upload-banner";
|
import { BannerUploaderRouter } from "@/pages/api/v1/server/get/[server]/settings/upload-banner";
|
||||||
import {
|
import {
|
||||||
generateUploadButton,
|
generateUploadButton,
|
||||||
generateUploadDropzone,
|
generateUploadDropzone,
|
||||||
} from "@uploadthing/react";
|
} from "@uploadthing/react";
|
||||||
import { ServerDescriptionBox } from "./customizations/server-description-box";
|
|
||||||
import { ServerBannerBox } from "./customizations/server-banner-box";
|
|
||||||
import { ServerMigrationBox } from "./customizations/server-migration-box";
|
|
||||||
import { ServerColorModeBox } from "./customizations/server-color-mode-box";
|
|
||||||
|
|
||||||
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";
|
||||||
@ -232,32 +228,49 @@ export function ServerEditorProvider({
|
|||||||
<>
|
<>
|
||||||
{serverData.server?.customizationData.isOwnedByUser ? (
|
{serverData.server?.customizationData.isOwnedByUser ? (
|
||||||
<div className="!max-h-[700px] !h-[700px] overflow-y-scroll">
|
<div className="!max-h-[700px] !h-[700px] overflow-y-scroll">
|
||||||
{serverData.server?.customizationData
|
<DrawerTitle className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
||||||
.customizationVersion === 2 ? (
|
Server Settings
|
||||||
<div>
|
</DrawerTitle>
|
||||||
<DrawerTitle className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
<Material className="grid gap-1 max-h-[700px]">
|
||||||
Server Settings
|
<strong>Server Description</strong>
|
||||||
</DrawerTitle>
|
<p className="mb-3">
|
||||||
<ServerDescriptionBox
|
A markdown enabled, fancy description for your server!
|
||||||
serverData={serverData}
|
Describe what players will expect from your server and
|
||||||
minehutData={minehutData}
|
why they should join; don't worry, you have more space
|
||||||
|
than MOTD's.
|
||||||
|
</p>
|
||||||
|
{!serverData.loading && (
|
||||||
|
<ServerEditorDescription
|
||||||
|
defaultMarkdown={
|
||||||
|
serverData.server?.customizationData.description ??
|
||||||
|
`# ${minehutData.name}`
|
||||||
|
}
|
||||||
|
onUpdate={(content) => console.log(content)}
|
||||||
/>
|
/>
|
||||||
<ServerBannerBox
|
)}
|
||||||
serverData={serverData}
|
</Material>
|
||||||
minehutData={minehutData}
|
<Material className="grid gap-1 mt-2 max-h-[700px]">
|
||||||
/>
|
<strong>Server Banner</strong>
|
||||||
<ServerColorModeBox
|
<p className="mb-3">
|
||||||
serverData={serverData}
|
Pick out whatever represents your server best! Images
|
||||||
minehutData={minehutData}
|
have a limit of 4.5MB, and the prefered aspect ratio for
|
||||||
/>
|
the banner should be 19:11 to look the best on MHSF.
|
||||||
</div>
|
</p>
|
||||||
) : (
|
<UploadDropzone
|
||||||
<ServerMigrationBox
|
endpoint="imageUploader"
|
||||||
oldVersion={1}
|
className="uploadthing-dropzone"
|
||||||
reupdate={() => serverData.refresh()}
|
onClientUploadComplete={(res) => {
|
||||||
id={minehutData._id}
|
console.log("Upload complete response:", res);
|
||||||
|
// Refresh the server data
|
||||||
|
serverData.refresh();
|
||||||
|
toast.success("Banner uploaded successfully!");
|
||||||
|
}}
|
||||||
|
onUploadError={(error: Error) => {
|
||||||
|
console.error("Upload error:", error);
|
||||||
|
toast.error(`Upload failed: ${error.message}`);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</Material>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Placeholder
|
<Placeholder
|
||||||
|
|||||||
@ -31,14 +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 {
|
import { EllipsisVertical, Flag, Heart, Pencil, Share, Star } from "lucide-react";
|
||||||
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";
|
||||||
@ -50,8 +43,6 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import useClipboard from "@/lib/useClipboard";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function ServerPageButtons({
|
export function ServerPageButtons({
|
||||||
server,
|
server,
|
||||||
@ -61,7 +52,6 @@ export function ServerPageButtons({
|
|||||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||||
}) {
|
}) {
|
||||||
const clerk = useClerk();
|
const clerk = useClerk();
|
||||||
const clipboard = useClipboard();
|
|
||||||
const favoritesStore = useFavoriteStore(mhsfData);
|
const favoritesStore = useFavoriteStore(mhsfData);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@ -115,29 +105,11 @@ export function ServerPageButtons({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuSeparator>Server</DropdownMenuSeparator>
|
<DropdownMenuSeparator>Server</DropdownMenuSeparator>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem className="flex items-center gap-2" onClick={() => window.dispatchEvent(new Event("open-server-editor"))}>
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() =>
|
|
||||||
window.dispatchEvent(new Event("open-server-editor"))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Pencil size={16} />
|
<Pencil size={16} />
|
||||||
Edit Server
|
Edit Server
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem className="flex items-center gap-2">
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => {
|
|
||||||
const data = {
|
|
||||||
url: `https://mhsf.app/server/v2/minehut/${server._id}`,
|
|
||||||
text: "Check out MHSF, the modern server finder!",
|
|
||||||
};
|
|
||||||
if (navigator.canShare(data)) navigator.share(data);
|
|
||||||
else {
|
|
||||||
clipboard.writeText(data.url);
|
|
||||||
toast.success("Sent to clipboard!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Share size={16} />
|
<Share size={16} />
|
||||||
Share
|
Share
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
import type { ServerResponse } from "@/lib/types/mh-server";
|
||||||
import IconDisplay from "../icons/minecraft-icon-display";
|
import IconDisplay from "../icons/minecraft-icon-display";
|
||||||
import { ServerPageTags } from "./server-page-tags";
|
import { ServerPageTags } from "./server-page-tags";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
@ -8,84 +8,57 @@ import { ServerPageButtons } from "./server-page-buttons";
|
|||||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
||||||
|
|
||||||
export function ServerMainPage({
|
export function ServerMainPage({
|
||||||
server,
|
server,
|
||||||
mhsfData,
|
mhsfData,
|
||||||
onlineServer
|
|
||||||
}: {
|
}: {
|
||||||
server: ServerResponse;
|
server: ServerResponse;
|
||||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||||
onlineServer?: OnlineServer;
|
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mhsfData.server?.customizationData.colorMode !== null) {
|
if (mhsfData.server?.customizationData.banner !== undefined)
|
||||||
if (mhsfData.server?.customizationData.colorMode === "dark")
|
window.dispatchEvent(new Event("force-dark-mode"));
|
||||||
window.dispatchEvent(new Event("force-dark-mode"));
|
});
|
||||||
if (mhsfData.server?.customizationData.colorMode === "light")
|
|
||||||
window.dispatchEvent(new Event("force-light-mode"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"xl:px-[100px]",
|
"xl:px-[100px]",
|
||||||
mhsfData.server?.customizationData.banner === undefined
|
mhsfData.server?.customizationData.banner === undefined
|
||||||
? "pt-[150px]"
|
? "pt-[150px]"
|
||||||
: "pt-[300px]",
|
: "pt-[300px]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{mhsfData.server?.customizationData.banner && (
|
{mhsfData.server?.customizationData.banner && (
|
||||||
<img
|
<img
|
||||||
src={mhsfData.server?.customizationData.banner}
|
src={mhsfData.server?.customizationData.banner}
|
||||||
alt="User provided banner for server"
|
alt="User provided banner for server"
|
||||||
className="rounded align-middle block ml-auto mr-auto absolute left-0 z-0 w-full object-fill"
|
className="rounded align-middle block ml-auto mr-auto absolute left-0 z-0 w-full object-fill"
|
||||||
style={{
|
style={{
|
||||||
maskImage: "linear-gradient(to top, transparent, black)",
|
maskImage: "linear-gradient(to top, transparent, black)",
|
||||||
top: "0",
|
top: "0",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="flex items-center gap-2 w-full relative">
|
<span className="flex items-center gap-2 w-full relative">
|
||||||
<div className="bg-secondary p-4 rounded-lg lg:ml-4">
|
<div className="bg-secondary p-4 rounded-lg lg:ml-4">
|
||||||
<IconDisplay server={server} />
|
<IconDisplay server={server} />
|
||||||
</div>
|
</div>
|
||||||
<p className="w-full">
|
<p className="w-full">
|
||||||
<div className="lg:flex justify-between w-full">
|
<div className="lg:flex justify-between w-full">
|
||||||
<h1 className="text-2xl font-bold flex items-center gap-1">
|
<h1 className="text-2xl font-bold">{server.name}</h1>
|
||||||
{server.name}
|
<span>
|
||||||
{onlineServer !== undefined && onlineServer?.author && (
|
<ServerPageButtons server={server} mhsfData={mhsfData} />
|
||||||
<span className="text-muted-foreground flex items-center gap-2">
|
</span>
|
||||||
by{" "}
|
</div>
|
||||||
{mhsfData.server?.customizationData.userProfilePicture && (
|
<span className="flex items-center gap-2 flex-wrap">
|
||||||
<Avatar className="h-[16px] w-[16px]">
|
<ServerPageTags server={server} className="mt-1" />
|
||||||
<AvatarImage
|
</span>
|
||||||
src={
|
</p>
|
||||||
mhsfData.server?.customizationData
|
</span>
|
||||||
.userProfilePicture ?? ""
|
<Separator className="my-6" />
|
||||||
}
|
<ServerRows server={server} mhsfData={mhsfData} />
|
||||||
alt="Server Owner Image"
|
</div>
|
||||||
/>
|
);
|
||||||
<AvatarFallback>{onlineServer?.author[0]}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
)}
|
|
||||||
{onlineServer?.author}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</h1>
|
|
||||||
<span>
|
|
||||||
<ServerPageButtons server={server} mhsfData={mhsfData} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="flex items-center gap-2 flex-wrap">
|
|
||||||
<ServerPageTags server={server} className="mt-1" />
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
<Separator className="my-6" />
|
|
||||||
<ServerRows server={server} mhsfData={mhsfData} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { ReportingProvider } from "./reporting/reporting-provider";
|
|||||||
import { ServerEditorProvider } from "./server-editor/server-editor-provider";
|
import { ServerEditorProvider } from "./server-editor/server-editor-provider";
|
||||||
|
|
||||||
export function ServerProvider({ serverId }: { serverId: string }) {
|
export function ServerProvider({ serverId }: { serverId: string }) {
|
||||||
const { server, error, loading, onlineServer } = useServer({ id: serverId });
|
const { server, error, loading } = useServer({ id: serverId });
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
const mhsf = useMHSFServer(serverId);
|
const mhsf = useMHSFServer(serverId);
|
||||||
|
|
||||||
@ -77,7 +77,6 @@ export function ServerProvider({ serverId }: { serverId: string }) {
|
|||||||
<ServerMainPage
|
<ServerMainPage
|
||||||
server={server as ServerResponse}
|
server={server as ServerResponse}
|
||||||
mhsfData={mhsf}
|
mhsfData={mhsf}
|
||||||
onlineServer={onlineServer ?? undefined}
|
|
||||||
/>
|
/>
|
||||||
</ReportingProvider>
|
</ReportingProvider>
|
||||||
</ServerEditorProvider>
|
</ServerEditorProvider>
|
||||||
|
|||||||
@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const AvatarImage = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Image
|
|
||||||
ref={ref}
|
|
||||||
className={cn("aspect-square h-full w-full", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
||||||
|
|
||||||
const AvatarFallback = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AvatarPrimitive.Fallback
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
|
||||||
@ -38,30 +38,24 @@ import { usePathname } from "next/navigation";
|
|||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
const [mounted, setMounted] = React.useState(false);
|
const [mounted, setMounted] = React.useState(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [forcedTheme, setForcedTheme] = React.useState<'dark' | 'light' | undefined>();
|
const [forcedDark, setForcedDark] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
|
||||||
window.addEventListener("force-dark-mode", () => {
|
window.addEventListener("force-dark-mode", () => {
|
||||||
setForcedTheme('dark');
|
setForcedDark(true);
|
||||||
});
|
|
||||||
window.addEventListener("force-light-mode", () => {
|
|
||||||
setForcedTheme('light');
|
|
||||||
});
|
|
||||||
window.addEventListener("force-no-mode", () => {
|
|
||||||
setForcedTheme(undefined);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setForcedTheme(undefined);
|
setForcedDark(false);
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextThemeProvider forcedTheme={forcedTheme} {...props}>
|
<NextThemeProvider forcedTheme={forcedDark ? "dark" : undefined} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</NextThemeProvider>
|
</NextThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { findServerData } from "@/pages/api/v1/server/get/[server]";
|
|
||||||
import { MongoClient, type WithId } from "mongodb";
|
|
||||||
import { ActualCustomization } from "./types/data";
|
|
||||||
|
|
||||||
type ServerMetadata = {
|
|
||||||
ownedServer: WithId<{
|
|
||||||
/** Legacy */
|
|
||||||
server?: string;
|
|
||||||
/** @deprecated Use `serverId` for better identification */
|
|
||||||
serverId?: string;
|
|
||||||
userId: string;
|
|
||||||
}>;
|
|
||||||
customizedServer: WithId<
|
|
||||||
ActualCustomization & {
|
|
||||||
serverId?: string;
|
|
||||||
/** @deprecated Use `serverId` for better identification */
|
|
||||||
server?: string;
|
|
||||||
}
|
|
||||||
> | null;
|
|
||||||
};
|
|
||||||
type ServerMetadataFunctional = ServerMetadata & {
|
|
||||||
changeServer: (
|
|
||||||
changes: Partial<
|
|
||||||
ActualCustomization & {
|
|
||||||
serverId?: string;
|
|
||||||
/** @deprecated Use `serverId` for better identification */
|
|
||||||
server?: string;
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
) => Promise<void> | void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function checkOwnedServerMetadata(
|
|
||||||
userId: string | null,
|
|
||||||
mongoClient: MongoClient,
|
|
||||||
serverSelector: { id: string; name?: string },
|
|
||||||
): Promise<ServerMetadataFunctional> {
|
|
||||||
// Step 1: Check auth
|
|
||||||
if (!userId) throw new Error("Unauthorized");
|
|
||||||
mongoClient.connect();
|
|
||||||
|
|
||||||
// Step 2: Check server
|
|
||||||
const serverData = await findServerData(serverSelector.id as string);
|
|
||||||
|
|
||||||
if (!serverData.exists) throw new Error("Server doesn't exist");
|
|
||||||
|
|
||||||
const db = mongoClient.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
|
||||||
const ownedServer = (await db.collection("owned-servers").findOne({
|
|
||||||
$or: [
|
|
||||||
{ serverId: serverSelector.id },
|
|
||||||
{ server: serverSelector.name ?? serverData.name },
|
|
||||||
],
|
|
||||||
})) as WithId<{
|
|
||||||
server?: string;
|
|
||||||
serverId?: string;
|
|
||||||
userId: string;
|
|
||||||
author: string;
|
|
||||||
}>;
|
|
||||||
if (!ownedServer) throw new Error("Server not linked");
|
|
||||||
if (ownedServer.author !== userId)
|
|
||||||
throw new Error("You don't own this server.");
|
|
||||||
|
|
||||||
const customizedServer = (await db.collection("customization").findOne({
|
|
||||||
$or: [
|
|
||||||
{ serverId: serverSelector.id },
|
|
||||||
{ server: serverSelector.name ?? serverData.name },
|
|
||||||
],
|
|
||||||
})) as WithId<
|
|
||||||
ActualCustomization & { serverId?: string; server?: string }
|
|
||||||
> | null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
ownedServer,
|
|
||||||
customizedServer,
|
|
||||||
changeServer: async (changes) => {
|
|
||||||
await db.collection("customization").updateOne(
|
|
||||||
{
|
|
||||||
$or: [
|
|
||||||
{ serverId: serverSelector.id },
|
|
||||||
{ server: serverSelector.name ?? serverData.name },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ $set: changes },
|
|
||||||
{ upsert: true },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
import { useCallback, useState } from 'react'
|
|
||||||
import * as jsxRuntime from 'react/jsx-runtime'
|
|
||||||
import rehypeReact, { type Options as RehypeReactOptions } from 'rehype-react'
|
|
||||||
import remarkParse, { type Options as RemarkParseOptions } from 'remark-parse'
|
|
||||||
import remarkRehype, { type Options as RemarkRehypeOptions } from 'remark-rehype'
|
|
||||||
import { unified, type Plugin, type PluggableList, type Processor } from 'unified'
|
|
||||||
import type { Root } from 'mdast'
|
|
||||||
|
|
||||||
export interface UseRemarkOptions {
|
|
||||||
remarkParseOptions?: RemarkParseOptions
|
|
||||||
remarkPlugins?: PluggableList
|
|
||||||
remarkRehypeOptions?: RemarkRehypeOptions
|
|
||||||
rehypePlugins?: PluggableList
|
|
||||||
rehypeReactOptions?: Pick<RehypeReactOptions, 'components'>
|
|
||||||
onError?: (err: Error) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useRemark({
|
|
||||||
remarkParseOptions,
|
|
||||||
remarkPlugins = [],
|
|
||||||
remarkRehypeOptions,
|
|
||||||
rehypePlugins = [],
|
|
||||||
rehypeReactOptions,
|
|
||||||
onError = () => { },
|
|
||||||
}: UseRemarkOptions = {}): [React.ReactElement | null, (source: string) => void] {
|
|
||||||
const [content, setContent] = useState<React.ReactElement | null>(null)
|
|
||||||
|
|
||||||
const setMarkdown = useCallback((source: string) => {
|
|
||||||
const processor = unified()
|
|
||||||
.use(remarkParse, remarkParseOptions)
|
|
||||||
.use(remarkPlugins as Plugin[])
|
|
||||||
.use(remarkRehype as Plugin, remarkRehypeOptions)
|
|
||||||
.use(rehypePlugins as Plugin[])
|
|
||||||
.use(rehypeReact, {
|
|
||||||
...rehypeReactOptions,
|
|
||||||
Fragment: jsxRuntime.Fragment,
|
|
||||||
jsx: jsxRuntime.jsx,
|
|
||||||
jsxs: jsxRuntime.jsxs,
|
|
||||||
} satisfies RehypeReactOptions) as unknown as Processor<Root, Root, React.ReactElement>
|
|
||||||
|
|
||||||
processor
|
|
||||||
.process(source)
|
|
||||||
.then(vfile => setContent(vfile.result as React.ReactElement))
|
|
||||||
.catch(onError)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return [content, setMarkdown]
|
|
||||||
}
|
|
||||||
@ -6,7 +6,6 @@ export function useServer(serverSpecifier: { id?: string; name?: string }) {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [server, setServer] = useState<ServerResponse | null>(null);
|
const [server, setServer] = useState<ServerResponse | null>(null);
|
||||||
const [onlineServer, setOnlineServer] = useState<OnlineServer | null>(null);
|
|
||||||
|
|
||||||
useEffectOnce(() => {
|
useEffectOnce(() => {
|
||||||
try {
|
try {
|
||||||
@ -17,15 +16,6 @@ export function useServer(serverSpecifier: { id?: string; name?: string }) {
|
|||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (json.server === null) throw new Error("Server not found");
|
if (json.server === null) throw new Error("Server not found");
|
||||||
|
|
||||||
if (server?.online) {
|
|
||||||
const res = await fetch("https://api.minehut.com/servers");
|
|
||||||
const json = await res.json() as {servers: OnlineServer[]};
|
|
||||||
const onlineServerData = json.servers.find((s) => s.staticInfo._id === server._id) ?? null;
|
|
||||||
if (onlineServerData) {
|
|
||||||
setOnlineServer(onlineServerData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setServer(json.server);
|
setServer(json.server);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})();
|
})();
|
||||||
@ -36,5 +26,5 @@ export function useServer(serverSpecifier: { id?: string; name?: string }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { loading, error, server, onlineServer };
|
return { loading, error, server };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,43 +30,27 @@
|
|||||||
|
|
||||||
import { Achievement } from "./achievement";
|
import { Achievement } from "./achievement";
|
||||||
|
|
||||||
export type ActualCustomization = {
|
|
||||||
description: string | undefined;
|
|
||||||
discord: string | undefined;
|
|
||||||
/** @version 1 @deprecated Use `colorMode` instead */
|
|
||||||
colorScheme: string | undefined;
|
|
||||||
/** @version 2 */
|
|
||||||
colorMode: "dark" | "light" | null;
|
|
||||||
customizationVersion: number | undefined;
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
/** @note Using non-`ufs.io` domains is deprecated */
|
|
||||||
banner: string;
|
|
||||||
_deletionId: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
banner: undefined;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type MHSFData = {
|
export type MHSFData = {
|
||||||
favoriteData: {
|
favoriteData: {
|
||||||
favoritedByAccount: boolean | null;
|
favoritedByAccount: boolean | null;
|
||||||
favoriteNumber: number;
|
favoriteNumber: number;
|
||||||
favoriteHistoricalData: { date: string; favorites: number }[];
|
favoriteHistoricalData: { date: string; favorites: number }[];
|
||||||
};
|
};
|
||||||
customizationData: {
|
customizationData: {
|
||||||
userProfilePicture: string | undefined;
|
description: string | undefined;
|
||||||
/** If undefined then this is 1. */
|
banner: string | undefined;
|
||||||
isOwned: boolean;
|
discord: string | undefined;
|
||||||
isOwnedByUser: boolean;
|
colorScheme: string | undefined;
|
||||||
} & ActualCustomization;
|
userProfilePicture: string | undefined;
|
||||||
playerData: {
|
isOwned: boolean;
|
||||||
historically: { date: string; playerCount: number }[];
|
isOwnedByUser: boolean;
|
||||||
max: number;
|
};
|
||||||
};
|
playerData: {
|
||||||
achievements: {
|
historically: { date: string; playerCount: number }[];
|
||||||
historically: { _id: string; name: string; achievements: Achievement[] }[];
|
max: number;
|
||||||
currently: Achievement[];
|
};
|
||||||
};
|
achievements: {
|
||||||
|
historically: { _id: string; name: string; achievements: Achievement[] }[];
|
||||||
|
currently: Achievement[];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
141
apps/www/src/pages/api/v1/server/get/[server]/customize.ts
Normal file
141
apps/www/src/pages/api/v1/server/get/[server]/customize.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* 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 { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { getAuth } from "@clerk/nextjs/server";
|
||||||
|
import { Document, MongoClient, WithId } from "mongodb";
|
||||||
|
import { waitUntil } from "@vercel/functions";
|
||||||
|
|
||||||
|
const validColors = [
|
||||||
|
"zinc",
|
||||||
|
"slate",
|
||||||
|
"stone",
|
||||||
|
"gray",
|
||||||
|
"neutral",
|
||||||
|
"red",
|
||||||
|
"rose",
|
||||||
|
"orange",
|
||||||
|
"green",
|
||||||
|
"blue",
|
||||||
|
"yellow",
|
||||||
|
"violet",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
const { userId } = getAuth(req);
|
||||||
|
const server = req.query.server as string;
|
||||||
|
|
||||||
|
const { customization }: { customization: any } = req.body;
|
||||||
|
if (
|
||||||
|
customization.description != undefined &&
|
||||||
|
(!(Array.from(customization.description).length < 1250) ||
|
||||||
|
!(Array.from(customization.description).length > 2))
|
||||||
|
)
|
||||||
|
return res.status(400).send({ message: "Description is incorrect length" });
|
||||||
|
|
||||||
|
if (
|
||||||
|
customization.discord != undefined &&
|
||||||
|
!/^\d*\.?\d*$/.test(customization.discord)
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.send({ message: "Discord server has invalid chars" });
|
||||||
|
|
||||||
|
if (
|
||||||
|
customization.colorScheme != undefined &&
|
||||||
|
!validColors.includes(customization.colorScheme)
|
||||||
|
)
|
||||||
|
return res.status(400).send({ message: "Color doesn't exist" });
|
||||||
|
|
||||||
|
if (customization == null) {
|
||||||
|
res.status(400).send({ message: "Couldn't find data" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 collection = db.collection("owned-servers");
|
||||||
|
const customizationColl = db.collection("customization");
|
||||||
|
if (!((await collection.findOne({ server, author: userId })) == undefined)) {
|
||||||
|
const alreadyExists =
|
||||||
|
(await customizationColl.findOne({ server })) != undefined;
|
||||||
|
|
||||||
|
if (!alreadyExists) {
|
||||||
|
await customizationColl.insertOne({
|
||||||
|
server,
|
||||||
|
colorScheme: customization.colorScheme,
|
||||||
|
description: customization.description,
|
||||||
|
banner: customization.banner,
|
||||||
|
discord: customization.discord,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const find = (await customizationColl.findOne({
|
||||||
|
server,
|
||||||
|
})) as WithId<Document>;
|
||||||
|
if (customization.colorScheme != undefined)
|
||||||
|
await customizationColl.findOneAndUpdate(
|
||||||
|
{ server },
|
||||||
|
{ $set: { colorScheme: customization.colorScheme } },
|
||||||
|
);
|
||||||
|
if (customization.description != undefined)
|
||||||
|
await customizationColl.findOneAndUpdate(
|
||||||
|
{ server },
|
||||||
|
{ $set: { description: customization.description } },
|
||||||
|
);
|
||||||
|
if (customization.banner != undefined)
|
||||||
|
await customizationColl.findOneAndUpdate(
|
||||||
|
{ server },
|
||||||
|
{ $set: { banner: customization.banner } },
|
||||||
|
);
|
||||||
|
if (customization.discord != undefined)
|
||||||
|
await customizationColl.findOneAndUpdate(
|
||||||
|
{ server },
|
||||||
|
{ $set: { discord: customization.discord } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Close the database, but don't close this
|
||||||
|
// serverless instance until it happens
|
||||||
|
waitUntil(client.close());
|
||||||
|
|
||||||
|
res.send({ message: "Done!" });
|
||||||
|
} else {
|
||||||
|
// Close the database, but don't close this
|
||||||
|
// serverless instance until it happens
|
||||||
|
waitUntil(client.close());
|
||||||
|
res.status(400).send({ message: "You don't own this server." });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -166,38 +166,41 @@ async function findCustomizationData(
|
|||||||
: null,
|
: null,
|
||||||
]);
|
]);
|
||||||
let user: User | undefined = undefined;
|
let user: User | undefined = undefined;
|
||||||
if (ownedServerData) {
|
try {
|
||||||
|
user = await clerk.users.getUser(ownedServerData?.author);
|
||||||
try {
|
} catch (e) {
|
||||||
user = await clerk.users.getUser(ownedServerData?.author);
|
console.warn(e);
|
||||||
} catch (e) {
|
if (customizationData || ownedServerData) {
|
||||||
console.warn(e);
|
|
||||||
if (customizationData || ownedServerData) {
|
|
||||||
return {
|
|
||||||
...(customizationData as any),
|
|
||||||
isOwned: true,
|
|
||||||
isOwnedByUser: ownedServerData?.author === userId,
|
|
||||||
userProfilePicture: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
isOwned: false,
|
...(customizationData as any),
|
||||||
isOwnedByUser: false,
|
isOwned: true,
|
||||||
description: undefined,
|
isOwnedByUser: ownedServerData?.author === userId,
|
||||||
banner: undefined,
|
userProfilePicture: null,
|
||||||
discord: undefined,
|
|
||||||
colorScheme: undefined,
|
|
||||||
userProfilePicture: undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
isOwned: false,
|
||||||
|
isOwnedByUser: false,
|
||||||
|
description: undefined,
|
||||||
|
banner: undefined,
|
||||||
|
discord: undefined,
|
||||||
|
colorScheme: undefined,
|
||||||
|
userProfilePicture: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
ownedServerData?.author === userId,
|
||||||
|
userId,
|
||||||
|
ownedServerData?.author,
|
||||||
|
);
|
||||||
|
|
||||||
if (customizationData || ownedServerData) {
|
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 ? user?.imageUrl : "no user",
|
userProfilePicture: userId ? user.imageUrl : "no user",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { checkOwnedServerMetadata } from "@/lib/check-owned-server";
|
|
||||||
import { getAuth } from "@clerk/nextjs/server";
|
|
||||||
import { MongoClient } from "mongodb";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { server: serverId, colorMode } = req.query;
|
|
||||||
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
|
||||||
|
|
||||||
const { changeServer } =
|
|
||||||
await checkOwnedServerMetadata(getAuth(req).userId ?? null, mongo, {
|
|
||||||
id: serverId as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (colorMode !== "light" && colorMode !== "dark" && colorMode !== undefined ) {
|
|
||||||
return res.status(400).send({ error: "Invalid value" })
|
|
||||||
}
|
|
||||||
|
|
||||||
await changeServer({
|
|
||||||
colorMode,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).send({ error: error });
|
|
||||||
}
|
|
||||||
return res.send({ message: "Success" });
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { checkOwnedServerMetadata } from "@/lib/check-owned-server";
|
|
||||||
import { getAuth } from "@clerk/nextjs/server";
|
|
||||||
import { MongoClient } from "mongodb";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { server: serverId, description } = req.query;
|
|
||||||
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
|
||||||
if (!description)
|
|
||||||
return res.status(400).send({ error: "No description provided" });
|
|
||||||
|
|
||||||
const { changeServer } =
|
|
||||||
await checkOwnedServerMetadata(getAuth(req).userId ?? null, mongo, {
|
|
||||||
id: serverId as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
await changeServer({
|
|
||||||
description: atob(decodeURIComponent(description as string)),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).send({ error: error });
|
|
||||||
}
|
|
||||||
return res.send({ message: "Success" });
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { checkOwnedServerMetadata } from "@/lib/check-owned-server";
|
|
||||||
import { getAuth } from "@clerk/nextjs/server";
|
|
||||||
import { MongoClient } from "mongodb";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { UTApi } from "uploadthing/server";
|
|
||||||
import type { ActualCustomization } from "@/lib/types/data";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { server: serverId } = req.query;
|
|
||||||
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
|
||||||
|
|
||||||
const { ownedServer, customizedServer, changeServer } =
|
|
||||||
await checkOwnedServerMetadata(getAuth(req).userId ?? null, mongo, {
|
|
||||||
id: serverId as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (customizedServer?.customizationVersion !== 2) {
|
|
||||||
if ((!customizedServer?.banner?.startsWith("https://exh89c9lva.ufs.sh")) && customizedServer?.banner) {
|
|
||||||
const utapi = new UTApi();
|
|
||||||
console.log(
|
|
||||||
`https://wsrv.nl/?url=${encodeURIComponent(customizedServer?.banner as string)}`)
|
|
||||||
const newBanner = await utapi.uploadFilesFromUrl(
|
|
||||||
`https://wsrv.nl/?url=${encodeURIComponent(customizedServer?.banner as string)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newBanner.error)
|
|
||||||
return res.status(400).send({ error: newBanner.error });
|
|
||||||
|
|
||||||
await changeServer({ banner: newBanner.data.ufsUrl });
|
|
||||||
}
|
|
||||||
await changeServer({
|
|
||||||
colorMode: "light",
|
|
||||||
customizationVersion: 2,
|
|
||||||
serverId: serverId as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.send({ message: "Successfully migrated from svc1 to svc2." });
|
|
||||||
}
|
|
||||||
return res.send({ message: "Already migrated." });
|
|
||||||
} catch (e) {
|
|
||||||
res.status(400).send({ error: e });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -29,13 +29,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getAuth } from "@clerk/nextjs/server";
|
import { getAuth } from "@clerk/nextjs/server";
|
||||||
import { createRouteHandler, createUploadthing } from "uploadthing/next-legacy";
|
import {
|
||||||
import { UploadThingError, UTApi } from "uploadthing/server";
|
createRouteHandler,
|
||||||
|
createUploadthing,
|
||||||
|
type FileRouter,
|
||||||
|
} from "uploadthing/next-legacy";
|
||||||
|
import { UploadThingError } from "uploadthing/server";
|
||||||
import { findServerData } from "..";
|
import { findServerData } from "..";
|
||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from "mongodb";
|
||||||
import type { FileRoute } from "uploadthing/types";
|
import type { FileRoute } from "uploadthing/types";
|
||||||
import type { Json } from "@uploadthing/shared";
|
import type { Json } from "@uploadthing/shared";
|
||||||
import { checkOwnedServerMetadata } from "@/lib/check-owned-server";
|
|
||||||
|
|
||||||
const f = createUploadthing();
|
const f = createUploadthing();
|
||||||
|
|
||||||
@ -49,96 +52,74 @@ export default createRouteHandler({
|
|||||||
})
|
})
|
||||||
// Set permissions and file types for this FileRoute
|
// Set permissions and file types for this FileRoute
|
||||||
.middleware(async ({ req, res }) => {
|
.middleware(async ({ req, res }) => {
|
||||||
// Step 1: Check authentication
|
// Step 1: Check authentication
|
||||||
const { userId } = getAuth(req);
|
const { userId } = getAuth(req);
|
||||||
|
|
||||||
if (!userId) throw new UploadThingError("Unauthorized");
|
if (!userId) throw new UploadThingError("Unauthorized");
|
||||||
|
|
||||||
// Step 2: Check server
|
// Step 2: Check server
|
||||||
const { server } = req.query;
|
const { server } = req.query;
|
||||||
const serverData = await findServerData(server as string);
|
const serverData = await findServerData(server as string);
|
||||||
const mongoClient = new MongoClient(process.env.MONGO_DB as string);
|
const mongoClient = new MongoClient(process.env.MONGO_DB as string);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!serverData.exists)
|
if (!serverData.exists) throw new UploadThingError("Server doesn't exist");
|
||||||
throw new UploadThingError("Server doesn't exist");
|
await mongoClient.connect();
|
||||||
await mongoClient.connect();
|
|
||||||
|
|
||||||
const db = mongoClient.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
const db = mongoClient.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||||
const ownedServer = await db
|
const ownedServer = await db.collection("owned-servers").findOne({ $or: [{ serverId: server }, { server: serverData.name }] });
|
||||||
.collection("owned-servers")
|
if (!ownedServer) throw new UploadThingError("Server not linked");
|
||||||
.findOne({
|
if (ownedServer.author !== userId) throw new UploadThingError("You don't own this server.");
|
||||||
$or: [{ serverId: server }, { server: serverData.name }],
|
|
||||||
});
|
|
||||||
if (!ownedServer) throw new UploadThingError("Server not linked");
|
|
||||||
if (ownedServer.author !== userId)
|
|
||||||
throw new UploadThingError("You don't own this server.");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId,
|
userId,
|
||||||
ownedServer,
|
ownedServer,
|
||||||
serverId: server,
|
serverId: server,
|
||||||
serverName: serverData.name,
|
serverName: serverData.name
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
await mongoClient.close();
|
await mongoClient.close();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onUploadComplete(async ({ metadata, file }) => {
|
.onUploadComplete(async ({ metadata, file }) => {
|
||||||
// This code RUNS ON YOUR SERVER after upload
|
// This code RUNS ON YOUR SERVER after upload
|
||||||
console.log("Upload complete for userId:", metadata.userId);
|
console.log("Upload complete for userId:", metadata.userId);
|
||||||
console.log("file url", file.ufsUrl);
|
console.log("file url", file.ufsUrl);
|
||||||
console.log("metadata:", metadata);
|
console.log("metadata:", metadata);
|
||||||
const utapi = new UTApi();
|
|
||||||
|
|
||||||
// Step 3: Update the server's customization data with the new banner URL
|
// Update the server's customization data with the new banner URL
|
||||||
const mongoClient = new MongoClient(process.env.MONGO_DB as string);
|
const mongoClient = new MongoClient(process.env.MONGO_DB as string);
|
||||||
try {
|
try {
|
||||||
const { customizedServer } =
|
await mongoClient.connect();
|
||||||
await checkOwnedServerMetadata(metadata.userId, mongoClient, {
|
const db = mongoClient.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||||
id: metadata.serverId as string,
|
|
||||||
});
|
|
||||||
const db = mongoClient.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
|
||||||
|
|
||||||
// Step 3.5: Delete old banner if needed
|
// Update or insert the customization data
|
||||||
if (customizedServer?.banner) {
|
const result = await db.collection("customization").updateOne(
|
||||||
await utapi.deleteFiles(customizedServer._deletionId);
|
{ $or: [{ serverId: metadata.serverId }, { server: metadata.serverName }] },
|
||||||
}
|
{ $set: { banner: file.ufsUrl } },
|
||||||
|
{ upsert: true }
|
||||||
|
);
|
||||||
|
|
||||||
// Step 4: Update or insert the customization data
|
console.log("Database update result:", result);
|
||||||
const result = await db
|
|
||||||
.collection("customization")
|
|
||||||
.updateOne(
|
|
||||||
{
|
|
||||||
$or: [
|
|
||||||
{ serverId: metadata.serverId },
|
|
||||||
{ server: metadata.serverName },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ $set: { banner: file.ufsUrl, customizationVersion: 2, _deletionId: file.key } },
|
|
||||||
{ upsert: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Database update result:", result);
|
// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
|
||||||
|
return { uploadedBy: metadata.userId };
|
||||||
// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
|
} catch (error) {
|
||||||
return { uploadedBy: metadata.userId };
|
console.error("Error updating database:", error);
|
||||||
} catch (error) {
|
throw error;
|
||||||
console.error("Error updating database:", error);
|
} finally {
|
||||||
throw error;
|
await mongoClient.close();
|
||||||
} finally {
|
}
|
||||||
await mongoClient.close();
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BannerUploaderRouter = {
|
export type BannerUploaderRouter = {
|
||||||
imageUploader: FileRoute<{
|
imageUploader: FileRoute<{
|
||||||
input: undefined;
|
input: undefined;
|
||||||
output: {
|
output: {
|
||||||
uploadedBy: string;
|
uploadedBy: string;
|
||||||
};
|
};
|
||||||
errorShape: Json;
|
errorShape: Json;
|
||||||
}>;
|
}>;
|
||||||
};
|
}
|
||||||
Loading…
Reference in New Issue
Block a user