mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-15 15:08:01 -05:00
Compare commits
No commits in common. "53f7cdb171ea2fab272108c3ee6b7e67d4b479ae" and "7d0bb445689054648ee3d3358b64c33ba45e934b" have entirely different histories.
53f7cdb171
...
7d0bb44568
@ -28,9 +28,8 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const obj = {
|
/** @type {import('next-sitemap').IConfig} */
|
||||||
|
module.exports = {
|
||||||
siteUrl: "https://mhsf.app",
|
siteUrl: "https://mhsf.app",
|
||||||
generateRobotsTxt: true
|
generateRobotsTxt: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export default obj;
|
|
||||||
@ -41,10 +41,6 @@ const nextConfig = {
|
|||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "avatars.githubusercontent.com"
|
hostname: "avatars.githubusercontent.com"
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "cdn.discordapp.com"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -21,11 +21,6 @@
|
|||||||
"@clerk/nextjs": "^6.9.2",
|
"@clerk/nextjs": "^6.9.2",
|
||||||
"@emotion/is-prop-valid": "^1.3.0",
|
"@emotion/is-prop-valid": "^1.3.0",
|
||||||
"@linear/sdk": "^31.0.0",
|
"@linear/sdk": "^31.0.0",
|
||||||
"@milkdown/plugin-history": "^7.9.0",
|
|
||||||
"@milkdown/plugin-listener": "^7.9.0",
|
|
||||||
"@milkdown/preset-commonmark": "^7.9.0",
|
|
||||||
"@milkdown/react": "^7.9.0",
|
|
||||||
"@milkdown/theme-nord": "^7.9.0",
|
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@number-flow/react": "^0.5.7",
|
"@number-flow/react": "^0.5.7",
|
||||||
"@radix-ui/react-aspect-ratio": "1.1.1",
|
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||||
|
|||||||
@ -1,12 +1,49 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
||||||
<url><loc>https://mhsf.app/settings</loc><lastmod>2025-05-05T04:09:03.452Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
<url><loc>https://mhsf.app/dashboard</loc><lastmod>2025-02-14T18:54:35.340Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
<url><loc>https://mhsf.app/support</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
<url><loc>https://mhsf.app/account/settings</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
<url><loc>https://mhsf.app/waitlist/oauth-need-discord</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
<url><loc>https://mhsf.app/account/settings/options</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
<url><loc>https://mhsf.app/waitlist/ref</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
<url><loc>https://mhsf.app</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
<url><loc>https://mhsf.app/servers/embedded/sl-modification-frame</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
<url><loc>https://mhsf.app/account/favorites</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
<url><loc>https://mhsf.app/home</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
<url><loc>https://mhsf.app/docs/advanced%2Fachievements</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
<url><loc>https://mhsf.app/servers/embedded/sl-modification-frame/files</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
<url><loc>https://mhsf.app/docs/advanced%2Fcommand-bar</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
<url><loc>https://mhsf.app/servers</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
<url><loc>https://mhsf.app/docs/advanced%2Fexternal</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
<url><loc>https://mhsf.app/waitlist</loc><lastmod>2025-05-05T04:09:03.471Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
<url><loc>https://mhsf.app/docs/advanced%2Ftech-stack</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
|
<url><loc>https://mhsf.app/docs/getting-started</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
|
<url><loc>https://mhsf.app/docs/guides%2Fcustomization</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
|
<url><loc>https://mhsf.app/docs/guides%2Flinking</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
|
<url><loc>https://mhsf.app/docs/guides%2Fowning-a-server</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
|
<url><loc>https://mhsf.app/docs/guides%2Freporting-server</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
|
<url><loc>https://mhsf.app/docs/legal%2Fexternal-content-agreement</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
|
<url><loc>https://mhsf.app/docs/reading</loc><lastmod>2025-02-14T18:54:35.389Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||||
</urlset>
|
</urlset>
|
||||||
@ -1,4 +1,34 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<sitemap><loc>https://mhsf.app/sitemap-0.xml</loc></sitemap>
|
<sitemap><loc>https://mhsf.app/sitemap-0.xml</loc></sitemap>
|
||||||
</sitemapindex>
|
</sitemapindex>
|
||||||
@ -1,35 +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 { WaitlistPage } from "@/components/feat/waitlist/waitlist-page";
|
|
||||||
|
|
||||||
export default function Waitlist() {
|
|
||||||
return <WaitlistPage />
|
|
||||||
}
|
|
||||||
@ -1,35 +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 { WaitlistReferralBeta } from "@/components/feat/waitlist/waitlist-referral-beta";
|
|
||||||
|
|
||||||
export default function ReferralBeta() {
|
|
||||||
return <WaitlistReferralBeta />
|
|
||||||
}
|
|
||||||
@ -27,13 +27,22 @@
|
|||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { Placeholder } from "@/components/ui/placeholder";
|
||||||
|
import { X } from "lucide-react";
|
||||||
import { IsScript } from "@/components/util/is-script";
|
import { IsScript } from "@/components/util/is-script";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { NavBar } from "@/components/feat/navbar/navbar";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { ThemeProvider } from "@/components/util/theme-provider";
|
import { ThemeProvider } from "@/components/util/theme-provider";
|
||||||
import { FontBoundary } from "@/components/util/font-boundary";
|
import { FontBoundary } from "@/components/util/font-boundary";
|
||||||
import { ClerkProvider } from "@/components/util/clerk-provider";
|
import { ClerkProvider } from "@/components/util/clerk-provider";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
|
import { Footer } from "@/components/feat/footer/footer";
|
||||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||||
import { IframeProtector } from "@/components/util/iframe-protector";
|
import { IframeProtector } from "@/components/util/iframe-protector";
|
||||||
import NextTopLoader from "@/components/util/top-loader";
|
import NextTopLoader from "@/components/util/top-loader";
|
||||||
@ -43,6 +52,8 @@ export default function RootLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const search = searchParams?.get("theme") || "light";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -38,7 +38,6 @@ import { ArrowLeft } from "lucide-react";
|
|||||||
import { useQueryState } from "nuqs";
|
import { useQueryState } from "nuqs";
|
||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { invertHex } from "../../page";
|
|
||||||
|
|
||||||
export default function ModificationPage({
|
export default function ModificationPage({
|
||||||
params,
|
params,
|
||||||
@ -49,6 +48,7 @@ export default function ModificationPage({
|
|||||||
const [backRoute] = useQueryState("b", {
|
const [backRoute] = useQueryState("b", {
|
||||||
defaultValue: "/servers/embedded/sl-modification-frame",
|
defaultValue: "/servers/embedded/sl-modification-frame",
|
||||||
});
|
});
|
||||||
|
console.log(mod);
|
||||||
const categoryObj = serverModDB.find(
|
const categoryObj = serverModDB.find(
|
||||||
(c) => c.displayTitle === atob(decodeURIComponent(category))
|
(c) => c.displayTitle === atob(decodeURIComponent(category))
|
||||||
);
|
);
|
||||||
@ -65,7 +65,7 @@ export default function ModificationPage({
|
|||||||
style={{ backgroundColor: modObj?.color }}
|
style={{ backgroundColor: modObj?.color }}
|
||||||
>
|
>
|
||||||
<Link href={backRoute}>
|
<Link href={backRoute}>
|
||||||
<ArrowLeft style={{color: invertHex(modObj?.color ?? "")}} />
|
<ArrowLeft />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,6 @@ import { useQueryState } from "nuqs";
|
|||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { invertHex } from "../../../page";
|
|
||||||
|
|
||||||
export default function ModificationPage({
|
export default function ModificationPage({
|
||||||
params,
|
params,
|
||||||
@ -79,6 +78,7 @@ export default function ModificationPage({
|
|||||||
const [backRoute] = useQueryState("b", {
|
const [backRoute] = useQueryState("b", {
|
||||||
defaultValue: "/servers/embedded/sl-modification-frame",
|
defaultValue: "/servers/embedded/sl-modification-frame",
|
||||||
});
|
});
|
||||||
|
console.log(mod);
|
||||||
const modIndex = (
|
const modIndex = (
|
||||||
(user?.unsafeMetadata
|
(user?.unsafeMetadata
|
||||||
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
|
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
|
||||||
@ -109,7 +109,7 @@ export default function ModificationPage({
|
|||||||
style={{ backgroundColor: modObj?.color }}
|
style={{ backgroundColor: modObj?.color }}
|
||||||
>
|
>
|
||||||
<Link href={backRoute}>
|
<Link href={backRoute}>
|
||||||
<ArrowLeft color={invertHex(modObj?.color)} />
|
<ArrowLeft />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -120,27 +120,22 @@ export default function ModificationPage({
|
|||||||
you proud?)
|
you proud?)
|
||||||
</Markdown>
|
</Markdown>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Button
|
<Button className="mt-2" onClick={async () => {
|
||||||
className="mt-2"
|
|
||||||
onClick={async () => {
|
|
||||||
const newModObj = {
|
const newModObj = {
|
||||||
...modObj,
|
...modObj,
|
||||||
active: !modObj.active,
|
active: !modObj.active
|
||||||
};
|
}
|
||||||
const modificationArray =
|
const modificationArray = (user?.unsafeMetadata
|
||||||
(user?.unsafeMetadata
|
.activatedModifications as ClerkCustomActivatedModification[]) ?? [];
|
||||||
.activatedModifications as ClerkCustomActivatedModification[]) ??
|
|
||||||
[];
|
|
||||||
modificationArray[modIndex] = newModObj;
|
modificationArray[modIndex] = newModObj;
|
||||||
await user?.update({
|
await user?.update({
|
||||||
unsafeMetadata: {
|
unsafeMetadata: {
|
||||||
...user.unsafeMetadata,
|
...user.unsafeMetadata,
|
||||||
activatedModifications: modificationArray,
|
activatedModifications: modificationArray
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
communicator.send("rerender-servers", {});
|
communicator.send("rerender-servers", {});
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{modObj?.active ? "Disable" : "Enable"}
|
{modObj?.active ? "Disable" : "Enable"}
|
||||||
</Button>
|
</Button>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|||||||
@ -51,6 +51,7 @@ export default async function ServerListCategoryFrame({
|
|||||||
const categoryObj = serverModDB.find(
|
const categoryObj = serverModDB.find(
|
||||||
(c) => c.displayTitle === atob(decodeURIComponent(category)),
|
(c) => c.displayTitle === atob(decodeURIComponent(category)),
|
||||||
);
|
);
|
||||||
|
``
|
||||||
return (
|
return (
|
||||||
<main className=" p-4">
|
<main className=" p-4">
|
||||||
<h1 className="text-xl font-bold w-full flex items-center gap-2">
|
<h1 className="text-xl font-bold w-full flex items-center gap-2">
|
||||||
@ -77,10 +78,7 @@ export default async function ServerListCategoryFrame({
|
|||||||
)}
|
)}
|
||||||
style={{ backgroundColor: m.color }}
|
style={{ backgroundColor: m.color }}
|
||||||
>
|
>
|
||||||
<m.icon
|
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
||||||
className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center"
|
|
||||||
color={invertHex(m.color)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-center w-full flex items-center justify-center">
|
<span className="text-sm text-center w-full flex items-center justify-center">
|
||||||
{m.name}
|
{m.name}
|
||||||
@ -97,27 +95,3 @@ export default async function ServerListCategoryFrame({
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertHex(hex: string) {
|
|
||||||
if (hex.indexOf("#") === 0) {
|
|
||||||
hex = hex.slice(1);
|
|
||||||
}
|
|
||||||
// convert 3-digit hex to 6-digits.
|
|
||||||
if (hex.length === 3) {
|
|
||||||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
||||||
}
|
|
||||||
if (hex.length !== 6) {
|
|
||||||
throw new Error("Invalid HEX color.");
|
|
||||||
}
|
|
||||||
// invert color components
|
|
||||||
const r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16),
|
|
||||||
g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16),
|
|
||||||
b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16);
|
|
||||||
// pad each with zeros and return
|
|
||||||
return "#" + padZero(r) + padZero(g) + padZero(b);
|
|
||||||
}
|
|
||||||
function padZero(str: string, len: number) {
|
|
||||||
len = len || 2;
|
|
||||||
const zeros = new Array(len).join("0");
|
|
||||||
return (zeros + str).slice(-len);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -39,7 +39,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { useRouter } from "@/lib/useRouter";
|
import { useRouter } from "@/lib/useRouter";
|
||||||
import { SignedIn, useUser } from "@clerk/nextjs";
|
import { SignedIn, useUser } from "@clerk/nextjs";
|
||||||
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||||
import { invertHex } from "./category/[category]/page";
|
|
||||||
|
|
||||||
export default function ServerListModificationFrame() {
|
export default function ServerListModificationFrame() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -82,7 +81,7 @@ export default function ServerListModificationFrame() {
|
|||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-6 lg:grid-cols-3 gap-2">
|
<div className="grid grid-cols-6 lg:grid-cols-3 gap-2">
|
||||||
{c.entries.slice(0, 6).map((m) => (
|
{c.entries.map((m) => (
|
||||||
<Material
|
<Material
|
||||||
elevation="high"
|
elevation="high"
|
||||||
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
|
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
|
||||||
@ -97,10 +96,7 @@ export default function ServerListModificationFrame() {
|
|||||||
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
||||||
style={{ backgroundColor: m.color }}
|
style={{ backgroundColor: m.color }}
|
||||||
>
|
>
|
||||||
<m.icon
|
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
||||||
className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center"
|
|
||||||
color={invertHex(m.color)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-center w-full flex items-center justify-center">
|
<span className="text-sm text-center w-full flex items-center justify-center">
|
||||||
{m.name}
|
{m.name}
|
||||||
@ -128,10 +124,7 @@ export default function ServerListModificationFrame() {
|
|||||||
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
||||||
style={{ backgroundColor: m.color }}
|
style={{ backgroundColor: m.color }}
|
||||||
>
|
>
|
||||||
<Binary
|
<Binary className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
||||||
className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center"
|
|
||||||
color={invertHex(m.color)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-center w-full flex items-center justify-center">
|
<span className="text-sm text-center w-full flex items-center justify-center">
|
||||||
{m.friendlyName}
|
{m.friendlyName}
|
||||||
|
|||||||
BIN
apps/www/src/app/api/og/fonts/Inter-Medium.ttf
Normal file
BIN
apps/www/src/app/api/og/fonts/Inter-Medium.ttf
Normal file
Binary file not shown.
@ -4,10 +4,13 @@ export async function loadFonts() {
|
|||||||
new URL("./Inter-Regular.ttf", import.meta.url)
|
new URL("./Inter-Regular.ttf", import.meta.url)
|
||||||
).then((res) => res.arrayBuffer());
|
).then((res) => res.arrayBuffer());
|
||||||
|
|
||||||
|
const interMediumFontP = fetch(
|
||||||
|
new URL("./Inter-Medium.ttf", import.meta.url)
|
||||||
|
).then((res) => res.arrayBuffer());
|
||||||
|
|
||||||
const interBoldFontP = fetch(
|
const interBoldFontP = fetch(
|
||||||
new URL("./Inter-Bold.ttf", import.meta.url)
|
new URL("./Inter-Bold.ttf", import.meta.url)
|
||||||
).then((res) => res.arrayBuffer());
|
).then((res) => res.arrayBuffer());
|
||||||
|
|
||||||
return Promise.all([interRegularFontP, interBoldFontP]);
|
return Promise.all([interRegularFontP, interMediumFontP, interBoldFontP]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ async function loadLocalFonts() {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
fs.readFileSync(path.join(fontPath, "Inter-Regular.ttf")),
|
fs.readFileSync(path.join(fontPath, "Inter-Regular.ttf")),
|
||||||
|
fs.readFileSync(path.join(fontPath, "Inter-Medium.ttf")),
|
||||||
fs.readFileSync(path.join(fontPath, "Inter-Bold.ttf")),
|
fs.readFileSync(path.join(fontPath, "Inter-Bold.ttf")),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -35,11 +36,19 @@ export async function GET(
|
|||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
// Load banner image from filesystem
|
||||||
|
const bannerPath = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"public",
|
||||||
|
"branding",
|
||||||
|
"bg-banner.png"
|
||||||
|
);
|
||||||
|
const bannerImageData = fs.readFileSync(bannerPath);
|
||||||
|
|
||||||
const id = (await params).id;
|
const id = (await params).id;
|
||||||
|
|
||||||
// Load fonts
|
// Load fonts
|
||||||
const [interRegular, interBold] = await loadLocalFonts();
|
const [interRegular, interMedium, interBold] = await loadLocalFonts();
|
||||||
|
|
||||||
// Verify server exists
|
// Verify server exists
|
||||||
const serverResponse = await fetch(`https://api.minehut.com/server/${id}`);
|
const serverResponse = await fetch(`https://api.minehut.com/server/${id}`);
|
||||||
@ -60,7 +69,7 @@ export async function GET(
|
|||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
backgroundColor: "white",
|
backgroundImage: `url(data:image/png;base64,${bannerImageData.toString("base64")})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
backgroundPosition: "center",
|
backgroundPosition: "center",
|
||||||
fontFamily: "Inter",
|
fontFamily: "Inter",
|
||||||
@ -248,7 +257,7 @@ export async function GET(
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
fontFamily: "Inter",
|
fontFamily: "Inter",
|
||||||
backgroundColor: "white",
|
backgroundImage: `url(data:image/png;base64,${bannerImageData.toString("base64")})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
backgroundPosition: "center",
|
backgroundPosition: "center",
|
||||||
}}
|
}}
|
||||||
@ -540,6 +549,12 @@ export async function GET(
|
|||||||
style: "normal",
|
style: "normal",
|
||||||
weight: 700,
|
weight: 700,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Inter",
|
||||||
|
data: interMedium,
|
||||||
|
style: "normal",
|
||||||
|
weight: 500,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -555,6 +570,21 @@ export async function GET(
|
|||||||
console.error("Failed to load fonts for error page:", e);
|
console.error("Failed to load fonts for error page:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to load the banner image
|
||||||
|
let bannerImageData: Buffer | null = null;
|
||||||
|
try {
|
||||||
|
const bannerPath = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"public",
|
||||||
|
"branding",
|
||||||
|
"dark-banner.png"
|
||||||
|
);
|
||||||
|
bannerImageData = fs.readFileSync(bannerPath);
|
||||||
|
} catch (e) {
|
||||||
|
// If banner image fails to load, use a solid color background
|
||||||
|
console.error("Failed to load banner image for error page:", e);
|
||||||
|
}
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
(
|
(
|
||||||
<div
|
<div
|
||||||
@ -562,6 +592,7 @@ export async function GET(
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
fontSize: 60,
|
fontSize: 60,
|
||||||
color: "white",
|
color: "white",
|
||||||
|
background: bannerImageData ? undefined : "#121212",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
padding: "50px 50px",
|
padding: "50px 50px",
|
||||||
@ -569,7 +600,11 @@ export async function GET(
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
fontFamily: "Inter",
|
fontFamily: "Inter",
|
||||||
backgroundColor: "white"
|
...(bannerImageData && {
|
||||||
|
backgroundImage: `url(data:image/png;base64,${bannerImageData.toString("base64")})`,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -128,11 +128,15 @@ export async function GET(
|
|||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
// Load banner image
|
||||||
|
const bannerImageData = await fetch(
|
||||||
|
new URL("/branding/bg-banner.png", request.url)
|
||||||
|
).then((res) => res.arrayBuffer());
|
||||||
|
|
||||||
const id = (await params).id;
|
const id = (await params).id;
|
||||||
|
|
||||||
// Load fonts
|
// Load fonts
|
||||||
const [interRegular, interBold] = await loadFonts();
|
const [interRegular, interMedium, interBold] = await loadFonts();
|
||||||
|
|
||||||
// Fetch server data
|
// Fetch server data
|
||||||
const response = await fetch(`https://api.minehut.com/server/${id}`);
|
const response = await fetch(`https://api.minehut.com/server/${id}`);
|
||||||
@ -154,6 +158,7 @@ export async function GET(
|
|||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
backgroundImage: `url(data:image/png;base64,${Buffer.from(bannerImageData).toString("base64")})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
backgroundPosition: "center",
|
backgroundPosition: "center",
|
||||||
fontFamily: "Inter",
|
fontFamily: "Inter",
|
||||||
@ -221,7 +226,7 @@ export async function GET(
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
fontFamily: "Inter",
|
fontFamily: "Inter",
|
||||||
backgroundColor: "white",
|
backgroundImage: `url(data:image/png;base64,${Buffer.from(bannerImageData).toString("base64")})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
backgroundPosition: "center",
|
backgroundPosition: "center",
|
||||||
}}
|
}}
|
||||||
@ -412,15 +417,30 @@ export async function GET(
|
|||||||
data: interBold,
|
data: interBold,
|
||||||
style: "normal",
|
style: "normal",
|
||||||
weight: 700,
|
weight: 700,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
name: "Inter",
|
||||||
|
data: interMedium,
|
||||||
|
style: "normal",
|
||||||
|
weight: 500,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const [interRegular, interBold] = await loadFonts();
|
const [interRegular, interMedium, interBold] = await loadFonts();
|
||||||
console.error("Error generating OG image:", error);
|
console.error("Error generating OG image:", error);
|
||||||
|
|
||||||
|
// Try to load the banner image again in case it failed earlier
|
||||||
|
let bannerImageData: ArrayBuffer | undefined;
|
||||||
|
try {
|
||||||
|
bannerImageData = await fetch(
|
||||||
|
new URL("/branding/dark-banner.png", request.url)
|
||||||
|
).then((res) => res.arrayBuffer());
|
||||||
|
} catch (e) {
|
||||||
|
// If banner image fails to load, use a solid color background
|
||||||
|
console.error("Failed to load banner image for error page:", e);
|
||||||
|
}
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
(
|
(
|
||||||
@ -429,7 +449,7 @@ export async function GET(
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
fontSize: 60,
|
fontSize: 60,
|
||||||
color: "white",
|
color: "white",
|
||||||
background: "#121212",
|
background: bannerImageData ? undefined : "#121212",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
padding: "50px 50px",
|
padding: "50px 50px",
|
||||||
@ -437,7 +457,11 @@ export async function GET(
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
fontFamily: "Inter",
|
fontFamily: "Inter",
|
||||||
backgroundColor: "white",
|
...(bannerImageData && {
|
||||||
|
backgroundImage: `url(data:image/png;base64,${Buffer.from(bannerImageData).toString("base64")})`,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
|
|||||||
@ -72,6 +72,13 @@
|
|||||||
--sidebar-border: 220 13% 91%;
|
--sidebar-border: 220 13% 91%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
/* Workaround for Tailwind being stupid */
|
||||||
|
border-color: hsl(214.3 31.8% 91.4%);
|
||||||
|
}
|
||||||
|
|
||||||
--sidebar: hsl(0 0% 98%);
|
--sidebar: hsl(0 0% 98%);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -234,19 +241,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: #7f600060
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.rainbow-tag {
|
|
||||||
background: linear-gradient(45deg, rgba(255, 0, 0, 1) 0%, rgba(255, 154, 0, 1) 10%, rgba(208, 222, 33, 1) 20%, rgba(79, 220, 74, 1) 30%, rgba(63, 218, 216, 1) 40%, rgba(47, 201, 226, 1) 50%, rgba(28, 127, 238, 1) 60%, rgba(95, 21, 242, 1) 70%, rgba(186, 12, 248, 1) 80%, rgba(251, 7, 217, 1) 90%, rgba(255, 0, 0, 1) 100%)
|
|
||||||
}
|
|
||||||
|
|
||||||
.shiki {
|
|
||||||
@apply min-w-full
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-shimmer {
|
.loading-shimmer {
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
animation-duration: 2s;
|
animation-duration: 2s;
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export default function RootLayout({
|
|||||||
title="JavaScript is required for MHSF"
|
title="JavaScript is required for MHSF"
|
||||||
description="MHSF cannot grab servers or do other external requests without JavaScript."
|
description="MHSF cannot grab servers or do other external requests without JavaScript."
|
||||||
>
|
>
|
||||||
<Link href="https://www.enable-javascript.com/" noextraicons>
|
<Link href="https://www.enable-javascript.com/" noExtraIcons>
|
||||||
<Button>Here's how</Button>
|
<Button>Here's how</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</Placeholder>
|
</Placeholder>
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { NotFoundComponent } from "@/components/util/not-found";
|
import { NotFoundComponent } from "@/components/util/not-found";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|||||||
@ -99,7 +99,9 @@ export default function Embed({ params }: { params: { server: string } }) {
|
|||||||
<div className="px-4 pt-2 flex items-center group overflow-hidden">
|
<div className="px-4 pt-2 flex items-center group overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
staticMode ? "block" : "opacity-0 group-hover:opacity-100 transform transition-all duration-300 ease-in-out group-hover:translate-x-0 -translate-x-full",
|
staticMode ? "block" : "opacity-0 group-hover:opacity-100",
|
||||||
|
"transform transition-all duration-300 ease-in-out",
|
||||||
|
"group-hover:translate-x-0 -translate-x-full",
|
||||||
"absolute left-[10px] w-0 group-hover:w-[64px]"
|
"absolute left-[10px] w-0 group-hover:w-[64px]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export function Footer() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<Link href="https://t.mhsf.app/d/m" noextraicons>
|
<Link href="https://t.mhsf.app/d/m" noExtraIcons>
|
||||||
<DropdownMenuItem className="py-2 flex items-center gap-2">
|
<DropdownMenuItem className="py-2 flex items-center gap-2">
|
||||||
<Image className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" src="https://avatars.githubusercontent.com/u/16529253?s=200&v=4" alt="Minehut" width={30} height={30} />
|
<Image className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" src="https://avatars.githubusercontent.com/u/16529253?s=200&v=4" alt="Minehut" width={30} height={30} />
|
||||||
<span className="block">
|
<span className="block">
|
||||||
@ -67,7 +67,7 @@ export function Footer() {
|
|||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="https://t.mhsf.app/d/u" noextraicons>
|
<Link href="https://t.mhsf.app/d/u" noExtraIcons>
|
||||||
<DropdownMenuItem className="py-2 flex items-center gap-2">
|
<DropdownMenuItem className="py-2 flex items-center gap-2">
|
||||||
<BrandingGenericIcon className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" width={30} height={30} />
|
<BrandingGenericIcon className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" width={30} height={30} />
|
||||||
<span className="block">
|
<span className="block">
|
||||||
@ -78,7 +78,7 @@ export function Footer() {
|
|||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Link href="https://github.com/DeveloLongScript/MHSF" noextraicons>
|
<Link href="https://github.com/DeveloLongScript/MHSF" noExtraIcons>
|
||||||
<Button variant="tertiary" size="square-md" className="flex items-center">
|
<Button variant="tertiary" size="square-md" className="flex items-center">
|
||||||
<Github className="w-[1.25em] h-[1.25em]" />
|
<Github className="w-[1.25em] h-[1.25em]" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -89,7 +89,7 @@ export function Footer() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="block px-4 lg:-translate-y-12">
|
<span className="block px-4 -translate-y-12">
|
||||||
<small className="text-[0.75rem]">
|
<small className="text-[0.75rem]">
|
||||||
MHSF is an open-source project licensed under the MIT license. MHSF is
|
MHSF is an open-source project licensed under the MIT license. MHSF is
|
||||||
not officially affiliated with with Minehut, Super League Enterprise,
|
not officially affiliated with with Minehut, Super League Enterprise,
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export function FooterStatus() {
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`https://${statusURL as string}${determineIfOutage() ? `/incident/${determineWhatOutage()?.id}` : ""}`}
|
href={`https://${statusURL as string}${determineIfOutage() ? `/incident/${determineWhatOutage()?.id}` : ""}`}
|
||||||
noextraicons
|
noExtraIcons
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button variant="tertiary">
|
<Button variant="tertiary">
|
||||||
|
|||||||
@ -59,55 +59,56 @@ export const brandingIconClipboard = `<svg width="266" height="265" viewBox="0 0
|
|||||||
export function BrandingColorfulIcon(props: SVGProps<SVGSVGElement>) {
|
export function BrandingColorfulIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="266"
|
width="266"
|
||||||
height="265"
|
height="265"
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 266 265"
|
viewBox="0 0 266 265"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
|
x="0.524048"
|
||||||
width="264.939"
|
width="264.939"
|
||||||
height="264.939"
|
height="264.939"
|
||||||
x="0.524"
|
|
||||||
fill="url(#paint0_linear_1_19)"
|
|
||||||
rx="66"
|
rx="66"
|
||||||
|
fill="url(#paint0_linear_1_19)"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
stroke="#fff"
|
d="M104.513 123.27H94.8717C92.3148 123.27 89.8626 122.254 88.0546 120.446C86.2466 118.638 85.2309 116.186 85.2309 113.629V94.3476C85.2309 91.7907 86.2466 89.3385 88.0546 87.5305C89.8626 85.7225 92.3148 84.7068 94.8717 84.7068H171.998C174.555 84.7068 177.007 85.7225 178.815 87.5305C180.623 89.3385 181.639 91.7907 181.639 94.3476V113.629C181.639 116.186 180.623 118.638 178.815 120.446C177.007 122.254 174.555 123.27 171.998 123.27H162.357M104.513 142.552H94.8717C92.3148 142.552 89.8626 143.567 88.0546 145.376C86.2466 147.184 85.2309 149.636 85.2309 152.193V171.474C85.2309 174.031 86.2466 176.483 88.0546 178.291C89.8626 180.099 92.3148 181.115 94.8717 181.115H171.998C174.555 181.115 177.007 180.099 178.815 178.291C180.623 176.483 181.639 174.031 181.639 171.474V152.193C181.639 149.636 180.623 147.184 178.815 145.376C177.007 143.567 174.555 142.552 171.998 142.552H162.357M104.513 103.988H104.561M104.513 161.833H104.561M138.255 103.988L118.974 132.911H147.896L128.615 161.833"
|
||||||
strokeLinecap="round"
|
stroke="white"
|
||||||
strokeLinejoin="round"
|
stroke-width="10"
|
||||||
strokeWidth="10"
|
stroke-linecap="round"
|
||||||
d="M104.513 123.27h-9.641a9.64 9.64 0 0 1-9.641-9.641V94.348a9.64 9.64 0 0 1 9.64-9.641h77.127a9.64 9.64 0 0 1 9.641 9.64v19.282a9.64 9.64 0 0 1-9.641 9.641h-9.641m-57.844 19.282h-9.641a9.64 9.64 0 0 0-9.64 9.641v19.281a9.64 9.64 0 0 0 9.64 9.641h77.126a9.64 9.64 0 0 0 9.641-9.641v-19.281a9.64 9.64 0 0 0-9.641-9.641h-9.641m-57.844-38.564h.048m-.048 57.845h.048m33.694-57.845-19.281 28.923h28.922l-19.281 28.922"
|
stroke-linejoin="round"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx="132.993"
|
cx="132.993"
|
||||||
cy="132.469"
|
cy="132.469"
|
||||||
r="91.378"
|
r="91.3779"
|
||||||
stroke="url(#paint1_linear_1_19)"
|
stroke="url(#paint1_linear_1_19)"
|
||||||
strokeWidth="8"
|
stroke-width="8"
|
||||||
/>
|
/>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint0_linear_1_19"
|
id="paint0_linear_1_19"
|
||||||
x1="107.824"
|
x1="107.824"
|
||||||
x2="230.579"
|
|
||||||
y1="54.754"
|
y1="54.754"
|
||||||
|
x2="230.579"
|
||||||
y2="225.198"
|
y2="225.198"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stopColor="#007BFF"/>
|
<stop stop-color="#007BFF" />
|
||||||
<stop offset="1" stopColor="#BF00FF" stopOpacity="0.5"/>
|
<stop offset="1" stop-color="#BF00FF" stop-opacity="0.5" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint1_linear_1_19"
|
id="paint1_linear_1_19"
|
||||||
x1="132.993"
|
x1="132.993"
|
||||||
|
y1="37.0914"
|
||||||
x2="132.993"
|
x2="132.993"
|
||||||
y1="37.091"
|
|
||||||
y2="227.847"
|
y2="227.847"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stopColor="#EFEC32"/>
|
<stop stop-color="#EFEC32" />
|
||||||
<stop offset="1" stopColor="#98FF60"/>
|
<stop offset="1" stop-color="#98FF60" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
@ -124,65 +125,65 @@ export function BrandingColorfulIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
export function BrandingPrideIcon(props: SVGProps<SVGSVGElement>) {
|
export function BrandingPrideIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="265"
|
width="265"
|
||||||
height="265"
|
height="265"
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 265 265"
|
viewBox="0 0 265 265"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
width="264.939"
|
width="264.939"
|
||||||
height="264.939"
|
height="264.939"
|
||||||
fill="url(#paint0_linear_1_30)"
|
|
||||||
rx="66"
|
rx="66"
|
||||||
|
fill="url(#paint0_linear_1_30)"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
stroke="#fff"
|
d="M103.988 123.27H94.3476C91.7907 123.27 89.3385 122.254 87.5305 120.446C85.7225 118.638 84.7068 116.186 84.7068 113.629V94.3476C84.7068 91.7907 85.7225 89.3385 87.5305 87.5305C89.3385 85.7225 91.7907 84.7068 94.3476 84.7068H171.474C174.031 84.7068 176.483 85.7225 178.291 87.5305C180.099 89.3385 181.115 91.7907 181.115 94.3476V113.629C181.115 116.186 180.099 118.638 178.291 120.446C176.483 122.254 174.031 123.27 171.474 123.27H161.833M103.988 142.552H94.3476C91.7907 142.552 89.3385 143.567 87.5305 145.376C85.7225 147.184 84.7068 149.636 84.7068 152.193V171.474C84.7068 174.031 85.7225 176.483 87.5305 178.291C89.3385 180.099 91.7907 181.115 94.3476 181.115H171.474C174.031 181.115 176.483 180.099 178.291 178.291C180.099 176.483 181.115 174.031 181.115 171.474V152.193C181.115 149.636 180.099 147.184 178.291 145.376C176.483 143.567 174.031 142.552 171.474 142.552H161.833M103.988 103.988H104.037M103.988 161.833H104.037M137.731 103.988L118.45 132.911H147.372L128.091 161.833"
|
||||||
strokeLinecap="round"
|
stroke="white"
|
||||||
strokeLinejoin="round"
|
stroke-width="10"
|
||||||
strokeWidth="10"
|
stroke-linecap="round"
|
||||||
d="M103.988 123.27h-9.64a9.64 9.64 0 0 1-9.641-9.641V94.348a9.64 9.64 0 0 1 9.64-9.641h77.127a9.64 9.64 0 0 1 9.641 9.64v19.282a9.64 9.64 0 0 1-9.641 9.641h-9.641m-57.845 19.282h-9.64a9.64 9.64 0 0 0-9.64 9.641v19.281a9.64 9.64 0 0 0 9.64 9.641h77.126a9.64 9.64 0 0 0 9.641-9.641v-19.281a9.64 9.64 0 0 0-9.641-9.641h-9.641m-57.845-38.564h.049m-.049 57.845h.049m33.694-57.845-19.281 28.923h28.922l-19.281 28.922"
|
stroke-linejoin="round"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx="132.469"
|
cx="132.469"
|
||||||
cy="132.469"
|
cy="132.469"
|
||||||
r="91.378"
|
r="91.3779"
|
||||||
stroke="url(#paint1_linear_1_30)"
|
stroke="url(#paint1_linear_1_30)"
|
||||||
strokeWidth="8"
|
stroke-width="8"
|
||||||
/>
|
/>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint0_linear_1_30"
|
id="paint0_linear_1_30"
|
||||||
x1="51.663"
|
x1="51.6631"
|
||||||
|
y1="26.9354"
|
||||||
x2="222.549"
|
x2="222.549"
|
||||||
y1="26.935"
|
|
||||||
y2="213.717"
|
y2="213.717"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stopColor="red" />
|
<stop stop-color="#FF0000" />
|
||||||
<stop offset="0.11" stopColor="#FF6200" />
|
<stop offset="0.110405" stop-color="#FF6200" />
|
||||||
<stop offset="0.226" stopColor="#FFAE00" />
|
<stop offset="0.225785" stop-color="#FFAE00" />
|
||||||
<stop offset="0.326" stopColor="#FFD500" />
|
<stop offset="0.326294" stop-color="#FFD500" />
|
||||||
<stop offset="0.422" stopColor="#99EA00" />
|
<stop offset="0.422381" stop-color="#99EA00" />
|
||||||
<stop offset="0.498" stopColor="#4DF457" />
|
<stop offset="0.498373" stop-color="#4DF457" />
|
||||||
<stop offset="0.593" stopColor="#26D3AB" />
|
<stop offset="0.593491" stop-color="#26D3AB" />
|
||||||
<stop offset="0.7" stopColor="#13A9D5" />
|
<stop offset="0.699814" stop-color="#13A9D5" />
|
||||||
<stop offset="0.806" stopColor="#A200FF" />
|
<stop offset="0.805673" stop-color="#A200FF" />
|
||||||
<stop offset="0.884" stopColor="#C62AEB" />
|
<stop offset="0.884464" stop-color="#C62AEB" />
|
||||||
<stop offset="0.957" stopColor="#fff" />
|
<stop offset="0.957056" stop-color="white" />
|
||||||
<stop offset="0.997" />
|
<stop offset="0.997383" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint1_linear_1_30"
|
id="paint1_linear_1_30"
|
||||||
x1="132.469"
|
x1="132.469"
|
||||||
|
y1="37.0914"
|
||||||
x2="132.469"
|
x2="132.469"
|
||||||
y1="37.091"
|
|
||||||
y2="227.847"
|
y2="227.847"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stopColor="#EFEC32" />
|
<stop stop-color="#EFEC32" />
|
||||||
<stop offset="1" stopColor="#98FF60" />
|
<stop offset="1" stop-color="#98FF60" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
@ -207,41 +208,41 @@ export function BrandingGenericIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
if (resolvedTheme === "dark") {
|
if (resolvedTheme === "dark") {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="265"
|
width="265"
|
||||||
height="266"
|
height="266"
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 265 266"
|
viewBox="0 0 265 266"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
|
x="0.0612793"
|
||||||
|
y="0.86145"
|
||||||
width="264.939"
|
width="264.939"
|
||||||
height="264.939"
|
height="264.939"
|
||||||
x="0.061"
|
|
||||||
y="0.861"
|
|
||||||
fill="url(#paint0_linear_1_20)"
|
|
||||||
rx="66"
|
rx="66"
|
||||||
|
fill="url(#paint0_linear_1_20)"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
stroke="#fff"
|
d="M104.05 124.132H94.4089C91.852 124.132 89.3998 123.116 87.5918 121.308C85.7838 119.5 84.7681 117.048 84.7681 114.491V95.2091C84.7681 92.6522 85.7838 90.2 87.5918 88.392C89.3998 86.584 91.852 85.5683 94.4089 85.5683H171.536C174.092 85.5683 176.545 86.584 178.353 88.392C180.161 90.2 181.176 92.6522 181.176 95.2091V114.491C181.176 117.048 180.161 119.5 178.353 121.308C176.545 123.116 174.092 124.132 171.536 124.132H161.895M104.05 143.413H94.4089C91.852 143.413 89.3998 144.429 87.5918 146.237C85.7838 148.045 84.7681 150.497 84.7681 153.054V172.336C84.7681 174.893 85.7838 177.345 87.5918 179.153C89.3998 180.961 91.852 181.977 94.4089 181.977H171.536C174.092 181.977 176.545 180.961 178.353 179.153C180.161 177.345 181.176 174.893 181.176 172.336V153.054C181.176 150.497 180.161 148.045 178.353 146.237C176.545 144.429 174.092 143.413 171.536 143.413H161.895M104.05 104.85H104.098M104.05 162.695H104.098M137.793 104.85L118.511 133.772H147.433L128.152 162.695"
|
||||||
strokeLinecap="round"
|
stroke="white"
|
||||||
strokeLinejoin="round"
|
stroke-width="10"
|
||||||
strokeWidth="10"
|
stroke-linecap="round"
|
||||||
d="M104.05 124.132h-9.641a9.64 9.64 0 0 1-9.64-9.641V95.209a9.64 9.64 0 0 1 9.64-9.64h77.127a9.64 9.64 0 0 1 9.64 9.64v19.282a9.64 9.64 0 0 1-9.64 9.641h-9.641m-57.845 19.281h-9.641a9.64 9.64 0 0 0-9.64 9.641v19.282a9.64 9.64 0 0 0 9.64 9.641h77.127a9.64 9.64 0 0 0 9.64-9.641v-19.282a9.64 9.64 0 0 0-9.64-9.641h-9.641M104.05 104.85h.048m-.048 57.845h.048m33.695-57.845-19.282 28.922h28.922l-19.281 28.923"
|
stroke-linejoin="round"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx="132.531"
|
cx="132.531"
|
||||||
cy="133.331"
|
cy="133.331"
|
||||||
r="91.378"
|
r="91.3779"
|
||||||
stroke="url(#paint1_linear_1_20)"
|
stroke="url(#paint1_linear_1_20)"
|
||||||
strokeWidth="8"
|
stroke-width="8"
|
||||||
/>
|
/>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint0_linear_1_20"
|
id="paint0_linear_1_20"
|
||||||
x1="107.361"
|
x1="107.361"
|
||||||
|
y1="55.6155"
|
||||||
x2="230.116"
|
x2="230.116"
|
||||||
y1="55.615"
|
|
||||||
y2="226.059"
|
y2="226.059"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
@ -250,13 +251,13 @@ export function BrandingGenericIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint1_linear_1_20"
|
id="paint1_linear_1_20"
|
||||||
x1="132.531"
|
x1="132.531"
|
||||||
|
y1="37.9529"
|
||||||
x2="132.531"
|
x2="132.531"
|
||||||
y1="37.953"
|
|
||||||
y2="228.709"
|
y2="228.709"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stopColor="#EFEC32" />
|
<stop stop-color="#EFEC32" />
|
||||||
<stop offset="1" stopColor="#98FF60" />
|
<stop offset="1" stop-color="#98FF60" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
@ -264,55 +265,90 @@ export function BrandingGenericIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="265"
|
width="265"
|
||||||
height="265"
|
height="265"
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 265 265"
|
viewBox="0 0 265 265"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
|
x="0.0612793"
|
||||||
width="264.939"
|
width="264.939"
|
||||||
height="264.939"
|
height="264.939"
|
||||||
x="0.061"
|
|
||||||
fill="url(#paint0_linear_1_25)"
|
|
||||||
rx="66"
|
rx="66"
|
||||||
|
fill="url(#paint0_linear_1_25)"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
stroke="#000"
|
d="M104.05 123.27H94.4089C91.852 123.27 89.3998 122.254 87.5918 120.446C85.7838 118.638 84.7681 116.186 84.7681 113.629V94.3476C84.7681 91.7907 85.7838 89.3385 87.5918 87.5305C89.3998 85.7225 91.852 84.7068 94.4089 84.7068H171.536C174.092 84.7068 176.545 85.7225 178.353 87.5305C180.161 89.3385 181.176 91.7907 181.176 94.3476V113.629C181.176 116.186 180.161 118.638 178.353 120.446C176.545 122.254 174.092 123.27 171.536 123.27H161.895M104.05 142.552H94.4089C91.852 142.552 89.3998 143.567 87.5918 145.376C85.7838 147.184 84.7681 149.636 84.7681 152.193V171.474C84.7681 174.031 85.7838 176.483 87.5918 178.291C89.3998 180.099 91.852 181.115 94.4089 181.115H171.536C174.092 181.115 176.545 180.099 178.353 178.291C180.161 176.483 181.176 174.031 181.176 171.474V152.193C181.176 149.636 180.161 147.184 178.353 145.376C176.545 143.567 174.092 142.552 171.536 142.552H161.895M104.05 103.988H104.098M104.05 161.833H104.098M137.793 103.988L118.511 132.911H147.433L128.152 161.833"
|
||||||
strokeLinecap="round"
|
stroke="black"
|
||||||
strokeLinejoin="round"
|
stroke-width="10"
|
||||||
strokeWidth="10"
|
stroke-linecap="round"
|
||||||
d="M104.05 123.27h-9.641a9.64 9.64 0 0 1-9.64-9.641V94.348a9.64 9.64 0 0 1 9.64-9.641h77.127a9.64 9.64 0 0 1 9.64 9.64v19.282a9.64 9.64 0 0 1-9.64 9.641h-9.641m-57.845 19.282h-9.641a9.64 9.64 0 0 0-9.64 9.641v19.281a9.64 9.64 0 0 0 9.64 9.641h77.127a9.64 9.64 0 0 0 9.64-9.641v-19.281a9.64 9.64 0 0 0-9.64-9.641h-9.641m-57.845-38.564h.048m-.048 57.845h.048m33.695-57.845-19.282 28.923h28.922l-19.281 28.922"
|
stroke-linejoin="round"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx="132.531"
|
cx="132.531"
|
||||||
cy="132.469"
|
cy="132.469"
|
||||||
r="91.378"
|
r="91.3779"
|
||||||
stroke="url(#paint1_linear_1_25)"
|
stroke="url(#paint1_linear_1_25)"
|
||||||
strokeWidth="8"
|
stroke-width="8"
|
||||||
/>
|
/>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint0_linear_1_25"
|
id="paint0_linear_1_25"
|
||||||
x1="107.361"
|
x1="107.361"
|
||||||
x2="230.116"
|
|
||||||
y1="54.754"
|
y1="54.754"
|
||||||
|
x2="230.116"
|
||||||
y2="225.198"
|
y2="225.198"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stopColor="#fff" />
|
<stop stop-color="white" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint1_linear_1_25"
|
id="paint1_linear_1_25"
|
||||||
x1="132.531"
|
x1="132.531"
|
||||||
|
y1="37.0914"
|
||||||
x2="132.531"
|
x2="132.531"
|
||||||
y1="37.091"
|
|
||||||
y2="227.847"
|
y2="227.847"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
>
|
>
|
||||||
<stop stopColor="#EFEC32" />
|
<stop stop-color="#EFEC32" />
|
||||||
<stop offset="1" stopColor="#98FF60" />
|
<stop offset="1" stop-color="#98FF60" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BadgeOfAffiliation(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="81"
|
||||||
|
height="81"
|
||||||
|
viewBox="0 0 81 81"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<rect width="81" height="81" rx="34" fill="url(#paint0_linear_1_12)" />
|
||||||
|
<path
|
||||||
|
d="M29.5 36.5H26C25.0717 36.5 24.1815 36.1313 23.5251 35.4749C22.8687 34.8185 22.5 33.9283 22.5 33V26C22.5 25.0717 22.8687 24.1815 23.5251 23.5251C24.1815 22.8687 25.0717 22.5 26 22.5H54C54.9283 22.5 55.8185 22.8687 56.4749 23.5251C57.1313 24.1815 57.5 25.0717 57.5 26V33C57.5 33.9283 57.1313 34.8185 56.4749 35.4749C55.8185 36.1313 54.9283 36.5 54 36.5H50.5M29.5 43.5H26C25.0717 43.5 24.1815 43.8687 23.5251 44.5251C22.8687 45.1815 22.5 46.0717 22.5 47V54C22.5 54.9283 22.8687 55.8185 23.5251 56.4749C24.1815 57.1313 25.0717 57.5 26 57.5H54C54.9283 57.5 55.8185 57.1313 56.4749 56.4749C57.1313 55.8185 57.5 54.9283 57.5 54V47C57.5 46.0717 57.1313 45.1815 56.4749 44.5251C55.8185 43.8687 54.9283 43.5 54 43.5H50.5M29.5 29.5H29.5175M29.5 50.5H29.5175M41.75 29.5L34.75 40H45.25L38.25 50.5"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear_1_12"
|
||||||
|
x1="40.5"
|
||||||
|
y1="0"
|
||||||
|
x2="40.5"
|
||||||
|
y2="81"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#37B14F" />
|
||||||
|
<stop offset="1" stop-color="#3D4B17" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@ -40,7 +40,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
export default function IconDisplay(props: {
|
export default function IconDisplay(props: {
|
||||||
server: OnlineServer | ServerResponse | { icon: string };
|
server: OnlineServer | ServerResponse;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -71,12 +71,12 @@ export function NavBar() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-[3rem] grid-cols-3 fixed z-10 flex",
|
"w-screen h-[3rem] grid-cols-3 fixed z-10 flex",
|
||||||
"items-center justify-self-start me-auto pl-4 flex-1 transition-all justify-between",
|
"items-center justify-self-start me-auto pl-4 flex-1 transition-all justify-between",
|
||||||
"lg:top-0 max-lg:bottom-0 bg-neutral-100 dark:bg-neutral-900",
|
"lg:top-0 max-lg:bottom-0",
|
||||||
showBorder
|
showBorder
|
||||||
? "border-b backdrop-blur-xl w-screen"
|
? "border-b backdrop-blur-xl"
|
||||||
: "max-lg:border-b max-lg:w-screen lg:border-b-slate-300 lg:dark:border-t-zinc-700 lg:border lg:m-2 lg:w-[calc(100vw-24px)] lg:rounded-lg lg:border-neutral-500/20 lg:bg-neutral-100 lg:dark:border-neutral-700/50 lg:dark:bg-neutral-900",
|
: "max-lg:border-b max-lg:backdrop-blur-xl",
|
||||||
pathname !== null && animatedTopbarPages.includes(pathname)
|
pathname !== null && animatedTopbarPages.includes(pathname)
|
||||||
? "[--animation-delay:1000ms] opacity-0 animate-fade-in"
|
? "[--animation-delay:1000ms] opacity-0 animate-fade-in"
|
||||||
: "",
|
: "",
|
||||||
@ -192,10 +192,8 @@ export function NavBar() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<SignedIn>
|
<SignedIn>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className="absolute right-0 -z-10 h-full
|
||||||
"absolute right-0 -z-10 h-full transition-all overflow-hidden w-full ml-auto",
|
overflow-hidden w-full ml-auto"
|
||||||
showBorder ? "" : "hidden",
|
|
||||||
)}
|
|
||||||
style={{ borderRadius: "inherit" }}
|
style={{ borderRadius: "inherit" }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@ -121,14 +121,14 @@ export function CustomErrors({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<Link
|
<Link
|
||||||
noextraicons
|
noExtraIcons
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`https://typescript.tv/errors/#ts${c.code}`}
|
href={`https://typescript.tv/errors/#ts${c.code}`}
|
||||||
>
|
>
|
||||||
<DropdownMenuItem>typescript.tv</DropdownMenuItem>
|
<DropdownMenuItem>typescript.tv</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
noextraicons
|
noExtraIcons
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`https://ts-error-translator.vercel.app/?error=${compressToEncodedURIComponent(c.messageText.toString())}`}
|
href={`https://ts-error-translator.vercel.app/?error=${compressToEncodedURIComponent(c.messageText.toString())}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -40,8 +40,7 @@ export function ModificationAction({ value }: { value?: Action }) {
|
|||||||
>) ?? []
|
>) ?? []
|
||||||
).findIndex(
|
).findIndex(
|
||||||
(c) =>
|
(c) =>
|
||||||
JSON.stringify(c.metadata) ===
|
JSON.stringify(c.metadata) === JSON.stringify(filter.toIdentifier()) &&
|
||||||
JSON.stringify(filter.toIdentifier()) &&
|
|
||||||
c.type === filter.getSpecificFilterId(),
|
c.type === filter.getSpecificFilterId(),
|
||||||
);
|
);
|
||||||
return existing;
|
return existing;
|
||||||
@ -49,7 +48,7 @@ export function ModificationAction({ value }: { value?: Action }) {
|
|||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => setApplied(findExisting()));
|
useEffect(() => setApplied(findExisting()))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -69,17 +68,15 @@ export function ModificationAction({ value }: { value?: Action }) {
|
|||||||
className="mt-1"
|
className="mt-1"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (value?.type() === "filter") {
|
if (value?.type() === "filter") {
|
||||||
const updatedUser = await user?.reload();
|
|
||||||
const filter = value as Filter;
|
const filter = value as Filter;
|
||||||
const existing = findExisting();
|
const existing = findExisting();
|
||||||
|
|
||||||
if (isSignedIn) {
|
if (isSignedIn) {
|
||||||
const existingArray =
|
const existingArray =
|
||||||
(updatedUser?.unsafeMetadata.filters as Array<
|
(user.unsafeMetadata.filters as Array<
|
||||||
ClerkEmbeddedFilter<unknown>
|
ClerkEmbeddedFilter<unknown>
|
||||||
>) ?? [];
|
>) ?? [];
|
||||||
const previousFilters = updatedUser?.unsafeMetadata
|
existingArray.splice(existing, 1);
|
||||||
.filters as Array<ClerkEmbeddedFilter<unknown>>;
|
|
||||||
if (existing === -1)
|
if (existing === -1)
|
||||||
await user.update({
|
await user.update({
|
||||||
unsafeMetadata: {
|
unsafeMetadata: {
|
||||||
@ -89,19 +86,19 @@ export function ModificationAction({ value }: { value?: Action }) {
|
|||||||
type: filter.getSpecificFilterId(),
|
type: filter.getSpecificFilterId(),
|
||||||
metadata: filter.toIdentifier(),
|
metadata: filter.toIdentifier(),
|
||||||
},
|
},
|
||||||
...previousFilters,
|
...((user.unsafeMetadata.filters as Array<
|
||||||
|
ClerkEmbeddedFilter<unknown>
|
||||||
|
>) ?? []),
|
||||||
] as Array<ClerkEmbeddedFilter<unknown>>,
|
] as Array<ClerkEmbeddedFilter<unknown>>,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
else {
|
else
|
||||||
existingArray.splice(existing, 1);
|
|
||||||
await user.update({
|
await user.update({
|
||||||
unsafeMetadata: {
|
unsafeMetadata: {
|
||||||
...user.unsafeMetadata,
|
|
||||||
filters: existingArray,
|
filters: existingArray,
|
||||||
|
...user.unsafeMetadata,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const existingArray =
|
const existingArray =
|
||||||
(JSON.parse(
|
(JSON.parse(
|
||||||
@ -113,20 +110,17 @@ export function ModificationAction({ value }: { value?: Action }) {
|
|||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"mhsf__filters",
|
"mhsf__filters",
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
...((JSON.parse(
|
|
||||||
localStorage.getItem("mhsf__filters") ?? "[]",
|
|
||||||
) as Array<ClerkEmbeddedFilter<unknown>>) ?? []),
|
|
||||||
{
|
{
|
||||||
type: filter.getSpecificFilterId(),
|
type: filter.getSpecificFilterId(),
|
||||||
metadata: filter.toIdentifier(),
|
metadata: filter.toIdentifier(),
|
||||||
},
|
},
|
||||||
|
...((JSON.parse(
|
||||||
|
localStorage.getItem("mhsf__filters") ?? "[]",
|
||||||
|
) as Array<ClerkEmbeddedFilter<unknown>>) ?? []),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
localStorage.setItem(
|
localStorage.setItem("mhsf__filters", JSON.stringify(existingArray));
|
||||||
"mhsf__filters",
|
|
||||||
JSON.stringify(existingArray),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setApplied(findExisting());
|
setApplied(findExisting());
|
||||||
|
|||||||
@ -35,7 +35,7 @@ import { ModificationFrame } from "./modification-frame";
|
|||||||
export function ModificationButton({disabled}: {disabled?: boolean}) {
|
export function ModificationButton({disabled}: {disabled?: boolean}) {
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger>
|
||||||
<Button disabled={disabled}>Filters & Sorting</Button>
|
<Button disabled={disabled}>Filters & Sorting</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,6 @@ export function ModificationFrame() {
|
|||||||
communication.toIframe.send("ping", {from: "top-layer"})
|
communication.toIframe.send("ping", {from: "top-layer"})
|
||||||
})
|
})
|
||||||
communication.toIframe.handle("rerender-servers", (c) => {
|
communication.toIframe.handle("rerender-servers", (c) => {
|
||||||
window.dispatchEvent(new Event("start-loading-server-view"))
|
|
||||||
window.dispatchEvent(new Event("update-modification-stack"))
|
window.dispatchEvent(new Event("update-modification-stack"))
|
||||||
})
|
})
|
||||||
}, [ref])
|
}, [ref])
|
||||||
|
|||||||
@ -39,7 +39,7 @@ import {
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useEffectOnce } from "@/lib/useEffectOnce";
|
import { useEffectOnce } from "@/lib/useEffectOnce";
|
||||||
import { allTags } from "@/config/tags";
|
import { allTags } from "@/config/tags";
|
||||||
import { type ReactNode, useEffect, useState } from "react";
|
import { type ReactNode, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -176,7 +176,6 @@ export function TagShower(props: {
|
|||||||
|
|
||||||
useEffectOnce(() => {
|
useEffectOnce(() => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
// biome-ignore lint/complexity/noForEach: no.
|
|
||||||
allTags.forEach((tag) => {
|
allTags.forEach((tag) => {
|
||||||
if (!tag.condition) {
|
if (!tag.condition) {
|
||||||
tag.name({ online: props.server }).then((n) => {
|
tag.name({ online: props.server }).then((n) => {
|
||||||
@ -247,7 +246,7 @@ export function TagShower(props: {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{'"'}
|
{'"'}
|
||||||
{t.docsName === undefined ? t.name : t.docsName}
|
{t.docsName == undefined ? t.name : t.docsName}
|
||||||
{'"'} documentation
|
{'"'} documentation
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription
|
<DialogDescription
|
||||||
|
|||||||
@ -44,34 +44,19 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ServerRandomServerProvider } from "./server-random-server-provider";
|
|
||||||
import { Dice2, Dices, EllipsisIcon, RefreshCcw, ShareIcon } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import useClipboard from "@/lib/useClipboard";
|
|
||||||
|
|
||||||
export function ServerList() {
|
export function ServerList() {
|
||||||
const { servers, loading, serverCount, playerCount, refresh } = useServers();
|
const { servers, loading, serverCount, playerCount } = useServers();
|
||||||
const {
|
const {
|
||||||
filteredData,
|
filteredData,
|
||||||
testModeEnabled,
|
testModeEnabled,
|
||||||
testModeLoading,
|
testModeLoading,
|
||||||
testModeStatus,
|
testModeStatus,
|
||||||
filterCount,
|
filterCount,
|
||||||
tagStrings,
|
|
||||||
loading: filterLoading,
|
loading: filterLoading,
|
||||||
} = useFilters(servers);
|
} = useFilters(servers);
|
||||||
const { itemsLength, fetchMoreData, hasMoreData, data } =
|
const { itemsLength, fetchMoreData, hasMoreData, data } =
|
||||||
useInfiniteScrolling(filteredData);
|
useInfiniteScrolling(filteredData);
|
||||||
const clipboard = useClipboard();
|
|
||||||
|
|
||||||
if (loading)
|
if (loading)
|
||||||
return (
|
return (
|
||||||
@ -82,7 +67,6 @@ export function ServerList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="px-3 lg:px-16">
|
<main className="px-3 lg:px-16">
|
||||||
<ServerRandomServerProvider servers={filteredData}>
|
|
||||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
||||||
Statistics
|
Statistics
|
||||||
</h1>
|
</h1>
|
||||||
@ -95,82 +79,18 @@ export function ServerList() {
|
|||||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl">
|
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl">
|
||||||
Servers
|
Servers
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center">
|
||||||
<span className="flex items-center">
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<ModificationButton disabled={testModeEnabled} />
|
<ModificationButton disabled={testModeEnabled} />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent side="bottom" className="backdrop-blur bg-transparent text-black dark:text-white ">{filterCount} modification(s) enabled</TooltipContent>
|
||||||
side="bottom"
|
|
||||||
className="backdrop-blur bg-transparent text-black dark:text-white "
|
|
||||||
>
|
|
||||||
{filterCount} modification(s) enabled
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ServerTestModeSelector
|
<ServerTestModeSelector
|
||||||
testModeStatus={testModeStatus}
|
testModeStatus={testModeStatus}
|
||||||
testModeEnabled={testModeEnabled}
|
testModeEnabled={testModeEnabled}
|
||||||
testModeLoading={testModeLoading}
|
testModeLoading={testModeLoading}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-1 ml-3 max-w-[calc(100vw-200px)] overflow-auto max-lg:pb-2">
|
|
||||||
{tagStrings.map((c) => (
|
|
||||||
<Badge
|
|
||||||
key={c}
|
|
||||||
className="flex px-3 break-keep whitespace-nowrap"
|
|
||||||
variant="gray-subtle"
|
|
||||||
>
|
|
||||||
{c}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<Button
|
|
||||||
className="flex items-center"
|
|
||||||
size="square-md"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<EllipsisIcon size={16} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuSeparator>Servers</DropdownMenuSeparator>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
window.dispatchEvent(new Event("open-random-server"))
|
|
||||||
}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Dices size={16} />
|
|
||||||
Pick random server
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => refresh()}
|
|
||||||
>
|
|
||||||
<RefreshCcw size={16} />
|
|
||||||
Reload
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator>Share</DropdownMenuSeparator>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => {
|
|
||||||
const data = { url: "https://mhsf.app", text: "Check out MHSF, the modern server finder!" };
|
|
||||||
if (navigator.canShare(data))
|
|
||||||
navigator.share(data)
|
|
||||||
else {
|
|
||||||
clipboard.writeText("https://mhsf.app")
|
|
||||||
toast.success("Sent to clipboard!")
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShareIcon size={16} />
|
|
||||||
Share MHSF!
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
{filterLoading ? (
|
{filterLoading ? (
|
||||||
<span className="mt-2 left-[50%] right-[50%] absolute">
|
<span className="mt-2 left-[50%] right-[50%] absolute">
|
||||||
@ -194,7 +114,6 @@ export function ServerList() {
|
|||||||
</div>
|
</div>
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
)}
|
)}
|
||||||
</ServerRandomServerProvider>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export function Statistics({
|
|||||||
setAverages(fetchJson);
|
setAverages(fetchJson);
|
||||||
})();
|
})();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
setError(true);
|
setError(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import { Placeholder } from "@/components/ui/placeholder";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { formalNames } from "@/config/achievements";
|
|
||||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
|
||||||
import type { Achievement } from "@/lib/types/achievement";
|
|
||||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
|
|
||||||
export function AchievementsView({
|
|
||||||
server,
|
|
||||||
mhsfData,
|
|
||||||
}: { server: ServerResponse; mhsfData: ReturnType<typeof useMHSFServer> }) {
|
|
||||||
return (
|
|
||||||
<Material className="p-4 relative h-[250px] max-lg:mt-3">
|
|
||||||
<span className="mb-2">
|
|
||||||
<strong className="text-lg">Achievements</strong>
|
|
||||||
|
|
||||||
<Separator className="my-2" />
|
|
||||||
</span>
|
|
||||||
<div className="p-2 max-h-[170px] overflow-auto">
|
|
||||||
{mhsfData.server?.achievements.currently.filter(
|
|
||||||
(value, index, array) => listify(array).indexOf(value.type) === index,
|
|
||||||
).length === 0 && (
|
|
||||||
<Placeholder
|
|
||||||
icon={<X />}
|
|
||||||
title="We couldn't find any achievements"
|
|
||||||
description="Maybe shake the box harder?"
|
|
||||||
className="mt-4"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{mhsfData.server?.achievements.currently
|
|
||||||
.filter(
|
|
||||||
(value, index, array) =>
|
|
||||||
listify(array).indexOf(value.type) === index,
|
|
||||||
)
|
|
||||||
.map((c, i) => {
|
|
||||||
const Icon = formalNames[c.type].icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-2" key={i}>
|
|
||||||
<span
|
|
||||||
className="flex items-center"
|
|
||||||
style={{ color: formalNames[c.type].color }}
|
|
||||||
>
|
|
||||||
<Icon size={16} className="mr-2" />
|
|
||||||
<span
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: formalNames[c.type].title,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<p>{formalNames[c.type].description}</p>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Achieved on {new Date(c.date).getMonth()}/
|
|
||||||
{new Date(c.date).getDate()}/{new Date(c.date).getFullYear()}{" "}
|
|
||||||
<span className="text-muted-foreground/70">
|
|
||||||
{new Date(c.date).toLocaleTimeString()}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Material>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const listify = (list: Achievement[]) => {
|
|
||||||
const newL: Array<string> = [];
|
|
||||||
|
|
||||||
list.forEach((c) => newL.push(c.type));
|
|
||||||
|
|
||||||
return newL;
|
|
||||||
};
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import { Ripple } from "../../home-page/ripple";
|
|
||||||
|
|
||||||
const PARTNER_HERO = "This server is partnered with MHSF.";
|
|
||||||
const PARTNER_DESCRIPTION =
|
|
||||||
"This server and its staff support the future of MHSF";
|
|
||||||
const PARTNER_DESCRIPTION_2 =
|
|
||||||
"and a portion of users on MHSF may come from this server";
|
|
||||||
const PARTNER_DESCRIPTION_3 = "or it's communication standards.";
|
|
||||||
|
|
||||||
export function AffiliateRow() {
|
|
||||||
return (
|
|
||||||
<Material className="p-4 col-span-2 row-span-2 relative h-[500px] max-lg:mb-3 flex items-center justify-center">
|
|
||||||
<span className="text-center">
|
|
||||||
<h1 className="animate-fade-in text-balance bg-gradient-to-br from-black from-30% to-black/40 bg-clip-text pb-6 text-2xl font-semibold leading-none tracking-tighter text-transparent opacity-0 [--animation-delay:200ms] sm:text-2xl md:text-3xl lg:text-4xl dark:from-white dark:to-white/40">
|
|
||||||
{PARTNER_HERO}
|
|
||||||
</h1>
|
|
||||||
<p className="animate-fade-in mb-6 mt-6 -translate-y-4 text-balance text-md tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms] md:text-xl">
|
|
||||||
{PARTNER_DESCRIPTION} <br /> {PARTNER_DESCRIPTION_2} <br /> {PARTNER_DESCRIPTION_3}
|
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
<Ripple mainCircleSize={700} className="max-md:hidden" />
|
|
||||||
</Material>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,241 +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 { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { useEmbedGenerator } from "@/lib/hooks/use-embed-generator";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { EllipsisVertical } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { ShikiRenderer } from "./embed-shiki-renderer";
|
|
||||||
import { codeToHtml } from "shiki";
|
|
||||||
import { useTheme } from "@/lib/hooks/use-theme";
|
|
||||||
import useClipboard from "@/lib/useClipboard";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function EmbedCreatorRow({ serverName }: { serverName: string }) {
|
|
||||||
const embedCreator = useEmbedGenerator(serverName);
|
|
||||||
const clipboard = useClipboard();
|
|
||||||
const { resolvedTheme } = useTheme();
|
|
||||||
const [tab, setTab] = useState<"preview" | "code">("preview");
|
|
||||||
const [highlightedHtml, setHighlightedHtml] = useState("");
|
|
||||||
const [highlightedJsx, setHighlightedJsx] = useState("");
|
|
||||||
const [codeTab, setCodeTab] = useState<"html" | "jsx">("html");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const selectedTheme =
|
|
||||||
resolvedTheme === "dark" ? "poimandres" : "vitesse-light";
|
|
||||||
async function highlightCode() {
|
|
||||||
const jsx = await codeToHtml(embedCreator.out.jsxCode ?? "", {
|
|
||||||
lang: "jsx",
|
|
||||||
theme: selectedTheme,
|
|
||||||
});
|
|
||||||
const html = await codeToHtml(embedCreator.out.htmlCode ?? "", {
|
|
||||||
lang: "html",
|
|
||||||
theme: selectedTheme,
|
|
||||||
});
|
|
||||||
setHighlightedHtml(html);
|
|
||||||
setHighlightedJsx(jsx);
|
|
||||||
}
|
|
||||||
|
|
||||||
highlightCode();
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Material className="p-4 relative h-[250px] max-lg:mt-3">
|
|
||||||
<span className="mb-2">
|
|
||||||
<span className="flex items-center justify-between">
|
|
||||||
<span className="flex gap-4 items-center">
|
|
||||||
<strong className="text-lg">Embed Creator</strong>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
"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",
|
|
||||||
tab === "preview" &&
|
|
||||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
|
|
||||||
)}
|
|
||||||
onClick={() => setTab("preview")}
|
|
||||||
>
|
|
||||||
Preview
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
"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",
|
|
||||||
tab === "code" &&
|
|
||||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
|
|
||||||
)}
|
|
||||||
onClick={() => setTab("code")}
|
|
||||||
>
|
|
||||||
Code
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<Button
|
|
||||||
className="flex items-center"
|
|
||||||
size="square-md"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<EllipsisVertical size={16} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuSeparator>General</DropdownMenuSeparator>
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={embedCreator.in.staticMode}
|
|
||||||
onCheckedChange={embedCreator.in.setStatic}
|
|
||||||
>
|
|
||||||
Static embed
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={embedCreator.in.removeMinehutBranding}
|
|
||||||
onCheckedChange={embedCreator.in.setRMHB}
|
|
||||||
>
|
|
||||||
Remove Minehut branding
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuSeparator>Theme</DropdownMenuSeparator>
|
|
||||||
<DropdownMenuRadioGroup
|
|
||||||
value={embedCreator.in.theme}
|
|
||||||
onValueChange={(c) =>
|
|
||||||
embedCreator.in.setTheme(c as "light" | "dark")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DropdownMenuRadioItem value="light">
|
|
||||||
Light Mode
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
<DropdownMenuRadioItem value="dark">
|
|
||||||
Dark Mode
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
</DropdownMenuRadioGroup>
|
|
||||||
<DropdownMenuSeparator>Copy</DropdownMenuSeparator>
|
|
||||||
{tab === "code" ? (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
clipboard.writeText(
|
|
||||||
embedCreator.out[
|
|
||||||
codeTab === "html" ? "htmlCode" : "jsxCode"
|
|
||||||
] as string,
|
|
||||||
);
|
|
||||||
toast.success(`Copied ${codeTab.toLocaleUpperCase()} code!`)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Copy code
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
clipboard.writeText(embedCreator.out.jsxCode as string);
|
|
||||||
toast.success("Copied!");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Copy JSX
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
clipboard.writeText(embedCreator.out.htmlCode as string);
|
|
||||||
toast.success("Copied!");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Copy HTML
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Separator className="my-2" />
|
|
||||||
</span>
|
|
||||||
{tab === "preview" && (
|
|
||||||
<iframe
|
|
||||||
src={embedCreator.out.finalURL}
|
|
||||||
className="max-md:w-full w-[390px]"
|
|
||||||
height={145}
|
|
||||||
style={{ borderRadius: "0.25rem" }}
|
|
||||||
allow="clipboard-write"
|
|
||||||
frameBorder={0}
|
|
||||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tab === "code" && (
|
|
||||||
<div className="max-h-[180px] overflow-auto">
|
|
||||||
<div className="dark:bg-[#1b1e28] w-full h-[32px] pt-2 px-2 rounded-t flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
"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",
|
|
||||||
codeTab === "html" &&
|
|
||||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
|
|
||||||
)}
|
|
||||||
onClick={() => setCodeTab("html")}
|
|
||||||
>
|
|
||||||
HTML
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
"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",
|
|
||||||
codeTab === "jsx" &&
|
|
||||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
|
|
||||||
)}
|
|
||||||
onClick={() => setCodeTab("jsx")}
|
|
||||||
>
|
|
||||||
JSX
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: Its shiki man give me a break :sob: */}
|
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: codeTab === "html" ? highlightedHtml : highlightedJsx,
|
|
||||||
}}
|
|
||||||
className="rounded-b"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Material>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
|
||||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { convert } from "../util";
|
|
||||||
import IconDisplay from "../../icons/minecraft-icon-display";
|
|
||||||
|
|
||||||
export function GeneralInfo({
|
|
||||||
server,
|
|
||||||
mhsfData,
|
|
||||||
}: { server: ServerResponse; mhsfData: ReturnType<typeof useMHSFServer> }) {
|
|
||||||
return (
|
|
||||||
<Material className="p-4 relative h-[250px] max-lg:mt-3">
|
|
||||||
<span className="mb-2">
|
|
||||||
<strong className="text-lg">Information</strong>
|
|
||||||
|
|
||||||
<Separator className="my-2" />
|
|
||||||
</span>
|
|
||||||
<div className="p-2 max-h-[170px] overflow-auto">
|
|
||||||
<InfoBox
|
|
||||||
title="Credits/Month"
|
|
||||||
description={Math.floor(server.credits_per_day)}
|
|
||||||
/>
|
|
||||||
<InfoBox
|
|
||||||
title="All time joins"
|
|
||||||
description={convert(server.joins)}
|
|
||||||
/>
|
|
||||||
<InfoBox
|
|
||||||
title="Server Id"
|
|
||||||
description={<code>{server._id}</code>}
|
|
||||||
/>
|
|
||||||
<InfoBox
|
|
||||||
title="Server Expired"
|
|
||||||
description={server.expired ? "Yes" : "No"}
|
|
||||||
/>
|
|
||||||
<InfoBox
|
|
||||||
title="Server External"
|
|
||||||
description={server?.rawPlan === undefined
|
|
||||||
? "? (unknown)"
|
|
||||||
: server?.rawPlan === "EXTERNAL" ? "Yes" : "No"}
|
|
||||||
/>
|
|
||||||
<InfoBox
|
|
||||||
title="Server Icon"
|
|
||||||
description={<div className="flex gap-1 items-center"><IconDisplay server={server}/><code>{(server.icon ?? "sign").toLocaleUpperCase()}</code></div>}
|
|
||||||
/>
|
|
||||||
<InfoBox
|
|
||||||
title="Visible"
|
|
||||||
description={server.visibility ? "Yes" : "No"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Material>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InfoBox({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
}: { title: ReactNode; description: ReactNode }) {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<strong className="text-sm">{title}</strong>
|
|
||||||
<p className="mb-1">{description}</p>
|
|
||||||
<Separator />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
|
||||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
|
||||||
import { getIndexFromRarity } from "@/lib/types/server-icon";
|
|
||||||
import IconDisplay from "../../icons/minecraft-icon-display";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { Banknote, Info, X } from "lucide-react";
|
|
||||||
import { useIcons } from "@/lib/hooks/use-icons";
|
|
||||||
import { Placeholder } from "@/components/ui/placeholder";
|
|
||||||
|
|
||||||
export function IconsRow({
|
|
||||||
server,
|
|
||||||
mhsfData,
|
|
||||||
}: { server: ServerResponse; mhsfData: ReturnType<typeof useMHSFServer> }) {
|
|
||||||
const { icons } = useIcons();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Material className="p-4 relative h-[250px] max-lg:mt-3">
|
|
||||||
<span className="mb-2">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<strong className="text-lg">Purchased Icons</strong>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Info size={16} className="ml-2" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
Purchased Icons are icons that are under the server's ownership, <br />
|
|
||||||
they may or may not available at that certain moment either.
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator className="my-2" />
|
|
||||||
</span>
|
|
||||||
<div className="p-2 max-h-[180px] overflow-auto">
|
|
||||||
{server?.purchased_icons.length === 0 &&
|
|
||||||
<Placeholder
|
|
||||||
icon={<X />}
|
|
||||||
title="We couldn't find any icons"
|
|
||||||
description="Maybe shake the box harder?"
|
|
||||||
className="mt-4"
|
|
||||||
/>}
|
|
||||||
{server?.purchased_icons.map((icon, i) => (
|
|
||||||
<p
|
|
||||||
key={i}
|
|
||||||
className="pb-4 flex items-center justify-between"
|
|
||||||
style={{
|
|
||||||
color: getIndexFromRarity(
|
|
||||||
icons?.find((c) => c._id === icon)?.rank.toLowerCase(),
|
|
||||||
).text,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<IconDisplay
|
|
||||||
server={{
|
|
||||||
icon: icons?.find((c) => c._id === icon)?.icon_name ?? "",
|
|
||||||
}}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
{icons?.find((c) => c._id === icon)?.display_name}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Info size={18} className="ml-2" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
Just because an item is available, it doesn't directly <br />
|
|
||||||
mean that it can be bought immediately, it just means its in the{" "}
|
|
||||||
<br />
|
|
||||||
pool of icons that are in the weekly rotation.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<span className="flex items-center">
|
|
||||||
<span className="mr-1">Available currently:</span>
|
|
||||||
{icons?.find((c) => c._id === icon)?.available ? "Yes" : "No"}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center">
|
|
||||||
<span className="mr-1">Disabled currently:</span>
|
|
||||||
{icons?.find((c) => c._id === icon)?.disabled ? "Yes" : "No"}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center">
|
|
||||||
<span className="mr-1">Price:</span>
|
|
||||||
<Banknote size={16} className="mr-1" />
|
|
||||||
{icons?.find((c) => c._id === icon)?.price} credits
|
|
||||||
</span>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="mx-2 p-1 pr-2 rounded italic font-bold"
|
|
||||||
style={{
|
|
||||||
backgroundColor: getIndexFromRarity(
|
|
||||||
icons?.find((c) => c._id === icon)?.rank.toLowerCase(),
|
|
||||||
).bg,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icons?.find((c) => c._id === icon)?.rank.toLocaleUpperCase()}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Material>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -39,13 +39,22 @@ import { Material } from "@/components/ui/material";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
import { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Ellipsis, EllipsisVertical, Shuffle } from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { RearrangeDrawer } from "../rearrange/rearrange-drawer";
|
||||||
|
|
||||||
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("motd");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Material className="p-4 relative h-[250px]">
|
<Material className="p-4 relative h-[250px]">
|
||||||
@ -109,8 +118,8 @@ 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] dark:prose-invert">
|
||||||
<Markdown className="min-w-full">{mhsfData.server?.customizationData.description}</Markdown>
|
<Markdown>{mhsfData.server?.customizationData.description}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Material>
|
</Material>
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
||||||
import { Drawer, DrawerContent, DrawerTitle } from "@/components/ui/drawer";
|
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export function ServerEditorProvider({children}: {children: ReactNode | ReactNode[]}) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener("open-server-editor", () => setOpen(true));
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return <>
|
|
||||||
{children}
|
|
||||||
<Drawer open={open} onOpenChange={setOpen}>
|
|
||||||
<DrawerContent>
|
|
||||||
<DrawerTitle>Server Settings</DrawerTitle>
|
|
||||||
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
@ -31,7 +31,7 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ServerResponse } from "@/lib/types/mh-server";
|
import { ServerResponse } from "@/lib/types/mh-server";
|
||||||
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
||||||
import { EllipsisVertical, Flag, Heart, Share, Star } from "lucide-react";
|
import { EllipsisVertical, Flag, Heart, Star } from "lucide-react";
|
||||||
import { useFavoriteStore } from "@/lib/hooks/use-favorite-store";
|
import { useFavoriteStore } from "@/lib/hooks/use-favorite-store";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||||
@ -104,12 +104,9 @@ export function ServerPageButtons({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuSeparator>Server</DropdownMenuSeparator>
|
<DropdownMenuSeparator>
|
||||||
<DropdownMenuItem className="flex items-center gap-2">
|
Destructive
|
||||||
<Share size={16} />
|
</DropdownMenuSeparator>
|
||||||
Share
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator>Destructive</DropdownMenuSeparator>
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-red-400 flex items-center gap-2"
|
className="text-red-400 flex items-center gap-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DebugProvider } from "./debug/debug-provider";
|
import { DebugProvider } from "./debug/debug-provider";
|
||||||
import { ReportingProvider } from "./reporting/reporting-provider";
|
import { ReportingProvider } from "./reporting/reporting-provider";
|
||||||
import { ServerEditorProvider } from "./server-editor/server-editor-provider";
|
|
||||||
|
|
||||||
export function ServerProvider({ serverId }: { serverId: string }) {
|
export function ServerProvider({ serverId }: { serverId: string }) {
|
||||||
const { server, error, loading } = useServer({ id: serverId });
|
const { server, error, loading } = useServer({ id: serverId });
|
||||||
@ -72,14 +71,9 @@ export function ServerProvider({ serverId }: { serverId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="px-10">
|
<div className="px-10">
|
||||||
<ServerEditorProvider>
|
|
||||||
<ReportingProvider server={mhsf}>
|
<ReportingProvider server={mhsf}>
|
||||||
<ServerMainPage
|
<ServerMainPage server={server as ServerResponse} mhsfData={mhsf} />
|
||||||
server={server as ServerResponse}
|
|
||||||
mhsfData={mhsf}
|
|
||||||
/>
|
|
||||||
</ReportingProvider>
|
</ReportingProvider>
|
||||||
</ServerEditorProvider>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DebugProvider>
|
</DebugProvider>
|
||||||
|
|||||||
@ -33,25 +33,14 @@ import useClipboard from "@/lib/useClipboard";
|
|||||||
import { MOTDRow } from "./motd/motd-row";
|
import { MOTDRow } from "./motd/motd-row";
|
||||||
import { StatisticsMainRow } from "./stats/stats-main-row";
|
import { StatisticsMainRow } from "./stats/stats-main-row";
|
||||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||||
import { GeneralInfo } from "./general-info/general-info";
|
|
||||||
import { AchievementsView } from "./achievements/achievements";
|
|
||||||
import { IconsRow } from "./icons/icons-row";
|
|
||||||
import { affiliates } from "./util";
|
|
||||||
import { AffiliateRow } from "./afilliate/affilliate-row";
|
|
||||||
import { EmbedCreatorRow } from "./embeds/embed-creator";
|
|
||||||
|
|
||||||
export function ServerRows({ server, mhsfData }: { server: ServerResponse, mhsfData: ReturnType<typeof useMHSFServer> }) {
|
export function ServerRows({ server, mhsfData }: { server: ServerResponse, mhsfData: ReturnType<typeof useMHSFServer> }) {
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="lg:grid lg:grid-cols-2 w-full gap-3">
|
<span className="lg:grid lg:grid-cols-2 w-full gap-3">
|
||||||
{affiliates.includes(server.name) && <AffiliateRow />}
|
|
||||||
<MOTDRow server={server} mhsfData={mhsfData}/>
|
<MOTDRow server={server} mhsfData={mhsfData}/>
|
||||||
<StatisticsMainRow server={server} mhsfData={mhsfData} />
|
<StatisticsMainRow server={server} mhsfData={mhsfData} />
|
||||||
<GeneralInfo server={server} mhsfData={mhsfData} />
|
|
||||||
<AchievementsView server={server} mhsfData={mhsfData} />
|
|
||||||
<IconsRow server={server} mhsfData={mhsfData} />
|
|
||||||
<EmbedCreatorRow serverName={server.name} />
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -126,8 +126,9 @@ export function StatisticsChart({
|
|||||||
data: any;
|
data: any;
|
||||||
mainDataPoint: string;
|
mainDataPoint: string;
|
||||||
}) {
|
}) {
|
||||||
|
console.log(data);
|
||||||
return (
|
return (
|
||||||
<ChartContainer config={chartConfig} className="max-h-[202px] max-lg:max-h-[177px] min-w-full">
|
<ChartContainer config={chartConfig} className="max-h-[202px] min-w-full">
|
||||||
<AreaChart
|
<AreaChart
|
||||||
accessibilityLayer
|
accessibilityLayer
|
||||||
data={data.slice(data.length - 30, data.length)}
|
data={data.slice(data.length - 30, data.length)}
|
||||||
|
|||||||
@ -40,8 +40,6 @@ export function convert(value: number) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const affiliates = ["CoreBoxx"]
|
|
||||||
|
|
||||||
export const loadingList = [
|
export const loadingList = [
|
||||||
"Making gamer's safer",
|
"Making gamer's safer",
|
||||||
"Finding why Apple is so expensive",
|
"Finding why Apple is so expensive",
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export function StatusButton() {
|
|||||||
if (loading) return <Spinner />;
|
if (loading) return <Spinner />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`https://${statusURL as string}`} noextraicons target="_blank">
|
<Link href={`https://${statusURL as string}`} noExtraIcons target="_blank">
|
||||||
<Button variant="secondary" className="rounded-xl">
|
<Button variant="secondary" className="rounded-xl">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import {
|
|
||||||
SignedIn,
|
|
||||||
SignedOut,
|
|
||||||
useReverification,
|
|
||||||
useUser,
|
|
||||||
} from "@clerk/nextjs";
|
|
||||||
import type { CreateExternalAccountParams, OAuthStrategy } from "@clerk/types";
|
|
||||||
import { UserInformation } from "./waitlist-page";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
import { useEffectOnce } from "@/lib/useEffectOnce";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useRouter } from "@/lib/useRouter";
|
|
||||||
|
|
||||||
export function WaitlistDiscordNeeded() {
|
|
||||||
return (
|
|
||||||
<div className="px-3 lg:px-32 pt-24">
|
|
||||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
|
||||||
Discord link required
|
|
||||||
</h1>
|
|
||||||
<p className="mb-3">
|
|
||||||
You are using an MHSF account that hasn't been linked w/ Discord. We{" "}
|
|
||||||
<br />
|
|
||||||
need to ensure you are in the beta, and need you to link your Discord to{" "}
|
|
||||||
<br />
|
|
||||||
verify your identity.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<SignedIn>
|
|
||||||
<Material className="mb-2">
|
|
||||||
<UserInformation discordPage />
|
|
||||||
</Material>
|
|
||||||
</SignedIn>
|
|
||||||
|
|
||||||
<Material>
|
|
||||||
<SignedOut>You're signed out.</SignedOut>
|
|
||||||
<SignedIn>
|
|
||||||
<SignedInBoundary />
|
|
||||||
</SignedIn>
|
|
||||||
</Material>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SignedInBoundary() {
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { user } = useUser();
|
|
||||||
const createExternalAccount = useReverification(
|
|
||||||
(params: CreateExternalAccountParams) =>
|
|
||||||
user?.createExternalAccount(params),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffectOnce(() => {
|
|
||||||
(async () => {
|
|
||||||
const user = await fetch("/api/v1/user/waitlist/get-discord-details");
|
|
||||||
|
|
||||||
if (user.status !== 200) setLoading(false);
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading) return <Spinner />;
|
|
||||||
|
|
||||||
const addDiscord = async () => {
|
|
||||||
await createExternalAccount({
|
|
||||||
strategy: "oauth_discord",
|
|
||||||
redirectUrl: "/waitlist",
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res?.verification?.externalVerificationRedirectURL) {
|
|
||||||
router.push(res.verification.externalVerificationRedirectURL.href);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("ERROR", err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button onClick={() => addDiscord()}>Link your Discord account</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Alert } from "@/components/ui/alert";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
import { useEffectOnce } from "@/lib/useEffectOnce";
|
|
||||||
import { useRouter } from "@/lib/useRouter";
|
|
||||||
import type { DiscordUser } from "@/pages/api/v1/user/waitlist/check-waitlist-eligibility";
|
|
||||||
import { SignedIn, SignedOut, useClerk, useUser } from "@clerk/nextjs";
|
|
||||||
import { CalendarArrowDown } from "lucide-react";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { WaitlistSuccessDialog } from "./waitlist-success";
|
|
||||||
|
|
||||||
export function WaitlistPage() {
|
|
||||||
const clerk = useClerk();
|
|
||||||
const { user } = useUser();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="px-3 lg:px-32 pt-24">
|
|
||||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
|
||||||
v2 private beta
|
|
||||||
</h1>
|
|
||||||
<p className="mb-3">
|
|
||||||
Hello there! MHSF has an exclusive beta that you may have been invited{" "}
|
|
||||||
<br /> to. Please sign into your account below or follow the
|
|
||||||
instructions.
|
|
||||||
</p>
|
|
||||||
<SignedIn>
|
|
||||||
<Material className="mb-2">
|
|
||||||
<UserInformation />
|
|
||||||
</Material>
|
|
||||||
</SignedIn>
|
|
||||||
<Material>
|
|
||||||
<SignedOut>
|
|
||||||
<p>
|
|
||||||
You must be signed in to check for eligibility for this beta. Please
|
|
||||||
make sure you use the Discord connection so we can check if you
|
|
||||||
eligibile for the beta.
|
|
||||||
</p>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<Button onClick={() => clerk.openSignIn()} variant="secondary">
|
|
||||||
Sign-in
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => clerk.openSignUp()}>Sign-up</Button>
|
|
||||||
</span>
|
|
||||||
</SignedOut>
|
|
||||||
<SignedIn>
|
|
||||||
{user?.publicMetadata.v2allowed !== true ? (
|
|
||||||
<SignedInBoundary />
|
|
||||||
) : (
|
|
||||||
<Alert variant="normal" className="gap-2">
|
|
||||||
You are already in the v2 beta.
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</SignedIn>
|
|
||||||
</Material>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UserInformation({ discordPage }: { discordPage?: boolean }) {
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [discordData, setDiscordData] = useState<DiscordUser | null>(null);
|
|
||||||
const router = useRouter();
|
|
||||||
const { user } = useUser();
|
|
||||||
|
|
||||||
useEffectOnce(() => {
|
|
||||||
(async () => {
|
|
||||||
const user = await fetch("/api/v1/user/waitlist/get-discord-details");
|
|
||||||
const json = await user.json();
|
|
||||||
|
|
||||||
if (user.status !== 200 && !discordPage) {
|
|
||||||
router.push("/waitlist/oauth-need-discord");
|
|
||||||
} else {
|
|
||||||
setDiscordData(json.discordData as DiscordUser);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading) return <Spinner />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-2 text-sm">
|
|
||||||
<Image
|
|
||||||
alt="Clerk Image"
|
|
||||||
src={
|
|
||||||
user?.imageUrl === undefined
|
|
||||||
? "https://img.clerk.com/preview.png?size=144&seed=seed&initials=AD&isSquare=true&bgType=marble&bgColor=6c47ff&fgType=silhouette&fgColor=FFFFFF&type=user&w=48&q=75"
|
|
||||||
: user?.imageUrl
|
|
||||||
}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className="rounded-full"
|
|
||||||
/>
|
|
||||||
<span className="block">
|
|
||||||
<p>Signed in as @{user?.username}</p>
|
|
||||||
|
|
||||||
{discordData !== undefined && discordData !== null && (
|
|
||||||
<p className="group cursor-pointer flex items-center gap-1">
|
|
||||||
Discord linked as {discordData.global_name}
|
|
||||||
<span className="text-muted-foreground hidden group-hover:block">
|
|
||||||
@{discordData.username}
|
|
||||||
</span>
|
|
||||||
{discordData.clan.identity_enabled === true && (
|
|
||||||
<Badge className="flex items-center">
|
|
||||||
<Image
|
|
||||||
src={`https://cdn.discordapp.com/clan-badges/${discordData.clan.identity_guild_id}/${discordData.clan.badge}.png?size=16`}
|
|
||||||
alt="clan tag bg"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
{discordData.clan.tag}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SignedInBoundary() {
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [text, setText] = useState("");
|
|
||||||
const [id, setId] = useState("");
|
|
||||||
const router = useRouter();
|
|
||||||
const [success, setSuccess] = useState(false);
|
|
||||||
|
|
||||||
useEffectOnce(() => {
|
|
||||||
(async () => {
|
|
||||||
const eligible = await fetch(
|
|
||||||
"/api/v1/user/waitlist/check-waitlist-eligibility",
|
|
||||||
);
|
|
||||||
const json = await eligible.json();
|
|
||||||
const status = eligible.status;
|
|
||||||
|
|
||||||
setText(json.message);
|
|
||||||
|
|
||||||
if (status === 200) {
|
|
||||||
setId(json.refUUID);
|
|
||||||
setSuccess(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading) return <Spinner />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p>
|
|
||||||
{text} <WaitlistSuccessDialog open={success} setOpen={() => true} uuid={id} />
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
/*
|
|
||||||
* MHSF, Minehut Server List
|
|
||||||
* All external content is rather licensed under the ECA Agreement
|
|
||||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
|
||||||
*
|
|
||||||
* All code under MHSF is licensed under the MIT License
|
|
||||||
* by open source contributors
|
|
||||||
*
|
|
||||||
* Copyright (c) 2025 dvelo
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to
|
|
||||||
* deal in the Software without restriction, including without limitation the
|
|
||||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
||||||
* sell copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Material } from "@/components/ui/material";
|
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
|
||||||
import { useEffectOnce } from "@/lib/useEffectOnce";
|
|
||||||
import { useRouter } from "@/lib/useRouter";
|
|
||||||
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
|
||||||
import { useQueryState } from "nuqs";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { WaitlistSuccessDialog } from "./waitlist-success";
|
|
||||||
|
|
||||||
export function WaitlistReferralBeta() {
|
|
||||||
const [id] = useQueryState("id", { defaultValue: "" });
|
|
||||||
const clerk = useClerk();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="px-3 lg:px-32 pt-24">
|
|
||||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
|
||||||
v2 private beta
|
|
||||||
</h1>
|
|
||||||
<p className="mb-3">
|
|
||||||
Hello there! MHSF has an exclusive beta that you may have been invited{" "}
|
|
||||||
<br /> to. Please sign into your account below or follow the
|
|
||||||
instructions.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Material>
|
|
||||||
<SignedIn>
|
|
||||||
<LoggedInBoundary id={id} />
|
|
||||||
</SignedIn>
|
|
||||||
<SignedOut>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="secondary" onClick={() => clerk.openSignIn()}>Sign In</Button>
|
|
||||||
<Button onClick={() => clerk.openSignUp()}>Sign Up</Button>
|
|
||||||
</div>
|
|
||||||
</SignedOut>
|
|
||||||
</Material>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LoggedInBoundary({ id }: { id: string }) {
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [success, setSuccess] = useState(false);
|
|
||||||
const [text, setText] = useState("");
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffectOnce(() => {
|
|
||||||
(async () => {
|
|
||||||
const refEligibility = await fetch(
|
|
||||||
"/api/v1/user/waitlist/ref-waitlist-eligibility",
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ id }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const status = refEligibility.status;
|
|
||||||
const json = await refEligibility.json();
|
|
||||||
|
|
||||||
if (status === 200) {
|
|
||||||
setSuccess(true);
|
|
||||||
} else {
|
|
||||||
setText(json.message);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading) return <Spinner />;
|
|
||||||
|
|
||||||
return <div>{!success && <>{text}</>} <WaitlistSuccessDialog open={success} setOpen={() => true} /></div>;
|
|
||||||
}
|
|
||||||
@ -1,90 +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 { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Link } from "@/components/util/link";
|
|
||||||
import useClipboard from "@/lib/useClipboard";
|
|
||||||
import { useRouter } from "@/lib/useRouter";
|
|
||||||
import { Copy } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export function WaitlistSuccessDialog({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
uuid,
|
|
||||||
}: { open: boolean; setOpen: (c: boolean) => void; uuid?: string }) {
|
|
||||||
const clipboard = useClipboard();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogTitle>You are eligibile!</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
You have been invited into the v2 private beta!{" "}
|
|
||||||
{uuid && (
|
|
||||||
<>
|
|
||||||
You may also invite up to two (2) people with a special link
|
|
||||||
below. <strong>You will only see this link once.</strong>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
|
||||||
{uuid && (
|
|
||||||
<span className="flex items-center">
|
|
||||||
<p>https://mhsf.app/waitlist/ref?id={uuid}</p>
|
|
||||||
<Button
|
|
||||||
size="square-md"
|
|
||||||
onClick={() => {
|
|
||||||
clipboard.writeText(`https://mhsf.app/waitlist/ref?id=${uuid}`);
|
|
||||||
toast.success("Copied!");
|
|
||||||
}}
|
|
||||||
className="flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<Copy size={16} />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogTrigger>
|
|
||||||
<Button onClick={() => router.push('/')}>Go home</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -61,7 +61,7 @@ const badgeVariants = cva(
|
|||||||
"purple-subtle": `bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400
|
"purple-subtle": `bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-400
|
||||||
ring-purple-400 dark:ring-purple-500/30`,
|
ring-purple-400 dark:ring-purple-500/30`,
|
||||||
rainbow:
|
rainbow:
|
||||||
"text-white ring-transparent z-10 bg-blur-[15px] rainbow-tag",
|
"text-white ring-transparent z-10 [background:_linear-gradient(45deg,rgba(255,_0,_0,_1)_0%,rgba(255,_154,_0,_1)_10%,rgba(208,_222,_33,_1)_20%,rgba(79,_220,_74,_1)_30%,rgba(63,_218,_216,_1)_40%,rgba(47,_201,_226,_1)_50%,rgba(28,_127,_238,_1)_60%,rgba(95,_21,_242,_1)_70%,rgba(186,_12,_248,_1)_80%,rgba(251,_7,_217,_1)_90%,rgba(255,_0,_0,_1)_100%);] backdrop-blur-sm opacity-60 ",
|
||||||
custom: "",
|
custom: "",
|
||||||
},
|
},
|
||||||
allowIconOnly: {
|
allowIconOnly: {
|
||||||
|
|||||||
@ -28,7 +28,6 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
function Placeholder({
|
function Placeholder({
|
||||||
@ -36,16 +35,14 @@ function Placeholder({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
children,
|
children,
|
||||||
className
|
|
||||||
}: {
|
}: {
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
title?: ReactNode | string;
|
title?: ReactNode | string;
|
||||||
description?: ReactNode | string;
|
description?: ReactNode | string;
|
||||||
className?: string;
|
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={cn("text-slate-700 dark:text-zinc-300 flex flex-col justify-center items-center gap-2", className)}>
|
<div className="text-slate-700 dark:text-zinc-300 flex flex-col justify-center items-center gap-2">
|
||||||
{icon && (
|
{icon && (
|
||||||
<div className="border border-slate-200 dark:border-zinc-700 dark:bg-zinc-800 p-3 rounded-full">
|
<div className="border border-slate-200 dark:border-zinc-700 dark:bg-zinc-800 p-3 rounded-full">
|
||||||
{icon}
|
{icon}
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const overflowXHiddenPages = ["/home"];
|
|||||||
|
|
||||||
export function FontBoundary({
|
export function FontBoundary({
|
||||||
children,
|
children,
|
||||||
className,
|
className
|
||||||
}: {
|
}: {
|
||||||
children?: ReactNode | ReactNode[];
|
children?: ReactNode | ReactNode[];
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -60,32 +60,8 @@ export function FontBoundary({
|
|||||||
});
|
});
|
||||||
}, [settingsStore]);
|
}, [settingsStore]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const classes = [
|
|
||||||
`font-${fontFamily}`,
|
|
||||||
(() => {
|
|
||||||
switch (fontFamily) {
|
|
||||||
case "geist-sans":
|
|
||||||
return GeistSans.className;
|
|
||||||
case "roboto":
|
|
||||||
return roboto.className;
|
|
||||||
case "inter":
|
|
||||||
return inter.className;
|
|
||||||
default:
|
|
||||||
return "system-ui-font--font-boundary";
|
|
||||||
}
|
|
||||||
})() as string,
|
|
||||||
"overflow-x-hidden",
|
|
||||||
className,
|
|
||||||
] as string[];
|
|
||||||
|
|
||||||
document.body.classList.add(...classes);
|
|
||||||
|
|
||||||
return () => document.body.classList.remove(...classes)
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<body
|
||||||
className={`font-${fontFamily} ${(() => {
|
className={`font-${fontFamily} ${(() => {
|
||||||
switch (fontFamily) {
|
switch (fontFamily) {
|
||||||
case "geist-sans":
|
case "geist-sans":
|
||||||
@ -97,9 +73,9 @@ export function FontBoundary({
|
|||||||
default:
|
default:
|
||||||
return "system-ui-font--font-boundary";
|
return "system-ui-font--font-boundary";
|
||||||
}
|
}
|
||||||
})()} overflow-x-hidden ${className}`}
|
})()} ${pathname !== null && overflowXHiddenPages.includes(pathname) ? "overflow-x-hidden" : ""} ${className}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</body>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export function Link(
|
|||||||
props: LinkProps & {
|
props: LinkProps & {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
noextraicons?: boolean;
|
noExtraIcons?: boolean;
|
||||||
target?: string;
|
target?: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@ -43,7 +43,7 @@ export function Link(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NextLink {...props} href={pageFind(href || "") || "#"} title={href}>
|
<NextLink {...props} href={pageFind(href || "") || "#"} title={href}>
|
||||||
{!props.noextraicons && (
|
{!props.noExtraIcons && (
|
||||||
<>
|
<>
|
||||||
{(href || "").startsWith("Docs:") && (
|
{(href || "").startsWith("Docs:") && (
|
||||||
<Book size={16} className="mr-[2px] inline-flex" />
|
<Book size={16} className="mr-[2px] inline-flex" />
|
||||||
@ -56,7 +56,7 @@ export function Link(
|
|||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
||||||
{!props.noextraicons && (href || "").startsWith("https") && (
|
{!props.noExtraIcons && (href || "").startsWith("https") && (
|
||||||
<ExternalLink size={12} className="ml-[2px] mb-[3px] inline-flex" />
|
<ExternalLink size={12} className="ml-[2px] mb-[3px] inline-flex" />
|
||||||
)}
|
)}
|
||||||
</NextLink>
|
</NextLink>
|
||||||
|
|||||||
@ -28,8 +28,6 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { Placeholder } from "../ui/placeholder";
|
import { Placeholder } from "../ui/placeholder";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|||||||
@ -28,8 +28,15 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { WaitlistDiscordNeeded } from "@/components/feat/waitlist/waitlist-discord-needed";
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
export default function OAuthNeedDiscord() {
|
export const affiliates: {
|
||||||
return <WaitlistDiscordNeeded />
|
name: string;
|
||||||
}
|
shop: string;
|
||||||
|
line: string;
|
||||||
|
mode: string[];
|
||||||
|
otherLinks: {
|
||||||
|
name: string;
|
||||||
|
icon: (props: SVGProps<SVGSVGElement>) => JSX.Element;
|
||||||
|
}[];
|
||||||
|
}[] = [];
|
||||||
93
apps/www/src/config/banners.tsx
Normal file
93
apps/www/src/config/banners.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* 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 GradientBanner from "@/components/effects/gradient-banner";
|
||||||
|
import MainBanner from "@/components/feat/MainBanner";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { DialogContent, Dialog } from "@/components/ui/dialog";
|
||||||
|
import AffiliatePopup from "@/components/misc/AffiliatePopup";
|
||||||
|
import { Gradient } from "stripe-gradient";
|
||||||
|
import {useRouter} from "@/lib/useRouter";
|
||||||
|
import {pageFind} from "@/components/misc/Link"
|
||||||
|
|
||||||
|
export const defaultBanners: {
|
||||||
|
bannerSpace: number;
|
||||||
|
bannerContent: React.ReactNode;
|
||||||
|
}[] = [
|
||||||
|
// The affilation banner ALWAYS has to be first.
|
||||||
|
{
|
||||||
|
bannerSpace: 1,
|
||||||
|
bannerContent: (
|
||||||
|
<>
|
||||||
|
<AffiliateBanner />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function AffiliateBanner() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div onClick={() => router.push(pageFind("Special:GitHub/releases/tag/1.8.0") as string)} className="cursor-pointer">
|
||||||
|
<MainBanner size={1} className="max-h-[2rem] border-0">
|
||||||
|
<GradientBanner>
|
||||||
|
<strong>v2</strong>: the future of MHSF
|
||||||
|
</GradientBanner>
|
||||||
|
</MainBanner>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bannerHooks: (() =>
|
||||||
|
| { bannerSpace: number; bannerContent: React.ReactNode }
|
||||||
|
| undefined)[] = [
|
||||||
|
() => {
|
||||||
|
if (process.env.NEXT_PUBLIC_VERCEL_ENV !== "production")
|
||||||
|
return {
|
||||||
|
bannerSpace: 1,
|
||||||
|
bannerContent: (
|
||||||
|
<MainBanner className="bg-orange-600">
|
||||||
|
Your not in production!{" "}
|
||||||
|
<Link href="https://mhsf.app">
|
||||||
|
<Button variant="link" className="dark:text-black">
|
||||||
|
Go to production
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</MainBanner>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -28,40 +28,61 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
export const allFolders: DocsFolder[] = [
|
||||||
|
{
|
||||||
|
name: "General",
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
title: "Getting Started",
|
||||||
|
url: "/docs/getting-started",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Reading",
|
||||||
|
url: "/docs/reading",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Guides",
|
||||||
|
docs: [
|
||||||
|
{
|
||||||
|
title: "Linking",
|
||||||
|
url: "/docs/guides/linking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Owning a Server",
|
||||||
|
url: "/docs/guides/owning-a-server",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Server Customization",
|
||||||
|
url: "/docs/guides/customization",
|
||||||
|
},
|
||||||
|
{ title: "Reporting a server", url: "/docs/guides/reporting-server" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Advanced",
|
||||||
|
docs: [
|
||||||
|
{ title: "Tech Stack", url: "/docs/advanced/tech-stack" },
|
||||||
|
{ title: "Using the Command-bar", url: "/docs/advanced/command-bar" },
|
||||||
|
{ title: "Tips with external servers", url: "/docs/advanced/external" },
|
||||||
|
{ title: "Achievements", url: "/docs/advanced/achievements" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Legal",
|
||||||
|
docs: [
|
||||||
|
{ title: "ECA Agreement", url: "/docs/legal/external-content-agreement" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
export type Docs = {
|
||||||
import type { OnlineServer } from "@/lib/types/mh-server";
|
title: string;
|
||||||
import { type ReactNode, useEffect, useState } from "react";
|
url: string;
|
||||||
import { MOTDRenderer } from "../server-page/motd/motd-renderer";
|
};
|
||||||
import IconDisplay from "../icons/minecraft-icon-display";
|
|
||||||
import ServerCard, { TagShower } from "./server-card";
|
|
||||||
|
|
||||||
export function ServerRandomServerProvider({
|
export type DocsFolder = {
|
||||||
servers,
|
name: string;
|
||||||
children,
|
docs: Array<Docs>;
|
||||||
}: { servers: OnlineServer[]; children: ReactNode | ReactNode[] }) {
|
};
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [selectedServer, setSelectedServer] = useState<OnlineServer | null>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (servers.length !== 0)
|
|
||||||
window.addEventListener("open-random-server", () => {
|
|
||||||
setSelectedServer(servers[Math.floor(Math.random() * servers.length)]);
|
|
||||||
setOpen(true);
|
|
||||||
});
|
|
||||||
}, [servers]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
{selectedServer !== null && selectedServer !== undefined && (
|
|
||||||
<ServerCard server={selectedServer}/>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -30,22 +30,13 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ArrowDownUpIcon,
|
ArrowDownUpIcon,
|
||||||
Database,
|
|
||||||
DatabaseZap,
|
|
||||||
HardDriveDownload,
|
|
||||||
List,
|
|
||||||
ServerCog,
|
ServerCog,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
UserPlus,
|
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { type Filter } from "../lib/types/filter";
|
import { type Filter } from "../lib/types/filter";
|
||||||
import type { Sort } from "../lib/types/sort";
|
import type { Sort } from "../lib/types/sort";
|
||||||
import { TagFilter } from "@/lib/types/filters/tag-filter";
|
import { TagFilter } from "@/lib/types/filters/tag-filter";
|
||||||
import { allCategories } from "./tags";
|
|
||||||
import { CategoryFilter } from "@/lib/types/filters/category-filter";
|
|
||||||
import { PlayerRangeFilter } from "@/lib/types/filters/player-range-filter";
|
|
||||||
import { CombinationFilter } from "@/lib/types/filters/combination-filter";
|
|
||||||
|
|
||||||
type ModDBCategory = {
|
type ModDBCategory = {
|
||||||
displayTitle: string;
|
displayTitle: string;
|
||||||
@ -63,7 +54,8 @@ type ModDBCategory = {
|
|||||||
export const serverModDB: ModDBCategory[] = [
|
export const serverModDB: ModDBCategory[] = [
|
||||||
{
|
{
|
||||||
displayTitle: "Create Custom Files",
|
displayTitle: "Create Custom Files",
|
||||||
description: `Create custom TypeScript-based filter or sorting systems, completely from the comfort of your own browser.
|
description:
|
||||||
|
`Create custom TypeScript-based filter or sorting systems, completely from the comfort of your own browser.
|
||||||
Types used are *builtin* and you can see live type definitions and IntelliSense in the editor.`,
|
Types used are *builtin* and you can see live type definitions and IntelliSense in the editor.`,
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
@ -71,91 +63,35 @@ export const serverModDB: ModDBCategory[] = [
|
|||||||
icon: ArrowDownUpIcon,
|
icon: ArrowDownUpIcon,
|
||||||
value: { customAction: "custom-sort" },
|
value: { customAction: "custom-sort" },
|
||||||
color: "#a3a68b",
|
color: "#a3a68b",
|
||||||
description:
|
description: "Create a new custom sort system using TypeScript, completely from the comfort of your own browser."
|
||||||
"Create a new custom sort system using TypeScript, completely from the comfort of your own browser.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Create Filter",
|
name: "Create Filter",
|
||||||
icon: SlidersHorizontal,
|
icon: SlidersHorizontal,
|
||||||
value: { customAction: "custom-filter" },
|
value: { customAction: "custom-filter" },
|
||||||
color: "#a3a68b",
|
color: "#a3a68b",
|
||||||
description:
|
description: "Create a new custom filtering system using TypeScript, completely from the comfort of your own browser."
|
||||||
"Create a new custom filtering system using TypeScript, completely from the comfort of your own browser.",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayTitle: "Custom Files",
|
displayTitle: "Custom Files",
|
||||||
description:
|
description: "These are all of your activated modifications made in the editor.",
|
||||||
"These are all of your activated modifications made in the editor.",
|
|
||||||
__custom: true,
|
__custom: true,
|
||||||
// Entries are already pre-loaded.
|
// Entries are already pre-loaded.
|
||||||
entries: [],
|
entries: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayTitle: "Tag Filters",
|
displayTitle: "Tag Filters",
|
||||||
description:
|
description: "These are filters that are associated with an assortment of tags.",
|
||||||
"These are filters that are associated with an assortment of tags.",
|
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
name: "Always Online",
|
name: "Always Online",
|
||||||
description: "All servers that are always online.",
|
description: "All servers that are always online.",
|
||||||
color: "#a380e0",
|
color: "#a380e0",
|
||||||
value: new TagFilter(3, false),
|
value: new TagFilter(2, false),
|
||||||
icon: ServerCog,
|
icon: ServerCog
|
||||||
},
|
}
|
||||||
{
|
]
|
||||||
name: "Minehut-hosted",
|
}
|
||||||
description:
|
|
||||||
"Only show servers hosted directly on Minehut's infrastructure, and not external servers.",
|
|
||||||
color: "#fce2e2",
|
|
||||||
value: new TagFilter(0, true),
|
|
||||||
icon: HardDriveDownload,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "No player-less servers",
|
|
||||||
description: "Exclude servers that have no players",
|
|
||||||
color: "#66c219",
|
|
||||||
value: new TagFilter(2, true),
|
|
||||||
icon: UserPlus,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayTitle: "Categories",
|
|
||||||
description: "Sort servers by the categories they directly are in.",
|
|
||||||
entries: allCategories.map((c, i) => {
|
|
||||||
return {
|
|
||||||
color: "#eae9e9",
|
|
||||||
value: new CategoryFilter(i),
|
|
||||||
name: c.name,
|
|
||||||
description: `Filter all servers that are have the category '${c.name}'`,
|
|
||||||
icon: List
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayTitle: "Legacy Player Range Filters",
|
|
||||||
description: "Use old player range filters from MHSF v0-v1.",
|
|
||||||
entries: [
|
|
||||||
{
|
|
||||||
color: "#ceaced",
|
|
||||||
value: new CombinationFilter([
|
|
||||||
new PlayerRangeFilter(7, 15),
|
|
||||||
new TagFilter(3, true),
|
|
||||||
]),
|
|
||||||
name: "Only allow smaller servers",
|
|
||||||
description:
|
|
||||||
"Only allow servers that have the player range 7-15, and cannot be Always Online.",
|
|
||||||
icon: Database,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
color: "#ceaced",
|
|
||||||
value: new PlayerRangeFilter(15, null),
|
|
||||||
name: "Only allow bigger servers",
|
|
||||||
description: "Only allow servers with more than 15 players",
|
|
||||||
icon: DatabaseZap,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -29,10 +29,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { BadgeColor } from "@/components/feat/server-list/server-card";
|
import type { BadgeColor } from "@/components/feat/server-list/server-card";
|
||||||
import { affiliates } from "@/components/feat/server-page/util";
|
|
||||||
import { MHSFData } from "@/lib/types/data";
|
import { MHSFData } from "@/lib/types/data";
|
||||||
import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
||||||
import { Cake, HardDriveDownload, ServerCog } from "lucide-react";
|
import { Cake, ServerCog } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
const serverCache: any = {};
|
const serverCache: any = {};
|
||||||
@ -66,30 +65,6 @@ export const allTags: Array<{
|
|||||||
__disab?: boolean;
|
__disab?: boolean;
|
||||||
__filter?: boolean;
|
__filter?: boolean;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
|
||||||
name: async (s) => (
|
|
||||||
<span>
|
|
||||||
<HardDriveDownload size={16} />
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
tooltipDesc: "This tag represents that the server is externally hosted.",
|
|
||||||
docsName: "External",
|
|
||||||
htmlDocs:
|
|
||||||
"If a server is externally hosted, this tag appears. This can also be seen in the server plan.",
|
|
||||||
condition: async (s) => {
|
|
||||||
return (
|
|
||||||
(s.online !== undefined
|
|
||||||
? s.online.staticInfo.serverPlan
|
|
||||||
: (s.server?.server_plan ?? "")
|
|
||||||
)
|
|
||||||
.split(" ")[0]
|
|
||||||
.split("_")[0]
|
|
||||||
.toLocaleLowerCase() === "external"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
role: "yellow-subtle",
|
|
||||||
__filter: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: async (c) => (
|
name: async (c) => (
|
||||||
<>
|
<>
|
||||||
@ -104,7 +79,7 @@ export const allTags: Array<{
|
|||||||
{String(
|
{String(
|
||||||
c.online === undefined
|
c.online === undefined
|
||||||
? c.server?.playerCount
|
? c.server?.playerCount
|
||||||
: c.online.playerData.playerCount,
|
: c.online.playerData.playerCount
|
||||||
)}{" "}
|
)}{" "}
|
||||||
online
|
online
|
||||||
</>
|
</>
|
||||||
@ -137,9 +112,8 @@ export const allTags: Array<{
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
condition: async (c) =>
|
condition: async (c) =>
|
||||||
(c.online === undefined
|
(c.online === undefined ? c.server?.playerCount: c.online.playerData.playerCount) ===
|
||||||
? c.server?.playerCount
|
0,
|
||||||
: c.online.playerData.playerCount) === 0,
|
|
||||||
htmlDocs: "Nobody is online this server.",
|
htmlDocs: "Nobody is online this server.",
|
||||||
tooltipDesc: "Nobody is online this server.",
|
tooltipDesc: "Nobody is online this server.",
|
||||||
role: "gray-subtle",
|
role: "gray-subtle",
|
||||||
@ -186,7 +160,7 @@ export const allTags: Array<{
|
|||||||
{
|
{
|
||||||
name: async () => "Partner",
|
name: async () => "Partner",
|
||||||
condition: async (s) =>
|
condition: async (s) =>
|
||||||
affiliates.includes((s.server ?? s.online ?? { name: "" }).name),
|
(s.server ?? s.online ?? { name: "" }).name === "CoreBoxx",
|
||||||
tooltipDesc: "This server is a partner with MHSF.",
|
tooltipDesc: "This server is a partner with MHSF.",
|
||||||
docsName: "Partner",
|
docsName: "Partner",
|
||||||
htmlDocs: "This tag represents that this server is a partner with MHSF.",
|
htmlDocs: "This tag represents that this server is a partner with MHSF.",
|
||||||
@ -398,7 +372,7 @@ export const allCategories: Array<{
|
|||||||
async function requestServer(s: OnlineServer): Promise<ServerResponse> {
|
async function requestServer(s: OnlineServer): Promise<ServerResponse> {
|
||||||
if (serverCache[s.name] === undefined) {
|
if (serverCache[s.name] === undefined) {
|
||||||
const re = await fetch(
|
const re = await fetch(
|
||||||
"https://api.minehut.com/server/" + s.name + "?byName=true",
|
"https://api.minehut.com/server/" + s.name + "?byName=true"
|
||||||
);
|
);
|
||||||
const json = await re.json();
|
const json = await re.json();
|
||||||
serverCache[s.name] = json.server;
|
serverCache[s.name] = json.server;
|
||||||
|
|||||||
@ -67,6 +67,7 @@ export async function getBackendProcedure(request: NextApiRequest): Promise<Back
|
|||||||
|
|
||||||
if (detectedIp !== null) {
|
if (detectedIp !== null) {
|
||||||
const collection = defaultDatabase.collection("blocked-ips");
|
const collection = defaultDatabase.collection("blocked-ips");
|
||||||
|
console.log(await collection.findOne({ ip: detectedIp }), detectedIp)
|
||||||
|
|
||||||
if (await collection.findOne({ ip: detectedIp }) !== null) {
|
if (await collection.findOne({ ip: detectedIp }) !== null) {
|
||||||
await mongoClient.close()
|
await mongoClient.close()
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
const JSX_INSERTS = `<iframe
|
|
||||||
src="{{ embed }}"
|
|
||||||
width={390}
|
|
||||||
height={145}
|
|
||||||
style={{ borderRadius: "0.25rem" }}
|
|
||||||
allow="clipboard-write"
|
|
||||||
frameBorder={0}
|
|
||||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
|
||||||
/>`
|
|
||||||
const HTML_INSERTS = `<iframe
|
|
||||||
src="{{ embed }}"
|
|
||||||
width="390"
|
|
||||||
height="145"
|
|
||||||
style="border-radius: 0.25rem;"
|
|
||||||
allow="clipboard-write"
|
|
||||||
frameborder="0"
|
|
||||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
|
|
||||||
></iframe>`
|
|
||||||
|
|
||||||
export function useEmbedGenerator(name: string) {
|
|
||||||
// In parameters
|
|
||||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
|
||||||
const [removeMinehutBranding, setRMHB] = useState<boolean>(false);
|
|
||||||
const [staticMode, setStatic] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// Out parameters
|
|
||||||
const [jsxCode, setJSX] = useState<string>();
|
|
||||||
const [htmlCode, setHTML] = useState<string>();
|
|
||||||
const [finalURL, setFinalURL] = useState<string>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const baseUrl = `${window.location.protocol}//${window.location.host}`;
|
|
||||||
const url = new URL(`/embed/${name}`, baseUrl);
|
|
||||||
|
|
||||||
url.searchParams.set("theme", theme);
|
|
||||||
if (removeMinehutBranding)
|
|
||||||
url.searchParams.set("branding", "false");
|
|
||||||
if (staticMode)
|
|
||||||
url.searchParams.set("static", "true")
|
|
||||||
|
|
||||||
setJSX(JSX_INSERTS.replaceAll("{{ embed }}", url.toString()));
|
|
||||||
setHTML(HTML_INSERTS.replaceAll("{{ embed }}", url.toString()));
|
|
||||||
setFinalURL(url.toString());
|
|
||||||
}, [theme, removeMinehutBranding, staticMode, name])
|
|
||||||
|
|
||||||
return {
|
|
||||||
in: {
|
|
||||||
theme, setTheme,
|
|
||||||
removeMinehutBranding, setRMHB,
|
|
||||||
staticMode, setStatic
|
|
||||||
},
|
|
||||||
out: {
|
|
||||||
jsxCode,
|
|
||||||
htmlCode,
|
|
||||||
finalURL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -37,7 +37,7 @@ import { transpileTypeScript } from "@/app/(sl-modification-frame)/servers/embed
|
|||||||
import { useUser } from "@clerk/nextjs";
|
import { useUser } from "@clerk/nextjs";
|
||||||
import type { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
import type { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||||
import { ClerkEmbeddedFilter } from "@/components/feat/server-list/modification/modification-action";
|
import { ClerkEmbeddedFilter } from "@/components/feat/server-list/modification/modification-action";
|
||||||
import { supportedFilters } from "../types/supportedFilters";
|
import { supportedFilters } from "../types/filter";
|
||||||
|
|
||||||
type EmbeddedFilter = {
|
type EmbeddedFilter = {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
@ -56,7 +56,6 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
const [testModeLoading, setTestModeLoading] = useState(true);
|
const [testModeLoading, setTestModeLoading] = useState(true);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filters, setFilters] = useState<EmbeddedFilter[]>([]);
|
const [filters, setFilters] = useState<EmbeddedFilter[]>([]);
|
||||||
const [tagStrings, setTagStrings] = useState<string[]>([]);
|
|
||||||
const [sort, setSort] = useState<SortFunction<OnlineServer> | null>(null);
|
const [sort, setSort] = useState<SortFunction<OnlineServer> | null>(null);
|
||||||
const { user, isSignedIn } = useUser();
|
const { user, isSignedIn } = useUser();
|
||||||
|
|
||||||
@ -69,6 +68,8 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
);
|
);
|
||||||
const sortedData = sort === null ? resultData : resultData.sort(sort);
|
const sortedData = sort === null ? resultData : resultData.sort(sort);
|
||||||
|
|
||||||
|
console.log({ sortedData, modificationMap, resultData, data, newFilters });
|
||||||
|
|
||||||
if (sortedData.length !== 0) setFilteredData(sortedData);
|
if (sortedData.length !== 0) setFilteredData(sortedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -166,6 +167,11 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
if (type === "sort") {
|
if (type === "sort") {
|
||||||
newServers = data.sort((a, b) => filterFunc(a, b));
|
newServers = data.sort((a, b) => filterFunc(a, b));
|
||||||
setTestModeStatus(`Sorted ${newServers.length} servers.`);
|
setTestModeStatus(`Sorted ${newServers.length} servers.`);
|
||||||
|
console.log(
|
||||||
|
newServers,
|
||||||
|
data.sort((a, b) => filterFunc(a, b)),
|
||||||
|
);
|
||||||
|
console.log(filterFunc);
|
||||||
setFilteredData(() => [...newServers]);
|
setFilteredData(() => [...newServers]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,16 +209,15 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
|
|
||||||
// biome-ignore lint: I'm gonna turn this off :sob:
|
// biome-ignore lint: I'm gonna turn this off :sob:
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("start-loading-server-view", () => setLoading(true))
|
|
||||||
if (!t)
|
if (!t)
|
||||||
window.addEventListener("update-modification-stack", async () => {
|
window.addEventListener("update-modification-stack", async () => {
|
||||||
await user?.reload();
|
await user?.reload();
|
||||||
|
setLoading(true);
|
||||||
let newFilters: EmbeddedFilter[] = [];
|
let newFilters: EmbeddedFilter[] = [];
|
||||||
const filters =
|
const filters =
|
||||||
((isSignedIn ? user.unsafeMetadata.filters : JSON.parse(localStorage.getItem("mhsf__filters") ?? "[]")) as Array<
|
((isSignedIn ? user.unsafeMetadata.filters : JSON.parse(localStorage.getItem("mhsf__filters") ?? "[]")) as Array<
|
||||||
ClerkEmbeddedFilter<unknown>
|
ClerkEmbeddedFilter<unknown>
|
||||||
>) ?? [];
|
>) ?? [];
|
||||||
setTagStrings([]);
|
|
||||||
|
|
||||||
if (isSignedIn) {
|
if (isSignedIn) {
|
||||||
const activatedModifications =
|
const activatedModifications =
|
||||||
@ -304,13 +309,15 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
newFilters.push({
|
newFilters.push({
|
||||||
identifier: filterType?.ns + (Math.random() * Math.random() * Math.random()).toString(),
|
identifier: filterType?.ns + (Math.random() * Math.random() * Math.random()).toString(),
|
||||||
functionFilter: (server: OnlineServer) => parsedFilter?.applyToServer({ online: server }) ?? true
|
functionFilter: (server: OnlineServer) => parsedFilter?.applyToServer({ online: server }) ?? true
|
||||||
|
})
|
||||||
});
|
});
|
||||||
setTagStrings((c) => [...c, ...(parsedFilter?.getTagStrings() as string[])])
|
|
||||||
});
|
console.log(newFilters);
|
||||||
|
|
||||||
await updateServers(newFilters);
|
await updateServers(newFilters);
|
||||||
});
|
});
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
console.log(filters);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filteredData,
|
filteredData,
|
||||||
@ -321,6 +328,5 @@ export function useFilters(data: OnlineServer[]) {
|
|||||||
filters.filter((item, index, array) => array.indexOf(item) === index)
|
filters.filter((item, index, array) => array.indexOf(item) === index)
|
||||||
.length + (sort === null ? 1 : 0),
|
.length + (sort === null ? 1 : 0),
|
||||||
loading,
|
loading,
|
||||||
tagStrings
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +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 { useEffect, useState } from "react";
|
|
||||||
import { getMinehutIcons, MinehutIcon } from "../types/server-icon";
|
|
||||||
|
|
||||||
export function useIcons() {
|
|
||||||
const [icons, setIcons] = useState<MinehutIcon[]>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getMinehutIcons().then((i) => setIcons(i));
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return { icons };
|
|
||||||
}
|
|
||||||
@ -40,6 +40,8 @@ export function useMHSFServer(id: string) {
|
|||||||
const response = await fetch("/api/v1/server/get/" + id);
|
const response = await fetch("/api/v1/server/get/" + id);
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
|
||||||
|
console.log(json.server);
|
||||||
|
|
||||||
setServer(json.server);
|
setServer(json.server);
|
||||||
})();
|
})();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export function useServer(serverSpecifier: { id?: string; name?: string }) {
|
|||||||
`https://api.minehut.com/server/${serverSpecifier.id || serverSpecifier.name}${serverSpecifier.name ? "?byName=true" : ""}`
|
`https://api.minehut.com/server/${serverSpecifier.id || serverSpecifier.name}${serverSpecifier.name ? "?byName=true" : ""}`
|
||||||
);
|
);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
console.log(json);
|
||||||
if (json.server === null) throw new Error("Server not found");
|
if (json.server === null) throw new Error("Server not found");
|
||||||
|
|
||||||
setServer(json.server);
|
setServer(json.server);
|
||||||
|
|||||||
@ -61,23 +61,6 @@ export function useServers() {
|
|||||||
serverCount,
|
serverCount,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
refresh: () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
(async () => {
|
|
||||||
const serversFetch = await fetch("https://api.minehut.com/servers");
|
|
||||||
const serversJson: ServersAPIResponse = await serversFetch.json();
|
|
||||||
|
|
||||||
setPlayerCount(serversJson.total_players);
|
|
||||||
setServerCount(serversJson.total_servers);
|
|
||||||
setServers(serversJson.servers);
|
|
||||||
setLoading(false);
|
|
||||||
})();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
setError(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
import { FilterIdentifier, Filter } from "./filter";
|
|
||||||
import { CategoryFilter } from "./filters/category-filter";
|
|
||||||
import { CombinationFilter } from "./filters/combination-filter";
|
|
||||||
import { PlayerRangeFilter } from "./filters/player-range-filter";
|
|
||||||
import { TagFilter } from "./filters/tag-filter";
|
|
||||||
|
|
||||||
export const filterRegistry = new Map<string, (identifier: FilterIdentifier) => Filter>();
|
|
||||||
|
|
||||||
// Register filters
|
|
||||||
filterRegistry.set("app.mhsf.filter.util.combinationFilter", (identifier: FilterIdentifier) => new CombinationFilter([]).fromIdentifier(identifier));
|
|
||||||
filterRegistry.set("app.mhsf.filter.tagFilter", (identifier: FilterIdentifier) => new TagFilter(0, false).fromIdentifier(identifier));
|
|
||||||
filterRegistry.set("app.mhsf.filter.categoryFilter", (identifier: FilterIdentifier) => new CategoryFilter(0).fromIdentifier(identifier));
|
|
||||||
filterRegistry.set("app.mhsf.filter.playerRangeFilter", (identifier: FilterIdentifier) => new PlayerRangeFilter(0, 0).fromIdentifier(identifier));
|
|
||||||
@ -31,22 +31,36 @@
|
|||||||
import { allTags } from "@/config/tags";
|
import { allTags } from "@/config/tags";
|
||||||
import type { OnlineServer, ServerResponse } from "./mh-server";
|
import type { OnlineServer, ServerResponse } from "./mh-server";
|
||||||
import type { MHSFData } from "./data";
|
import type { MHSFData } from "./data";
|
||||||
|
import { TagFilter } from "./filters/tag-filter";
|
||||||
export type FilterIdentifier = {
|
import { CategoryFilter } from "./filters/category-filter";
|
||||||
[key: string]: string | number | boolean | null | Array<FilterIdentifier> | FilterIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Any filter that can be converted back and forth from a string or a Filter object */
|
/* Any filter that can be converted back and forth from a string or a Filter object */
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
type(): "filter";
|
type(): "filter";
|
||||||
toIdentifier(): FilterIdentifier;
|
toIdentifier(): { [key: string]: string | number | boolean };
|
||||||
getSpecificFilterId(): string;
|
getSpecificFilterId(): string;
|
||||||
fromIdentifier(identifier: FilterIdentifier): Filter;
|
fromIdentifier(identifier: {
|
||||||
|
[key: string]: string | number | boolean;
|
||||||
|
}): Filter;
|
||||||
applyToServer(server: {
|
applyToServer(server: {
|
||||||
online?: OnlineServer;
|
online?: OnlineServer;
|
||||||
server?: ServerResponse;
|
server?: ServerResponse;
|
||||||
mhsfData?: MHSFData;
|
mhsfData?: MHSFData;
|
||||||
}): Promise<boolean>;
|
}): Promise<boolean>;
|
||||||
getTagStrings(): string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const supportedFilters: {
|
||||||
|
ns: string;
|
||||||
|
fi: (identifier: {
|
||||||
|
[key: string]: string | number | boolean;
|
||||||
|
}) => Filter;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
ns: "app.mhsf.filter.tagFilter",
|
||||||
|
fi: new TagFilter(0, false).fromIdentifier
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ns: "app.mhsf.filter.categoryFilter",
|
||||||
|
fi: new CategoryFilter(0).fromIdentifier
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
import { allCategories } from "@/config/tags";
|
import { allCategories } from "@/config/tags";
|
||||||
import type { MHSFData } from "../data";
|
import type { MHSFData } from "../data";
|
||||||
import type { Filter, FilterIdentifier } from "../filter";
|
import type { Filter } from "../filter";
|
||||||
import type { OnlineServer, ServerResponse } from "../mh-server";
|
import type { OnlineServer, ServerResponse } from "../mh-server";
|
||||||
|
|
||||||
export class CategoryFilter implements Filter {
|
export class CategoryFilter implements Filter {
|
||||||
@ -40,11 +40,11 @@ export class CategoryFilter implements Filter {
|
|||||||
return "filter";
|
return "filter";
|
||||||
}
|
}
|
||||||
|
|
||||||
toIdentifier(): FilterIdentifier {
|
toIdentifier(): { [key: string]: string | number | boolean } {
|
||||||
return { categoryIndex: this.categoryIndex };
|
return { categoryIndex: this.categoryIndex };
|
||||||
}
|
}
|
||||||
|
|
||||||
fromIdentifier(identifier: FilterIdentifier): Filter {
|
fromIdentifier(identifier: { [key: string]: string | number | boolean; }): Filter {
|
||||||
return new CategoryFilter(identifier.categoryIndex as number);
|
return new CategoryFilter(identifier.categoryIndex as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,8 +61,4 @@ export class CategoryFilter implements Filter {
|
|||||||
constructor(categoryIndex: number) {
|
constructor(categoryIndex: number) {
|
||||||
this.categoryIndex = categoryIndex;
|
this.categoryIndex = categoryIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagStrings(): string[] {
|
|
||||||
return [`Server is a ${allCategories[this.categoryIndex].name} server`]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,80 +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 type { MHSFData } from "../data";
|
|
||||||
import { type Filter, type FilterIdentifier } from "../filter";
|
|
||||||
import { filterRegistry } from "../filter-registry";
|
|
||||||
import type { OnlineServer, ServerResponse } from "../mh-server";
|
|
||||||
|
|
||||||
export class CombinationFilter implements Filter {
|
|
||||||
filters: Filter[];
|
|
||||||
|
|
||||||
fromIdentifier(identifier: FilterIdentifier): Filter {
|
|
||||||
return new CombinationFilter((identifier.filters as Array<{type: string, metadata: { [key: string]: string | number | boolean }}>).map((c) => {
|
|
||||||
const factory = filterRegistry.get(c.type);
|
|
||||||
return factory ? factory(c.metadata) : undefined;
|
|
||||||
}).filter((v) => v !== undefined))
|
|
||||||
}
|
|
||||||
|
|
||||||
toIdentifier(): FilterIdentifier {
|
|
||||||
return {
|
|
||||||
filters: this.filters.map((c) => {
|
|
||||||
return { type: c.getSpecificFilterId(), metadata: c.toIdentifier() };
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getSpecificFilterId(): string {
|
|
||||||
return "app.mhsf.filter.util.combinationFilter";
|
|
||||||
}
|
|
||||||
|
|
||||||
type(): "filter" {
|
|
||||||
return "filter";
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyToServer(server: {
|
|
||||||
online?: OnlineServer;
|
|
||||||
server?: ServerResponse;
|
|
||||||
mhsfData?: MHSFData;
|
|
||||||
}): Promise<boolean> {
|
|
||||||
const map = this.filters.map((c) => c.applyToServer(server));
|
|
||||||
const asynced = await Promise.all(map);
|
|
||||||
|
|
||||||
return !asynced.includes(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(filters: Filter[]) {
|
|
||||||
this.filters = filters;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTagStrings(): string[] {
|
|
||||||
return this.filters.flatMap((c) => c.getTagStrings());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,91 +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 WARRANTIE
|
|
||||||
* 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 { MHSFData } from "../data";
|
|
||||||
import type { Filter, FilterIdentifier } from "../filter";
|
|
||||||
import { OnlineServer, ServerResponse } from "../mh-server";
|
|
||||||
|
|
||||||
export class PlayerRangeFilter implements Filter {
|
|
||||||
min: number | null;
|
|
||||||
max: number | null;
|
|
||||||
|
|
||||||
type(): "filter" {
|
|
||||||
return "filter";
|
|
||||||
}
|
|
||||||
|
|
||||||
getSpecificFilterId(): string {
|
|
||||||
return "app.mhsf.filter.playerRangeFilter";
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(min: number | null, max: number | null) {
|
|
||||||
this.min = min;
|
|
||||||
this.max = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
fromIdentifier(identifier: FilterIdentifier): Filter {
|
|
||||||
return new PlayerRangeFilter(identifier.min as number | null, identifier.max as number | null);
|
|
||||||
}
|
|
||||||
|
|
||||||
toIdentifier(): FilterIdentifier {
|
|
||||||
return {min: this.min, max: this.max};
|
|
||||||
}
|
|
||||||
|
|
||||||
applyToServer(server: {
|
|
||||||
online?: OnlineServer;
|
|
||||||
server?: ServerResponse;
|
|
||||||
mhsfData?: MHSFData;
|
|
||||||
}): Promise<boolean> {
|
|
||||||
if (this.max === null && this.min === null)
|
|
||||||
return new Promise((r) => r(true));
|
|
||||||
|
|
||||||
if (this.max === null)
|
|
||||||
return new Promise((r) => r((server.online?.playerData.playerCount ?? 0) >= (this.min ?? 0)))
|
|
||||||
|
|
||||||
if (this.min === null)
|
|
||||||
return new Promise((r) => r((server.online?.playerData.playerCount ?? 0) <= (this.max ?? 0)))
|
|
||||||
|
|
||||||
return new Promise((r) =>
|
|
||||||
r(
|
|
||||||
(server.online?.playerData.playerCount ?? 0) <= (this.max ?? 0) &&
|
|
||||||
(server.online?.playerData.playerCount ?? 0) >= (this.min ?? 0),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
getTagStrings(): string[] {
|
|
||||||
const tagArray = [];
|
|
||||||
|
|
||||||
if (this.max !== null)
|
|
||||||
tagArray.push(`${this.max} maximum players`)
|
|
||||||
if (this.min !== null)
|
|
||||||
tagArray.push(`${this.min} minimum players`)
|
|
||||||
|
|
||||||
return tagArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -31,7 +31,7 @@
|
|||||||
import { allTags } from "@/config/tags";
|
import { allTags } from "@/config/tags";
|
||||||
import type { MHSFData } from "../data";
|
import type { MHSFData } from "../data";
|
||||||
import type { OnlineServer, ServerResponse } from "../mh-server";
|
import type { OnlineServer, ServerResponse } from "../mh-server";
|
||||||
import type { Filter, FilterIdentifier } from "../filter";
|
import type { Filter } from "../filter";
|
||||||
|
|
||||||
export class TagFilter implements Filter {
|
export class TagFilter implements Filter {
|
||||||
tagId: string;
|
tagId: string;
|
||||||
@ -41,7 +41,7 @@ export class TagFilter implements Filter {
|
|||||||
return "filter";
|
return "filter";
|
||||||
}
|
}
|
||||||
|
|
||||||
toIdentifier(): FilterIdentifier {
|
toIdentifier(): { [key: string]: string | number | boolean } {
|
||||||
return { tagId: this.tagId, opposite: this.opposite };
|
return { tagId: this.tagId, opposite: this.opposite };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,9 @@ export class TagFilter implements Filter {
|
|||||||
return "app.mhsf.filter.tagFilter";
|
return "app.mhsf.filter.tagFilter";
|
||||||
}
|
}
|
||||||
|
|
||||||
fromIdentifier(identifier: FilterIdentifier): Filter {
|
fromIdentifier(identifier: {
|
||||||
|
[key: string]: string | number | boolean;
|
||||||
|
}): Filter {
|
||||||
return new TagFilter(identifier.tagId as string, identifier.opposite as boolean);
|
return new TagFilter(identifier.tagId as string, identifier.opposite as boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +74,12 @@ export class TagFilter implements Filter {
|
|||||||
).condition ?? (() => true)
|
).condition ?? (() => true)
|
||||||
)(server);
|
)(server);
|
||||||
|
|
||||||
|
console.log(result, server.online?.name, (
|
||||||
|
allTags.find((c) => btoa(c.docsName) === this.tagId) ?? {
|
||||||
|
condition: () => true,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
if (typeof result === "boolean")
|
if (typeof result === "boolean")
|
||||||
return new Promise((r) => r(this.opposite ? !result : result))
|
return new Promise((r) => r(this.opposite ? !result : result))
|
||||||
|
|
||||||
@ -79,10 +87,4 @@ export class TagFilter implements Filter {
|
|||||||
result.then((c) => r(this.opposite ? !c : c))
|
result.then((c) => r(this.opposite ? !c : c))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagStrings(): string[] {
|
|
||||||
if (this.opposite)
|
|
||||||
return [`Server does not have the '${allTags.find((c) => btoa(c.docsName) === this.tagId)?.docsName}' filter`]
|
|
||||||
return [`Server has the '${allTags.find((c) => btoa(c.docsName) === this.tagId)?.docsName}' filter`]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
export async function getMinehutIcons(): Promise<MinehutIcon[] | undefined> {
|
export async function getMinehutIcons(): Promise<MinehutIcon[] | undefined> {
|
||||||
const icons = await fetch("https://api.minehut.com/servers/icons");
|
const icons = await fetch("https://api.minehut.com/servers/icons");
|
||||||
|
console.log(icons);
|
||||||
if (!icons.ok) return undefined;
|
if (!icons.ok) return undefined;
|
||||||
return await icons.json();
|
return await icons.json();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,5 +38,4 @@ export interface Sort {
|
|||||||
[key: string]: string | number | boolean;
|
[key: string]: string | number | boolean;
|
||||||
}): Sort;
|
}): Sort;
|
||||||
sortToServers(serverA: OnlineServer, serverB: OnlineServer): number;
|
sortToServers(serverA: OnlineServer, serverB: OnlineServer): number;
|
||||||
getTagStrings(): string[];
|
|
||||||
}
|
}
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { FilterIdentifier, Filter } from "./filter";
|
|
||||||
import { filterRegistry } from "./filter-registry";
|
|
||||||
|
|
||||||
export const supportedFilters = Array.from(filterRegistry.entries()).map(([ns, fi]) => ({
|
|
||||||
ns,
|
|
||||||
fi
|
|
||||||
}));
|
|
||||||
@ -43,11 +43,7 @@ const isOldServerRoute = createRouteMatcher([
|
|||||||
"/server/:serverName",
|
"/server/:serverName",
|
||||||
"/server/:serverName/statistics",
|
"/server/:serverName/statistics",
|
||||||
]);
|
]);
|
||||||
const isWaitlistPage = createRouteMatcher(["/waitlist", "/waitlist(.*)"]);
|
const apiRoute = createRouteMatcher(["/api/(.*)"]);
|
||||||
const apiWaitlistPage = createRouteMatcher([
|
|
||||||
"/api/v1/user/waitlist(.*)",
|
|
||||||
"/api/v1/get-status",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
|
export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
|
||||||
? clerkMiddleware(async (auth, req) => {
|
? clerkMiddleware(async (auth, req) => {
|
||||||
@ -56,19 +52,6 @@ export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
|
|||||||
const requestHeaders = new Headers(req.headers);
|
const requestHeaders = new Headers(req.headers);
|
||||||
requestHeaders.set("x-url", req.url);
|
requestHeaders.set("x-url", req.url);
|
||||||
|
|
||||||
if (!isWaitlistPage(req) && !apiWaitlistPage(req)) {
|
|
||||||
if (process.env.NEXT_PUBLIC_WAITLIST_ENABLED === "true") {
|
|
||||||
if (authRes.userId === null)
|
|
||||||
return NextResponse.redirect(new URL("/waitlist", req.url));
|
|
||||||
|
|
||||||
const metadata = (await client.users.getUser(authRes.userId))
|
|
||||||
.publicMetadata;
|
|
||||||
if (metadata.v2allowed !== true)
|
|
||||||
return NextResponse.redirect(new URL("/waitlist", req.url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (isRootRoute(req)) {
|
if (isRootRoute(req)) {
|
||||||
switch (authRes.userId === null) {
|
switch (authRes.userId === null) {
|
||||||
case false:
|
case false:
|
||||||
|
|||||||
@ -95,6 +95,7 @@ export default serve({
|
|||||||
server: server.name,
|
server: server.name,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
});
|
});
|
||||||
|
console.log(i, mh.servers.length);
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -95,6 +95,7 @@ export default async function handler(
|
|||||||
|
|
||||||
res.send({ result: dailyAverages });
|
res.send({ result: dailyAverages });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
res.status(500).json({ message: "An error occurred while fetching data" });
|
res.status(500).json({ message: "An error occurred while fetching data" });
|
||||||
} finally {
|
} finally {
|
||||||
await client.close();
|
await client.close();
|
||||||
|
|||||||
@ -114,6 +114,7 @@ export default async function handler(
|
|||||||
|
|
||||||
res.send({ data });
|
res.send({ data });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
res.status(500).json({ message: "An error occurred while fetching data" });
|
res.status(500).json({ message: "An error occurred while fetching data" });
|
||||||
} finally {
|
} finally {
|
||||||
await client.close();
|
await client.close();
|
||||||
|
|||||||
@ -30,7 +30,6 @@
|
|||||||
|
|
||||||
import { getBackendProcedure } from "@/lib/backend-procedure";
|
import { getBackendProcedure } from "@/lib/backend-procedure";
|
||||||
import type { MHSFData } from "@/lib/types/data";
|
import type { MHSFData } from "@/lib/types/data";
|
||||||
import { clerkClient, getAuth } from "@clerk/nextjs/server";
|
|
||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from "mongodb";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
@ -84,17 +83,17 @@ export default async function handler(
|
|||||||
await mongo.connect();
|
await mongo.connect();
|
||||||
const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||||
const stats = mongo.db("mhsf")
|
const stats = mongo.db("mhsf")
|
||||||
const {userId} = getAuth(req);
|
const userId = req.cookies.userId;
|
||||||
|
|
||||||
// Run queries in parallel
|
// Run queries in parallel
|
||||||
const [favoriteData, customizationData, playerData, achievements] =
|
const [favoriteData, customizationData, playerData, achievements] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
findFavoriteData(serverData.name, userId ?? undefined, stats, {
|
findFavoriteData(serverData.name, userId, stats, {
|
||||||
maxFavoriteEntries,
|
maxFavoriteEntries,
|
||||||
favoriteTimespanStart,
|
favoriteTimespanStart,
|
||||||
favoriteTimespanEnd,
|
favoriteTimespanEnd,
|
||||||
}),
|
}),
|
||||||
findCustomizationData(serverData.name, userId ?? undefined, stats),
|
findCustomizationData(serverData.name, userId, db),
|
||||||
findPlayerData(serverData.name, stats, {
|
findPlayerData(serverData.name, stats, {
|
||||||
maxPlayerEntries,
|
maxPlayerEntries,
|
||||||
playerTimespanStart,
|
playerTimespanStart,
|
||||||
@ -148,7 +147,6 @@ async function findCustomizationData(
|
|||||||
isOwned: boolean;
|
isOwned: boolean;
|
||||||
isOwnedByUser: boolean;
|
isOwnedByUser: boolean;
|
||||||
}> {
|
}> {
|
||||||
const clerk = await clerkClient();
|
|
||||||
// Run queries in parallel
|
// Run queries in parallel
|
||||||
const [customizationData, ownedServerData] = await Promise.all([
|
const [customizationData, ownedServerData] = await Promise.all([
|
||||||
db.collection("customization").findOne({ server: serverName }),
|
db.collection("customization").findOne({ server: serverName }),
|
||||||
@ -162,7 +160,6 @@ async function findCustomizationData(
|
|||||||
...(customizationData as any),
|
...(customizationData as any),
|
||||||
isOwned: true,
|
isOwned: true,
|
||||||
isOwnedByUser: ownedServerData?.author === userId,
|
isOwnedByUser: ownedServerData?.author === userId,
|
||||||
userProfilePicture: userId ? (await clerk.users.getUser(ownedServerData?.author)).imageUrl : 'no user'
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,127 +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 { clerkClient, getAuth } from "@clerk/nextjs/server";
|
|
||||||
import { MongoClient } from "mongodb";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
) {
|
|
||||||
const { userId } = getAuth(req);
|
|
||||||
if (!userId) {
|
|
||||||
return res.json({ message: "User not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the OAuth access token for the user
|
|
||||||
const provider = "discord";
|
|
||||||
|
|
||||||
const client = await clerkClient();
|
|
||||||
|
|
||||||
const clerkResponse = await client.users.getUserOauthAccessToken(
|
|
||||||
userId,
|
|
||||||
provider,
|
|
||||||
);
|
|
||||||
|
|
||||||
const accessToken = clerkResponse.data[0]?.token || "";
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
return res.status(401).json({ message: "Access token not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch("https://discord.com/api/users/@me", {
|
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
|
||||||
});
|
|
||||||
const json: DiscordUser = await response.json()
|
|
||||||
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
|
||||||
const db = mongo.db("mhsf").collection('waitlist-approved');
|
|
||||||
const refs = mongo.db("mhsf").collection('waitlist-refs');
|
|
||||||
|
|
||||||
const entry = await db.findOneAndDelete({
|
|
||||||
user: json.username
|
|
||||||
})
|
|
||||||
|
|
||||||
if (entry === null) {
|
|
||||||
return res.status(400).send({message: "You are unfortunately not eligible."})
|
|
||||||
}
|
|
||||||
|
|
||||||
const rand = crypto.randomUUID();
|
|
||||||
await refs.insertOne({
|
|
||||||
usersRemaining: 2,
|
|
||||||
id: rand,
|
|
||||||
userAssociatedTo: json.username
|
|
||||||
})
|
|
||||||
|
|
||||||
const user = await client.users.getUser(userId);
|
|
||||||
|
|
||||||
await client.users.updateUserMetadata(userId, {
|
|
||||||
publicMetadata: {
|
|
||||||
...user.publicMetadata,
|
|
||||||
v2allowed: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.send({message: "You are eligible!", refUUID: rand})
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DiscordUser {
|
|
||||||
id: string
|
|
||||||
username: string
|
|
||||||
avatar: string
|
|
||||||
discriminator: string
|
|
||||||
public_flags: number
|
|
||||||
flags: number
|
|
||||||
accent_color: number
|
|
||||||
global_name: string
|
|
||||||
banner_color: string
|
|
||||||
clan: Clan
|
|
||||||
primary_guild: PrimaryGuild
|
|
||||||
mfa_enabled: boolean
|
|
||||||
locale: string
|
|
||||||
premium_type: number
|
|
||||||
email: string
|
|
||||||
verified: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Clan {
|
|
||||||
identity_guild_id: string
|
|
||||||
identity_enabled: boolean
|
|
||||||
tag: string
|
|
||||||
badge: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PrimaryGuild {
|
|
||||||
identity_guild_id: string
|
|
||||||
identity_enabled: boolean
|
|
||||||
tag: string
|
|
||||||
badge: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { clerkClient, getAuth } from "@clerk/nextjs/server";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
) {
|
|
||||||
const { userId } = getAuth(req);
|
|
||||||
if (!userId) {
|
|
||||||
return res.json({ message: "User not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the OAuth access token for the user
|
|
||||||
const provider = "discord";
|
|
||||||
|
|
||||||
const client = await clerkClient();
|
|
||||||
|
|
||||||
const clerkResponse = await client.users.getUserOauthAccessToken(
|
|
||||||
userId,
|
|
||||||
provider,
|
|
||||||
);
|
|
||||||
|
|
||||||
const accessToken = clerkResponse.data[0]?.token || "";
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
return res.status(401).json({ message: "Access token not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch("https://discord.com/api/users/@me", {
|
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
|
||||||
});
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
res.send({ discordData: json });
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,92 +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 { clerkClient, getAuth } from "@clerk/nextjs/server";
|
|
||||||
import { MongoClient } from "mongodb";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
) {
|
|
||||||
const client = await clerkClient();
|
|
||||||
const { userId } = getAuth(req);
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(400).json({ message: "User not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await client.users.getUser(userId);
|
|
||||||
if (user.publicMetadata.v2allowed === true) {
|
|
||||||
return res.status(400).json({ message: "v2 already allowed." });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = req.body;
|
|
||||||
if (!id) {
|
|
||||||
return res.status(400).json({ message: "ID not specified" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const uuidTested =
|
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
if (uuidTested === false)
|
|
||||||
return res.status(400).json({ message: "UUID not valid" });
|
|
||||||
|
|
||||||
const mongo = new MongoClient(process.env.MONGO_DB as string);
|
|
||||||
const refs = mongo.db("mhsf").collection("waitlist-refs");
|
|
||||||
|
|
||||||
const ref = await refs.findOne({
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
if (ref !== undefined && ref !== null) {
|
|
||||||
if (ref.usersRemaining !== 0) {
|
|
||||||
await refs.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$inc: { usersRemaining: -1 },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await client.users.updateUserMetadata(userId, {
|
|
||||||
publicMetadata: {
|
|
||||||
...user.publicMetadata,
|
|
||||||
v2allowed: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.send({ message: "You are eligible!" });
|
|
||||||
}
|
|
||||||
return res.status(400).json({ message: "No users left" });
|
|
||||||
}
|
|
||||||
return res.status(400).json({ message: "Unknown ID" });
|
|
||||||
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user