mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-07 16:54:59 -05:00
feat: enforcing color modes
This commit is contained in:
parent
20afc2de8e
commit
2a58192a14
@ -45,7 +45,12 @@ 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,6 +43,7 @@
|
|||||||
"@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",
|
||||||
@ -93,10 +94,15 @@
|
|||||||
"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",
|
||||||
@ -155,7 +161,6 @@
|
|||||||
"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,15 +37,26 @@ 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 { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
import type { 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(mhsfData.server?.customizationData.description !== undefined ? "description" : "motd");
|
const [tab, setTab] = useState(
|
||||||
|
mhsfData.server?.customizationData.description !== undefined
|
||||||
|
? "description"
|
||||||
|
: "motd",
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Material className="p-4 relative h-[250px]">
|
<Material className="p-4 relative h-[250px]">
|
||||||
@ -72,7 +83,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")}
|
||||||
>
|
>
|
||||||
@ -110,9 +121,37 @@ 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 className="min-w-full">{mhsfData.server?.customizationData.description}</Markdown>
|
<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} 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 { 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 { 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 {
|
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";
|
||||||
@ -228,49 +232,32 @@ 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">
|
||||||
<DrawerTitle className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
{serverData.server?.customizationData
|
||||||
Server Settings
|
.customizationVersion === 2 ? (
|
||||||
</DrawerTitle>
|
<div>
|
||||||
<Material className="grid gap-1 max-h-[700px]">
|
<DrawerTitle className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
||||||
<strong>Server Description</strong>
|
Server Settings
|
||||||
<p className="mb-3">
|
</DrawerTitle>
|
||||||
A markdown enabled, fancy description for your server!
|
<ServerDescriptionBox
|
||||||
Describe what players will expect from your server and
|
serverData={serverData}
|
||||||
why they should join; don't worry, you have more space
|
minehutData={minehutData}
|
||||||
than MOTD's.
|
|
||||||
</p>
|
|
||||||
{!serverData.loading && (
|
|
||||||
<ServerEditorDescription
|
|
||||||
defaultMarkdown={
|
|
||||||
serverData.server?.customizationData.description ??
|
|
||||||
`# ${minehutData.name}`
|
|
||||||
}
|
|
||||||
onUpdate={(content) => console.log(content)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<ServerBannerBox
|
||||||
</Material>
|
serverData={serverData}
|
||||||
<Material className="grid gap-1 mt-2 max-h-[700px]">
|
minehutData={minehutData}
|
||||||
<strong>Server Banner</strong>
|
/>
|
||||||
<p className="mb-3">
|
<ServerColorModeBox
|
||||||
Pick out whatever represents your server best! Images
|
serverData={serverData}
|
||||||
have a limit of 4.5MB, and the prefered aspect ratio for
|
minehutData={minehutData}
|
||||||
the banner should be 19:11 to look the best on MHSF.
|
/>
|
||||||
</p>
|
</div>
|
||||||
<UploadDropzone
|
) : (
|
||||||
endpoint="imageUploader"
|
<ServerMigrationBox
|
||||||
className="uploadthing-dropzone"
|
oldVersion={1}
|
||||||
onClientUploadComplete={(res) => {
|
reupdate={() => serverData.refresh()}
|
||||||
console.log("Upload complete response:", res);
|
id={minehutData._id}
|
||||||
// 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
|
||||||
|
|||||||
@ -17,8 +17,12 @@ export function ServerMainPage({
|
|||||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mhsfData.server?.customizationData.banner !== undefined)
|
if (mhsfData.server?.customizationData.colorMode !== undefined) {
|
||||||
window.dispatchEvent(new Event("force-dark-mode"));
|
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 (
|
return (
|
||||||
|
|||||||
@ -38,24 +38,27 @@ 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 [forcedDark, setForcedDark] = React.useState(false);
|
const [forcedTheme, setForcedTheme] = React.useState<'dark' | 'light' | undefined>();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
|
||||||
window.addEventListener("force-dark-mode", () => {
|
window.addEventListener("force-dark-mode", () => {
|
||||||
setForcedDark(true);
|
setForcedTheme('dark');
|
||||||
|
});
|
||||||
|
window.addEventListener("force-light-mode", () => {
|
||||||
|
setForcedTheme('light');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setForcedDark(false);
|
setForcedTheme(undefined);
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextThemeProvider forcedTheme={forcedDark ? "dark" : undefined} {...props}>
|
<NextThemeProvider forcedTheme={forcedTheme} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</NextThemeProvider>
|
</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,27 +30,43 @@
|
|||||||
|
|
||||||
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" | undefined;
|
||||||
|
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: {
|
||||||
description: string | undefined;
|
userProfilePicture: string | undefined;
|
||||||
banner: string | undefined;
|
/** If undefined then this is 1. */
|
||||||
discord: string | undefined;
|
isOwned: boolean;
|
||||||
colorScheme: string | undefined;
|
isOwnedByUser: boolean;
|
||||||
userProfilePicture: string | undefined;
|
} & ActualCustomization;
|
||||||
isOwned: boolean;
|
playerData: {
|
||||||
isOwnedByUser: boolean;
|
historically: { date: string; playerCount: number }[];
|
||||||
};
|
max: number;
|
||||||
playerData: {
|
};
|
||||||
historically: { date: string; playerCount: number }[];
|
achievements: {
|
||||||
max: number;
|
historically: { _id: string; name: string; achievements: Achievement[] }[];
|
||||||
};
|
currently: Achievement[];
|
||||||
achievements: {
|
};
|
||||||
historically: { _id: string; name: string; achievements: Achievement[] }[];
|
|
||||||
currently: Achievement[];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,41 +166,38 @@ async function findCustomizationData(
|
|||||||
: null,
|
: null,
|
||||||
]);
|
]);
|
||||||
let user: User | undefined = undefined;
|
let user: User | undefined = undefined;
|
||||||
try {
|
if (ownedServerData) {
|
||||||
user = await clerk.users.getUser(ownedServerData?.author);
|
|
||||||
} catch (e) {
|
try {
|
||||||
console.warn(e);
|
user = await clerk.users.getUser(ownedServerData?.author);
|
||||||
if (customizationData || ownedServerData) {
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
if (customizationData || ownedServerData) {
|
||||||
|
return {
|
||||||
|
...(customizationData as any),
|
||||||
|
isOwned: true,
|
||||||
|
isOwnedByUser: ownedServerData?.author === userId,
|
||||||
|
userProfilePicture: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...(customizationData as any),
|
isOwned: false,
|
||||||
isOwned: true,
|
isOwnedByUser: false,
|
||||||
isOwnedByUser: ownedServerData?.author === userId,
|
description: undefined,
|
||||||
userProfilePicture: null,
|
banner: undefined,
|
||||||
|
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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 { getAuth } from "@clerk/nextjs/server";
|
||||||
import {
|
import { createRouteHandler, createUploadthing } from "uploadthing/next-legacy";
|
||||||
createRouteHandler,
|
import { UploadThingError, UTApi } from "uploadthing/server";
|
||||||
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();
|
||||||
|
|
||||||
@ -52,74 +49,96 @@ 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) throw new UploadThingError("Server doesn't exist");
|
if (!serverData.exists)
|
||||||
await mongoClient.connect();
|
throw new UploadThingError("Server doesn't exist");
|
||||||
|
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.collection("owned-servers").findOne({ $or: [{ serverId: server }, { server: serverData.name }] });
|
const ownedServer = await db
|
||||||
if (!ownedServer) throw new UploadThingError("Server not linked");
|
.collection("owned-servers")
|
||||||
if (ownedServer.author !== userId) throw new UploadThingError("You don't own this server.");
|
.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.");
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
// 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);
|
const mongoClient = new MongoClient(process.env.MONGO_DB as string);
|
||||||
try {
|
try {
|
||||||
await mongoClient.connect();
|
const { customizedServer } =
|
||||||
const db = mongoClient.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
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
|
// Step 3.5: Delete old banner if needed
|
||||||
const result = await db.collection("customization").updateOne(
|
if (customizedServer?.banner) {
|
||||||
{ $or: [{ serverId: metadata.serverId }, { server: metadata.serverName }] },
|
await utapi.deleteFiles(customizedServer._deletionId);
|
||||||
{ $set: { banner: file.ufsUrl } },
|
}
|
||||||
{ upsert: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Database update result:", result);
|
// 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 },
|
||||||
|
);
|
||||||
|
|
||||||
// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
|
console.log("Database update result:", result);
|
||||||
return { uploadedBy: metadata.userId };
|
|
||||||
} catch (error) {
|
// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
|
||||||
console.error("Error updating database:", error);
|
return { uploadedBy: metadata.userId };
|
||||||
throw error;
|
} catch (error) {
|
||||||
} finally {
|
console.error("Error updating database:", error);
|
||||||
await mongoClient.close();
|
throw error;
|
||||||
}
|
} 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