mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-07 18:44:59 -05:00
feat: enforcing color modes
This commit is contained in:
parent
20afc2de8e
commit
2a58192a14
@ -45,7 +45,12 @@ const nextConfig = {
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "cdn.discordapp.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "exh89c9lva.ufs.sh",
|
||||
pathname: "/f/*",
|
||||
},
|
||||
],
|
||||
},
|
||||
async redirects() {
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
"@radix-ui/react-switch": "1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@shikijs/rehype": "^3.4.0",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@trpc/client": "^11.0.0",
|
||||
"@trpc/next": "^11.0.0",
|
||||
@ -93,10 +94,15 @@
|
||||
"react-fade-in": "^2.0.1",
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"react-shiki": "^0.6.0",
|
||||
"react-snowfall": "^2.2.0",
|
||||
"recharts": "^2.15.1",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rehype-react": "^8.0.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"request-ip": "^3.3.0",
|
||||
"sonner": "^1.7.0",
|
||||
@ -155,7 +161,6 @@
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.0.23",
|
||||
"recharts": "^2.15.1",
|
||||
"shiki": "^1.23.0",
|
||||
|
||||
@ -37,15 +37,26 @@ import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Material } from "@/components/ui/material";
|
||||
import { useState } from "react";
|
||||
import { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import ShikiHighlighter, {
|
||||
type Element,
|
||||
isInlineCode,
|
||||
rehypeInlineCodeProperty,
|
||||
} from "react-shiki";
|
||||
|
||||
export function MOTDRow({
|
||||
server,
|
||||
mhsfData,
|
||||
}: { server: ServerResponse; mhsfData: ReturnType<typeof useMHSFServer> }) {
|
||||
const clipboard = useClipboard();
|
||||
const [tab, setTab] = useState(mhsfData.server?.customizationData.description !== undefined ? "description" : "motd");
|
||||
const [tab, setTab] = useState(
|
||||
mhsfData.server?.customizationData.description !== undefined
|
||||
? "description"
|
||||
: "motd",
|
||||
);
|
||||
|
||||
return (
|
||||
<Material className="p-4 relative h-[250px]">
|
||||
@ -110,9 +121,37 @@ export function MOTDRow({
|
||||
)}
|
||||
{tab === "description" && (
|
||||
<div className="prose mt-2 break-words overflow-y-auto max-h-[175px] min-w-full dark:prose-invert">
|
||||
<Markdown className="min-w-full">{mhsfData.server?.customizationData.description}</Markdown>
|
||||
<Markdown
|
||||
components={{
|
||||
code: CodeHighlight,
|
||||
}}
|
||||
>
|
||||
{mhsfData.server?.customizationData.description}
|
||||
</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</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} theme="poimandres" {...props}>
|
||||
{String(children)}
|
||||
</ShikiHighlighter>
|
||||
);
|
||||
};
|
||||
|
||||
74
apps/www/src/components/feat/server-page/server-editor/customizations/server-banner-box.tsx
Normal file
74
apps/www/src/components/feat/server-page/server-editor/customizations/server-banner-box.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
94
apps/www/src/components/feat/server-page/server-editor/customizations/server-color-mode-box.tsx
Normal file
94
apps/www/src/components/feat/server-page/server-editor/customizations/server-color-mode-box.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
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 !== undefined);
|
||||
setColorMode(serverData.server?.customizationData.colorMode ?? "light");
|
||||
}, [serverData])
|
||||
|
||||
useEffect(() => {
|
||||
if (colorMode === "idc") {
|
||||
setColorModeEnabled(false);
|
||||
setColorMode("light");
|
||||
}
|
||||
if (colorMode === "dark")
|
||||
window.dispatchEvent(new Event("force-dark-mode"));
|
||||
|
||||
if (colorMode === "light")
|
||||
window.dispatchEvent(new Event("force-light-mode"));
|
||||
}, [colorMode]);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
78
apps/www/src/components/feat/server-page/server-editor/customizations/server-description-box.tsx
Normal file
78
apps/www/src/components/feat/server-page/server-editor/customizations/server-description-box.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
69
apps/www/src/components/feat/server-page/server-editor/customizations/server-migration-box.tsx
Normal file
69
apps/www/src/components/feat/server-page/server-editor/customizations/server-migration-box.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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,11 +20,15 @@ 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";
|
||||
import { BannerUploaderRouter } from "@/pages/api/v1/server/get/[server]/settings/upload-banner";
|
||||
import type { BannerUploaderRouter } from "@/pages/api/v1/server/get/[server]/settings/upload-banner";
|
||||
import {
|
||||
generateUploadButton,
|
||||
generateUploadDropzone,
|
||||
} 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 =
|
||||
"bg-green-200 border-green-400 dark:bg-green-800 dark:border-green-600";
|
||||
@ -228,49 +232,32 @@ export function ServerEditorProvider({
|
||||
<>
|
||||
{serverData.server?.customizationData.isOwnedByUser ? (
|
||||
<div className="!max-h-[700px] !h-[700px] overflow-y-scroll">
|
||||
{serverData.server?.customizationData
|
||||
.customizationVersion === 2 ? (
|
||||
<div>
|
||||
<DrawerTitle className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
||||
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 ??
|
||||
`# ${minehutData.name}`
|
||||
}
|
||||
onUpdate={(content) => console.log(content)}
|
||||
<ServerDescriptionBox
|
||||
serverData={serverData}
|
||||
minehutData={minehutData}
|
||||
/>
|
||||
<ServerBannerBox
|
||||
serverData={serverData}
|
||||
minehutData={minehutData}
|
||||
/>
|
||||
<ServerColorModeBox
|
||||
serverData={serverData}
|
||||
minehutData={minehutData}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ServerMigrationBox
|
||||
oldVersion={1}
|
||||
reupdate={() => serverData.refresh()}
|
||||
id={minehutData._id}
|
||||
/>
|
||||
)}
|
||||
</Material>
|
||||
<Material className="grid gap-1 mt-2 max-h-[700px]">
|
||||
<strong>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.
|
||||
</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>
|
||||
</div>
|
||||
) : (
|
||||
<Placeholder
|
||||
|
||||
@ -17,8 +17,12 @@ export function ServerMainPage({
|
||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (mhsfData.server?.customizationData.banner !== undefined)
|
||||
if (mhsfData.server?.customizationData.colorMode !== undefined) {
|
||||
if (mhsfData.server?.customizationData.colorMode === "dark")
|
||||
window.dispatchEvent(new Event("force-dark-mode"));
|
||||
if (mhsfData.server?.customizationData.colorMode === "light")
|
||||
window.dispatchEvent(new Event("force-light-mode"));
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@ -38,24 +38,27 @@ import { usePathname } from "next/navigation";
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
const pathname = usePathname();
|
||||
const [forcedDark, setForcedDark] = React.useState(false);
|
||||
const [forcedTheme, setForcedTheme] = React.useState<'dark' | 'light' | undefined>();
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
window.addEventListener("force-dark-mode", () => {
|
||||
setForcedDark(true);
|
||||
setForcedTheme('dark');
|
||||
});
|
||||
window.addEventListener("force-light-mode", () => {
|
||||
setForcedTheme('light');
|
||||
});
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
setForcedDark(false);
|
||||
setForcedTheme(undefined);
|
||||
}, [pathname]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<NextThemeProvider forcedTheme={forcedDark ? "dark" : undefined} {...props}>
|
||||
<NextThemeProvider forcedTheme={forcedTheme} {...props}>
|
||||
{children}
|
||||
</NextThemeProvider>
|
||||
);
|
||||
|
||||
118
apps/www/src/lib/check-owned-server.ts
Normal file
118
apps/www/src/lib/check-owned-server.ts
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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 },
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
77
apps/www/src/lib/hooks/use-remark.tsx
Normal file
77
apps/www/src/lib/hooks/use-remark.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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]
|
||||
}
|
||||
@ -30,6 +30,25 @@
|
||||
|
||||
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" | undefined;
|
||||
customizationVersion: number | undefined;
|
||||
} & (
|
||||
| {
|
||||
/** @note Using non-`ufs.io` domains is deprecated */
|
||||
banner: string;
|
||||
_deletionId: string;
|
||||
}
|
||||
| {
|
||||
banner: undefined;
|
||||
}
|
||||
);
|
||||
|
||||
export type MHSFData = {
|
||||
favoriteData: {
|
||||
favoritedByAccount: boolean | null;
|
||||
@ -37,14 +56,11 @@ export type MHSFData = {
|
||||
favoriteHistoricalData: { date: string; favorites: number }[];
|
||||
};
|
||||
customizationData: {
|
||||
description: string | undefined;
|
||||
banner: string | undefined;
|
||||
discord: string | undefined;
|
||||
colorScheme: string | undefined;
|
||||
userProfilePicture: string | undefined;
|
||||
/** If undefined then this is 1. */
|
||||
isOwned: boolean;
|
||||
isOwnedByUser: boolean;
|
||||
};
|
||||
} & ActualCustomization;
|
||||
playerData: {
|
||||
historically: { date: string; playerCount: number }[];
|
||||
max: number;
|
||||
|
||||
@ -1,141 +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 { 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,6 +166,8 @@ async function findCustomizationData(
|
||||
: null,
|
||||
]);
|
||||
let user: User | undefined = undefined;
|
||||
if (ownedServerData) {
|
||||
|
||||
try {
|
||||
user = await clerk.users.getUser(ownedServerData?.author);
|
||||
} catch (e) {
|
||||
@ -188,19 +190,14 @@ async function findCustomizationData(
|
||||
userProfilePicture: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(
|
||||
ownedServerData?.author === userId,
|
||||
userId,
|
||||
ownedServerData?.author,
|
||||
);
|
||||
}
|
||||
|
||||
if (customizationData || ownedServerData) {
|
||||
return {
|
||||
...(customizationData as any),
|
||||
isOwned: true,
|
||||
isOwnedByUser: ownedServerData?.author === userId,
|
||||
userProfilePicture: userId ? user.imageUrl : "no user",
|
||||
userProfilePicture: userId ? user?.imageUrl : "no user",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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" });
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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" });
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
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,16 +29,13 @@
|
||||
*/
|
||||
|
||||
import { getAuth } from "@clerk/nextjs/server";
|
||||
import {
|
||||
createRouteHandler,
|
||||
createUploadthing,
|
||||
type FileRouter,
|
||||
} from "uploadthing/next-legacy";
|
||||
import { UploadThingError } from "uploadthing/server";
|
||||
import { createRouteHandler, createUploadthing } from "uploadthing/next-legacy";
|
||||
import { UploadThingError, UTApi } from "uploadthing/server";
|
||||
import { findServerData } from "..";
|
||||
import { MongoClient } from "mongodb";
|
||||
import type { FileRoute } from "uploadthing/types";
|
||||
import type { Json } from "@uploadthing/shared";
|
||||
import { checkOwnedServerMetadata } from "@/lib/check-owned-server";
|
||||
|
||||
const f = createUploadthing();
|
||||
|
||||
@ -63,19 +60,25 @@ export default createRouteHandler({
|
||||
const mongoClient = new MongoClient(process.env.MONGO_DB as string);
|
||||
|
||||
try {
|
||||
if (!serverData.exists) throw new UploadThingError("Server doesn't exist");
|
||||
if (!serverData.exists)
|
||||
throw new UploadThingError("Server doesn't exist");
|
||||
await mongoClient.connect();
|
||||
|
||||
const db = mongoClient.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const ownedServer = await db.collection("owned-servers").findOne({ $or: [{ serverId: server }, { server: serverData.name }] });
|
||||
const ownedServer = await db
|
||||
.collection("owned-servers")
|
||||
.findOne({
|
||||
$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.");
|
||||
if (ownedServer.author !== userId)
|
||||
throw new UploadThingError("You don't own this server.");
|
||||
|
||||
return {
|
||||
userId,
|
||||
ownedServer,
|
||||
serverId: server,
|
||||
serverName: serverData.name
|
||||
serverName: serverData.name,
|
||||
};
|
||||
} finally {
|
||||
await mongoClient.close();
|
||||
@ -86,18 +89,34 @@ export default createRouteHandler({
|
||||
console.log("Upload complete for userId:", metadata.userId);
|
||||
console.log("file url", file.ufsUrl);
|
||||
console.log("metadata:", metadata);
|
||||
const utapi = new UTApi();
|
||||
|
||||
// Update the server's customization data with the new banner URL
|
||||
// Step 3: Update the server's customization data with the new banner URL
|
||||
const mongoClient = new MongoClient(process.env.MONGO_DB as string);
|
||||
try {
|
||||
await mongoClient.connect();
|
||||
const { customizedServer } =
|
||||
await checkOwnedServerMetadata(metadata.userId, mongoClient, {
|
||||
id: metadata.serverId as string,
|
||||
});
|
||||
const db = mongoClient.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
|
||||
// Update or insert the customization data
|
||||
const result = await db.collection("customization").updateOne(
|
||||
{ $or: [{ serverId: metadata.serverId }, { server: metadata.serverName }] },
|
||||
{ $set: { banner: file.ufsUrl } },
|
||||
{ upsert: true }
|
||||
// Step 3.5: Delete old banner if needed
|
||||
if (customizedServer?.banner) {
|
||||
await utapi.deleteFiles(customizedServer._deletionId);
|
||||
}
|
||||
|
||||
// Step 4: Update or insert the customization data
|
||||
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);
|
||||
@ -122,4 +141,4 @@ export type BannerUploaderRouter = {
|
||||
};
|
||||
errorShape: Json;
|
||||
}>;
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user