mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-15 03:28:03 -05:00
feat: add waitlist and other stuffs
This commit is contained in:
parent
7d0bb44568
commit
3ca0cadfbc
@ -41,6 +41,10 @@ const nextConfig = {
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "avatars.githubusercontent.com"
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "cdn.discordapp.com"
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
@ -21,6 +21,11 @@
|
||||
"@clerk/nextjs": "^6.9.2",
|
||||
"@emotion/is-prop-valid": "^1.3.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",
|
||||
"@number-flow/react": "^0.5.7",
|
||||
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||
|
||||
0
apps/www/src/app/(main)/misc/see-waitlist-codes/page.tsx
Normal file
0
apps/www/src/app/(main)/misc/see-waitlist-codes/page.tsx
Normal file
@ -28,15 +28,8 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { SVGProps } from "react";
|
||||
import { WaitlistDiscordNeeded } from "@/components/feat/waitlist/waitlist-discord-needed";
|
||||
|
||||
export const affiliates: {
|
||||
name: string;
|
||||
shop: string;
|
||||
line: string;
|
||||
mode: string[];
|
||||
otherLinks: {
|
||||
name: string;
|
||||
icon: (props: SVGProps<SVGSVGElement>) => JSX.Element;
|
||||
}[];
|
||||
}[] = [];
|
||||
export default function OAuthNeedDiscord() {
|
||||
return <WaitlistDiscordNeeded />
|
||||
}
|
||||
35
apps/www/src/app/(main)/waitlist/page.tsx
Normal file
35
apps/www/src/app/(main)/waitlist/page.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 />
|
||||
}
|
||||
35
apps/www/src/app/(main)/waitlist/ref/page.tsx
Normal file
35
apps/www/src/app/(main)/waitlist/ref/page.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 />
|
||||
}
|
||||
@ -38,6 +38,7 @@ import { ArrowLeft } from "lucide-react";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { use } from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import { invertHex } from "../../page";
|
||||
|
||||
export default function ModificationPage({
|
||||
params,
|
||||
@ -48,7 +49,6 @@ export default function ModificationPage({
|
||||
const [backRoute] = useQueryState("b", {
|
||||
defaultValue: "/servers/embedded/sl-modification-frame",
|
||||
});
|
||||
console.log(mod);
|
||||
const categoryObj = serverModDB.find(
|
||||
(c) => c.displayTitle === atob(decodeURIComponent(category))
|
||||
);
|
||||
@ -65,7 +65,7 @@ export default function ModificationPage({
|
||||
style={{ backgroundColor: modObj?.color }}
|
||||
>
|
||||
<Link href={backRoute}>
|
||||
<ArrowLeft />
|
||||
<ArrowLeft style={{color: invertHex(modObj?.color ?? "")}} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
@ -65,6 +65,7 @@ import { useQueryState } from "nuqs";
|
||||
import { use } from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import { toast } from "sonner";
|
||||
import { invertHex } from "../../../page";
|
||||
|
||||
export default function ModificationPage({
|
||||
params,
|
||||
@ -78,7 +79,6 @@ export default function ModificationPage({
|
||||
const [backRoute] = useQueryState("b", {
|
||||
defaultValue: "/servers/embedded/sl-modification-frame",
|
||||
});
|
||||
console.log(mod);
|
||||
const modIndex = (
|
||||
(user?.unsafeMetadata
|
||||
.activatedModifications as ClerkCustomActivatedModification[]) ?? []
|
||||
@ -109,7 +109,7 @@ export default function ModificationPage({
|
||||
style={{ backgroundColor: modObj?.color }}
|
||||
>
|
||||
<Link href={backRoute}>
|
||||
<ArrowLeft />
|
||||
<ArrowLeft color={invertHex(modObj?.color)} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@ -120,22 +120,27 @@ export default function ModificationPage({
|
||||
you proud?)
|
||||
</Markdown>
|
||||
<div className="flex justify-between items-center">
|
||||
<Button className="mt-2" onClick={async () => {
|
||||
const newModObj = {
|
||||
...modObj,
|
||||
active: !modObj.active
|
||||
}
|
||||
const modificationArray = (user?.unsafeMetadata
|
||||
.activatedModifications as ClerkCustomActivatedModification[]) ?? [];
|
||||
modificationArray[modIndex] = newModObj;
|
||||
await user?.update({
|
||||
unsafeMetadata: {
|
||||
...user.unsafeMetadata,
|
||||
activatedModifications: modificationArray
|
||||
}
|
||||
});
|
||||
communicator.send("rerender-servers", {});
|
||||
}}>
|
||||
<Button
|
||||
className="mt-2"
|
||||
onClick={async () => {
|
||||
const newModObj = {
|
||||
...modObj,
|
||||
active: !modObj.active,
|
||||
};
|
||||
const modificationArray =
|
||||
(user?.unsafeMetadata
|
||||
.activatedModifications as ClerkCustomActivatedModification[]) ??
|
||||
[];
|
||||
modificationArray[modIndex] = newModObj;
|
||||
await user?.update({
|
||||
unsafeMetadata: {
|
||||
...user.unsafeMetadata,
|
||||
activatedModifications: modificationArray,
|
||||
},
|
||||
});
|
||||
communicator.send("rerender-servers", {});
|
||||
}}
|
||||
>
|
||||
{modObj?.active ? "Disable" : "Enable"}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
|
||||
@ -51,7 +51,6 @@ export default async function ServerListCategoryFrame({
|
||||
const categoryObj = serverModDB.find(
|
||||
(c) => c.displayTitle === atob(decodeURIComponent(category)),
|
||||
);
|
||||
``
|
||||
return (
|
||||
<main className=" p-4">
|
||||
<h1 className="text-xl font-bold w-full flex items-center gap-2">
|
||||
@ -78,7 +77,10 @@ export default async function ServerListCategoryFrame({
|
||||
)}
|
||||
style={{ backgroundColor: m.color }}
|
||||
>
|
||||
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
||||
<m.icon
|
||||
className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center"
|
||||
color={invertHex(m.color)}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-center w-full flex items-center justify-center">
|
||||
{m.name}
|
||||
@ -95,3 +97,27 @@ export default async function ServerListCategoryFrame({
|
||||
</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,6 +39,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "@/lib/useRouter";
|
||||
import { SignedIn, useUser } from "@clerk/nextjs";
|
||||
import { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||
import { invertHex } from "./category/[category]/page";
|
||||
|
||||
export default function ServerListModificationFrame() {
|
||||
const router = useRouter();
|
||||
@ -81,7 +82,7 @@ export default function ServerListModificationFrame() {
|
||||
</Link>
|
||||
</h2>
|
||||
<div className="grid grid-cols-6 lg:grid-cols-3 gap-2">
|
||||
{c.entries.map((m) => (
|
||||
{c.entries.slice(0, 6).map((m) => (
|
||||
<Material
|
||||
elevation="high"
|
||||
className="p-2 hover:drop-shadow-card-hover cursor-pointer"
|
||||
@ -96,7 +97,10 @@ export default function ServerListModificationFrame() {
|
||||
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
||||
style={{ backgroundColor: m.color }}
|
||||
>
|
||||
<m.icon className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
||||
<m.icon
|
||||
className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center"
|
||||
color={invertHex(m.color)}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-center w-full flex items-center justify-center">
|
||||
{m.name}
|
||||
@ -124,7 +128,10 @@ export default function ServerListModificationFrame() {
|
||||
className="w-full h-[40px] mb-2 rounded-lg items-center text-center justify-center"
|
||||
style={{ backgroundColor: m.color }}
|
||||
>
|
||||
<Binary className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center" />
|
||||
<Binary
|
||||
className="relative top-[calc(50%-12px)] items-center w-full text-center justify-center"
|
||||
color={invertHex(m.color)}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-center w-full flex items-center justify-center">
|
||||
{m.friendlyName}
|
||||
|
||||
@ -72,13 +72,6 @@
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--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%);
|
||||
|
||||
}
|
||||
@ -241,6 +234,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
::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 {
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation-duration: 2s;
|
||||
|
||||
@ -52,7 +52,7 @@ export default function RootLayout({
|
||||
title="JavaScript is required for MHSF"
|
||||
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>
|
||||
</Link>
|
||||
</Placeholder>
|
||||
|
||||
@ -99,9 +99,7 @@ export default function Embed({ params }: { params: { server: string } }) {
|
||||
<div className="px-4 pt-2 flex items-center group overflow-hidden">
|
||||
<div
|
||||
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]"
|
||||
)}
|
||||
>
|
||||
|
||||
@ -58,7 +58,7 @@ export function Footer() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<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">
|
||||
<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">
|
||||
@ -67,7 +67,7 @@ export function Footer() {
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</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">
|
||||
<BrandingGenericIcon className="max-w-[30px] max-h-[30px] rounded border border-muted-foreground" width={30} height={30} />
|
||||
<span className="block">
|
||||
@ -78,7 +78,7 @@ export function Footer() {
|
||||
</Link>
|
||||
</DropdownMenuContent>
|
||||
</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">
|
||||
<Github className="w-[1.25em] h-[1.25em]" />
|
||||
</Button>
|
||||
@ -89,7 +89,7 @@ export function Footer() {
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<span className="block px-4 -translate-y-12">
|
||||
<span className="block px-4 lg:-translate-y-12">
|
||||
<small className="text-[0.75rem]">
|
||||
MHSF is an open-source project licensed under the MIT license. MHSF is
|
||||
not officially affiliated with with Minehut, Super League Enterprise,
|
||||
|
||||
@ -27,7 +27,7 @@ export function FooterStatus() {
|
||||
return (
|
||||
<Link
|
||||
href={`https://${statusURL as string}${determineIfOutage() ? `/incident/${determineWhatOutage()?.id}` : ""}`}
|
||||
noExtraIcons
|
||||
noextraicons
|
||||
target="_blank"
|
||||
>
|
||||
<Button variant="tertiary">
|
||||
|
||||
@ -57,62 +57,61 @@ export const brandingIconClipboard = `<svg width="266" height="265" viewBox="0 0
|
||||
* @returns A JSX element representing the colorful branding icon.
|
||||
*/
|
||||
export function BrandingColorfulIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width="266"
|
||||
height="265"
|
||||
viewBox="0 0 266 265"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
x="0.524048"
|
||||
width="264.939"
|
||||
height="264.939"
|
||||
rx="66"
|
||||
fill="url(#paint0_linear_1_19)"
|
||||
/>
|
||||
<path
|
||||
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"
|
||||
stroke="white"
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle
|
||||
cx="132.993"
|
||||
cy="132.469"
|
||||
r="91.3779"
|
||||
stroke="url(#paint1_linear_1_19)"
|
||||
stroke-width="8"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_19"
|
||||
x1="107.824"
|
||||
y1="54.754"
|
||||
x2="230.579"
|
||||
y2="225.198"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#007BFF" />
|
||||
<stop offset="1" stop-color="#BF00FF" stop-opacity="0.5" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1_19"
|
||||
x1="132.993"
|
||||
y1="37.0914"
|
||||
x2="132.993"
|
||||
y2="227.847"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#EFEC32" />
|
||||
<stop offset="1" stop-color="#98FF60" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="266"
|
||||
height="265"
|
||||
fill="none"
|
||||
viewBox="0 0 266 265"
|
||||
>
|
||||
<rect
|
||||
width="264.939"
|
||||
height="264.939"
|
||||
x="0.524"
|
||||
fill="url(#paint0_linear_1_19)"
|
||||
rx="66"
|
||||
/>
|
||||
<path
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="10"
|
||||
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"
|
||||
/>
|
||||
<circle
|
||||
cx="132.993"
|
||||
cy="132.469"
|
||||
r="91.378"
|
||||
stroke="url(#paint1_linear_1_19)"
|
||||
strokeWidth="8"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_19"
|
||||
x1="107.824"
|
||||
x2="230.579"
|
||||
y1="54.754"
|
||||
y2="225.198"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#007BFF"/>
|
||||
<stop offset="1" stopColor="#BF00FF" stopOpacity="0.5"/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1_19"
|
||||
x1="132.993"
|
||||
x2="132.993"
|
||||
y1="37.091"
|
||||
y2="227.847"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EFEC32"/>
|
||||
<stop offset="1" stopColor="#98FF60"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Returns the optional Pride icon
|
||||
@ -123,71 +122,71 @@ export function BrandingColorfulIcon(props: SVGProps<SVGSVGElement>) {
|
||||
* @returns A JSX element representing the branding icon.
|
||||
*/
|
||||
export function BrandingPrideIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width="265"
|
||||
height="265"
|
||||
viewBox="0 0 265 265"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
width="264.939"
|
||||
height="264.939"
|
||||
rx="66"
|
||||
fill="url(#paint0_linear_1_30)"
|
||||
/>
|
||||
<path
|
||||
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"
|
||||
stroke="white"
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle
|
||||
cx="132.469"
|
||||
cy="132.469"
|
||||
r="91.3779"
|
||||
stroke="url(#paint1_linear_1_30)"
|
||||
stroke-width="8"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_30"
|
||||
x1="51.6631"
|
||||
y1="26.9354"
|
||||
x2="222.549"
|
||||
y2="213.717"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FF0000" />
|
||||
<stop offset="0.110405" stop-color="#FF6200" />
|
||||
<stop offset="0.225785" stop-color="#FFAE00" />
|
||||
<stop offset="0.326294" stop-color="#FFD500" />
|
||||
<stop offset="0.422381" stop-color="#99EA00" />
|
||||
<stop offset="0.498373" stop-color="#4DF457" />
|
||||
<stop offset="0.593491" stop-color="#26D3AB" />
|
||||
<stop offset="0.699814" stop-color="#13A9D5" />
|
||||
<stop offset="0.805673" stop-color="#A200FF" />
|
||||
<stop offset="0.884464" stop-color="#C62AEB" />
|
||||
<stop offset="0.957056" stop-color="white" />
|
||||
<stop offset="0.997383" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1_30"
|
||||
x1="132.469"
|
||||
y1="37.0914"
|
||||
x2="132.469"
|
||||
y2="227.847"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#EFEC32" />
|
||||
<stop offset="1" stop-color="#98FF60" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="265"
|
||||
height="265"
|
||||
fill="none"
|
||||
viewBox="0 0 265 265"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
width="264.939"
|
||||
height="264.939"
|
||||
fill="url(#paint0_linear_1_30)"
|
||||
rx="66"
|
||||
/>
|
||||
<path
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="10"
|
||||
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"
|
||||
/>
|
||||
<circle
|
||||
cx="132.469"
|
||||
cy="132.469"
|
||||
r="91.378"
|
||||
stroke="url(#paint1_linear_1_30)"
|
||||
strokeWidth="8"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_30"
|
||||
x1="51.663"
|
||||
x2="222.549"
|
||||
y1="26.935"
|
||||
y2="213.717"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="red" />
|
||||
<stop offset="0.11" stopColor="#FF6200" />
|
||||
<stop offset="0.226" stopColor="#FFAE00" />
|
||||
<stop offset="0.326" stopColor="#FFD500" />
|
||||
<stop offset="0.422" stopColor="#99EA00" />
|
||||
<stop offset="0.498" stopColor="#4DF457" />
|
||||
<stop offset="0.593" stopColor="#26D3AB" />
|
||||
<stop offset="0.7" stopColor="#13A9D5" />
|
||||
<stop offset="0.806" stopColor="#A200FF" />
|
||||
<stop offset="0.884" stopColor="#C62AEB" />
|
||||
<stop offset="0.957" stopColor="#fff" />
|
||||
<stop offset="0.997" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1_30"
|
||||
x1="132.469"
|
||||
x2="132.469"
|
||||
y1="37.091"
|
||||
y2="227.847"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EFEC32" />
|
||||
<stop offset="1" stopColor="#98FF60" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -203,170 +202,135 @@ export function BrandingPrideIcon(props: SVGProps<SVGSVGElement>) {
|
||||
* @returns A JSX element representing the branding icon.
|
||||
*/
|
||||
export function BrandingGenericIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
if (resolvedTheme === "dark") {
|
||||
return (
|
||||
<svg
|
||||
width="265"
|
||||
height="266"
|
||||
viewBox="0 0 265 266"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
x="0.0612793"
|
||||
y="0.86145"
|
||||
width="264.939"
|
||||
height="264.939"
|
||||
rx="66"
|
||||
fill="url(#paint0_linear_1_20)"
|
||||
/>
|
||||
<path
|
||||
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"
|
||||
stroke="white"
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle
|
||||
cx="132.531"
|
||||
cy="133.331"
|
||||
r="91.3779"
|
||||
stroke="url(#paint1_linear_1_20)"
|
||||
stroke-width="8"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_20"
|
||||
x1="107.361"
|
||||
y1="55.6155"
|
||||
x2="230.116"
|
||||
y2="226.059"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1_20"
|
||||
x1="132.531"
|
||||
y1="37.9529"
|
||||
x2="132.531"
|
||||
y2="228.709"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#EFEC32" />
|
||||
<stop offset="1" stop-color="#98FF60" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
width="265"
|
||||
height="265"
|
||||
viewBox="0 0 265 265"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
x="0.0612793"
|
||||
width="264.939"
|
||||
height="264.939"
|
||||
rx="66"
|
||||
fill="url(#paint0_linear_1_25)"
|
||||
/>
|
||||
<path
|
||||
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"
|
||||
stroke="black"
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle
|
||||
cx="132.531"
|
||||
cy="132.469"
|
||||
r="91.3779"
|
||||
stroke="url(#paint1_linear_1_25)"
|
||||
stroke-width="8"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_25"
|
||||
x1="107.361"
|
||||
y1="54.754"
|
||||
x2="230.116"
|
||||
y2="225.198"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1_25"
|
||||
x1="132.531"
|
||||
y1="37.0914"
|
||||
x2="132.531"
|
||||
y2="227.847"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#EFEC32" />
|
||||
<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>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
if (resolvedTheme === "dark") {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="265"
|
||||
height="266"
|
||||
fill="none"
|
||||
viewBox="0 0 265 266"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
width="264.939"
|
||||
height="264.939"
|
||||
x="0.061"
|
||||
y="0.861"
|
||||
fill="url(#paint0_linear_1_20)"
|
||||
rx="66"
|
||||
/>
|
||||
<path
|
||||
stroke="#fff"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="10"
|
||||
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"
|
||||
/>
|
||||
<circle
|
||||
cx="132.531"
|
||||
cy="133.331"
|
||||
r="91.378"
|
||||
stroke="url(#paint1_linear_1_20)"
|
||||
strokeWidth="8"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_20"
|
||||
x1="107.361"
|
||||
x2="230.116"
|
||||
y1="55.615"
|
||||
y2="226.059"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1_20"
|
||||
x1="132.531"
|
||||
x2="132.531"
|
||||
y1="37.953"
|
||||
y2="228.709"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EFEC32" />
|
||||
<stop offset="1" stopColor="#98FF60" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="265"
|
||||
height="265"
|
||||
fill="none"
|
||||
viewBox="0 0 265 265"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
width="264.939"
|
||||
height="264.939"
|
||||
x="0.061"
|
||||
fill="url(#paint0_linear_1_25)"
|
||||
rx="66"
|
||||
/>
|
||||
<path
|
||||
stroke="#000"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="10"
|
||||
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"
|
||||
/>
|
||||
<circle
|
||||
cx="132.531"
|
||||
cy="132.469"
|
||||
r="91.378"
|
||||
stroke="url(#paint1_linear_1_25)"
|
||||
strokeWidth="8"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1_25"
|
||||
x1="107.361"
|
||||
x2="230.116"
|
||||
y1="54.754"
|
||||
y2="225.198"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#fff" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1_25"
|
||||
x1="132.531"
|
||||
x2="132.531"
|
||||
y1="37.091"
|
||||
y2="227.847"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#EFEC32" />
|
||||
<stop offset="1" stopColor="#98FF60" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export const Discord = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
viewBox="0 0 256 199"
|
||||
width="1em"
|
||||
height="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z"
|
||||
fill="#5865F2"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
viewBox="0 0 256 199"
|
||||
width="1em"
|
||||
height="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z"
|
||||
fill="#5865F2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@ -40,7 +40,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function IconDisplay(props: {
|
||||
server: OnlineServer | ServerResponse;
|
||||
server: OnlineServer | ServerResponse | { icon: string };
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@ -71,12 +71,12 @@ export function NavBar() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-screen h-[3rem] grid-cols-3 fixed z-10 flex",
|
||||
"h-[3rem] grid-cols-3 fixed z-10 flex",
|
||||
"items-center justify-self-start me-auto pl-4 flex-1 transition-all justify-between",
|
||||
"lg:top-0 max-lg:bottom-0",
|
||||
"lg:top-0 max-lg:bottom-0 bg-neutral-100 dark:bg-neutral-900",
|
||||
showBorder
|
||||
? "border-b backdrop-blur-xl"
|
||||
: "max-lg:border-b max-lg:backdrop-blur-xl",
|
||||
? "border-b backdrop-blur-xl w-screen"
|
||||
: "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",
|
||||
pathname !== null && animatedTopbarPages.includes(pathname)
|
||||
? "[--animation-delay:1000ms] opacity-0 animate-fade-in"
|
||||
: "",
|
||||
@ -192,8 +192,10 @@ export function NavBar() {
|
||||
</DropdownMenu>
|
||||
<SignedIn>
|
||||
<div
|
||||
className="absolute right-0 -z-10 h-full
|
||||
overflow-hidden w-full ml-auto"
|
||||
className={cn(
|
||||
"absolute right-0 -z-10 h-full transition-all overflow-hidden w-full ml-auto",
|
||||
showBorder ? "" : "hidden",
|
||||
)}
|
||||
style={{ borderRadius: "inherit" }}
|
||||
>
|
||||
<img
|
||||
|
||||
@ -121,14 +121,14 @@ export function CustomErrors({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<Link
|
||||
noExtraIcons
|
||||
noextraicons
|
||||
target="_blank"
|
||||
href={`https://typescript.tv/errors/#ts${c.code}`}
|
||||
>
|
||||
<DropdownMenuItem>typescript.tv</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link
|
||||
noExtraIcons
|
||||
noextraicons
|
||||
target="_blank"
|
||||
href={`https://ts-error-translator.vercel.app/?error=${compressToEncodedURIComponent(c.messageText.toString())}`}
|
||||
>
|
||||
|
||||
@ -40,7 +40,8 @@ export function ModificationAction({ value }: { value?: Action }) {
|
||||
>) ?? []
|
||||
).findIndex(
|
||||
(c) =>
|
||||
JSON.stringify(c.metadata) === JSON.stringify(filter.toIdentifier()) &&
|
||||
JSON.stringify(c.metadata) ===
|
||||
JSON.stringify(filter.toIdentifier()) &&
|
||||
c.type === filter.getSpecificFilterId(),
|
||||
);
|
||||
return existing;
|
||||
@ -48,7 +49,7 @@ export function ModificationAction({ value }: { value?: Action }) {
|
||||
return -1;
|
||||
};
|
||||
|
||||
useEffect(() => setApplied(findExisting()))
|
||||
useEffect(() => setApplied(findExisting()));
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -68,15 +69,17 @@ export function ModificationAction({ value }: { value?: Action }) {
|
||||
className="mt-1"
|
||||
onClick={async () => {
|
||||
if (value?.type() === "filter") {
|
||||
const updatedUser = await user?.reload();
|
||||
const filter = value as Filter;
|
||||
const existing = findExisting();
|
||||
|
||||
if (isSignedIn) {
|
||||
const existingArray =
|
||||
(user.unsafeMetadata.filters as Array<
|
||||
(updatedUser?.unsafeMetadata.filters as Array<
|
||||
ClerkEmbeddedFilter<unknown>
|
||||
>) ?? [];
|
||||
existingArray.splice(existing, 1);
|
||||
const previousFilters = updatedUser?.unsafeMetadata
|
||||
.filters as Array<ClerkEmbeddedFilter<unknown>>;
|
||||
if (existing === -1)
|
||||
await user.update({
|
||||
unsafeMetadata: {
|
||||
@ -86,19 +89,19 @@ export function ModificationAction({ value }: { value?: Action }) {
|
||||
type: filter.getSpecificFilterId(),
|
||||
metadata: filter.toIdentifier(),
|
||||
},
|
||||
...((user.unsafeMetadata.filters as Array<
|
||||
ClerkEmbeddedFilter<unknown>
|
||||
>) ?? []),
|
||||
...previousFilters,
|
||||
] as Array<ClerkEmbeddedFilter<unknown>>,
|
||||
},
|
||||
});
|
||||
else
|
||||
else {
|
||||
existingArray.splice(existing, 1);
|
||||
await user.update({
|
||||
unsafeMetadata: {
|
||||
filters: existingArray,
|
||||
...user.unsafeMetadata,
|
||||
filters: existingArray,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const existingArray =
|
||||
(JSON.parse(
|
||||
@ -110,17 +113,20 @@ export function ModificationAction({ value }: { value?: Action }) {
|
||||
localStorage.setItem(
|
||||
"mhsf__filters",
|
||||
JSON.stringify([
|
||||
...((JSON.parse(
|
||||
localStorage.getItem("mhsf__filters") ?? "[]",
|
||||
) as Array<ClerkEmbeddedFilter<unknown>>) ?? []),
|
||||
{
|
||||
type: filter.getSpecificFilterId(),
|
||||
metadata: filter.toIdentifier(),
|
||||
},
|
||||
...((JSON.parse(
|
||||
localStorage.getItem("mhsf__filters") ?? "[]",
|
||||
) as Array<ClerkEmbeddedFilter<unknown>>) ?? []),
|
||||
]),
|
||||
);
|
||||
else
|
||||
localStorage.setItem("mhsf__filters", JSON.stringify(existingArray));
|
||||
localStorage.setItem(
|
||||
"mhsf__filters",
|
||||
JSON.stringify(existingArray),
|
||||
);
|
||||
}
|
||||
|
||||
setApplied(findExisting());
|
||||
|
||||
@ -35,7 +35,7 @@ import { ModificationFrame } from "./modification-frame";
|
||||
export function ModificationButton({disabled}: {disabled?: boolean}) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<DialogTrigger asChild>
|
||||
<Button disabled={disabled}>Filters & Sorting</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
|
||||
@ -42,6 +42,7 @@ export function ModificationFrame() {
|
||||
communication.toIframe.send("ping", {from: "top-layer"})
|
||||
})
|
||||
communication.toIframe.handle("rerender-servers", (c) => {
|
||||
window.dispatchEvent(new Event("start-loading-server-view"))
|
||||
window.dispatchEvent(new Event("update-modification-stack"))
|
||||
})
|
||||
}, [ref])
|
||||
|
||||
@ -39,7 +39,7 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { useEffectOnce } from "@/lib/useEffectOnce";
|
||||
import { allTags } from "@/config/tags";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
@ -176,6 +176,7 @@ export function TagShower(props: {
|
||||
|
||||
useEffectOnce(() => {
|
||||
if (loading) {
|
||||
// biome-ignore lint/complexity/noForEach: no.
|
||||
allTags.forEach((tag) => {
|
||||
if (!tag.condition) {
|
||||
tag.name({ online: props.server }).then((n) => {
|
||||
@ -246,7 +247,7 @@ export function TagShower(props: {
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{'"'}
|
||||
{t.docsName == undefined ? t.name : t.docsName}
|
||||
{t.docsName === undefined ? t.name : t.docsName}
|
||||
{'"'} documentation
|
||||
</DialogTitle>
|
||||
<DialogDescription
|
||||
|
||||
@ -44,19 +44,34 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} 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() {
|
||||
const { servers, loading, serverCount, playerCount } = useServers();
|
||||
const { servers, loading, serverCount, playerCount, refresh } = useServers();
|
||||
const {
|
||||
filteredData,
|
||||
testModeEnabled,
|
||||
testModeLoading,
|
||||
testModeStatus,
|
||||
filterCount,
|
||||
tagStrings,
|
||||
loading: filterLoading,
|
||||
} = useFilters(servers);
|
||||
const { itemsLength, fetchMoreData, hasMoreData, data } =
|
||||
useInfiniteScrolling(filteredData);
|
||||
const clipboard = useClipboard();
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
@ -67,53 +82,119 @@ export function ServerList() {
|
||||
|
||||
return (
|
||||
<main className="px-3 lg:px-16">
|
||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
||||
Statistics
|
||||
</h1>
|
||||
<Statistics
|
||||
totalServers={serverCount}
|
||||
totalPlayers={playerCount}
|
||||
topServer={servers[0]}
|
||||
/>
|
||||
<Separator className="my-6" />
|
||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl">
|
||||
Servers
|
||||
</h1>
|
||||
<div className="flex items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<ModificationButton disabled={testModeEnabled} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="backdrop-blur bg-transparent text-black dark:text-white ">{filterCount} modification(s) enabled</TooltipContent>
|
||||
</Tooltip>
|
||||
<ServerTestModeSelector
|
||||
testModeStatus={testModeStatus}
|
||||
testModeEnabled={testModeEnabled}
|
||||
testModeLoading={testModeLoading}
|
||||
<ServerRandomServerProvider servers={filteredData}>
|
||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
||||
Statistics
|
||||
</h1>
|
||||
<Statistics
|
||||
totalServers={serverCount}
|
||||
totalPlayers={playerCount}
|
||||
topServer={servers[0]}
|
||||
/>
|
||||
</div>
|
||||
{filterLoading ? (
|
||||
<span className="mt-2 left-[50%] right-[50%] absolute">
|
||||
<Spinner />
|
||||
</span>
|
||||
) : (
|
||||
<InfiniteScroll
|
||||
dataLength={itemsLength}
|
||||
next={fetchMoreData}
|
||||
hasMore={hasMoreData}
|
||||
loader={
|
||||
<span className="mt-2 left-[50%] right-[50%] absolute">
|
||||
<Spinner />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 mt-3">
|
||||
{data.map((c) => (
|
||||
<ServerCard server={c} key={c.staticInfo._id} />
|
||||
))}
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
<Separator className="my-6" />
|
||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl">
|
||||
Servers
|
||||
</h1>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<ModificationButton disabled={testModeEnabled} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
className="backdrop-blur bg-transparent text-black dark:text-white "
|
||||
>
|
||||
{filterCount} modification(s) enabled
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<ServerTestModeSelector
|
||||
testModeStatus={testModeStatus}
|
||||
testModeEnabled={testModeEnabled}
|
||||
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>
|
||||
{filterLoading ? (
|
||||
<span className="mt-2 left-[50%] right-[50%] absolute">
|
||||
<Spinner />
|
||||
</span>
|
||||
) : (
|
||||
<InfiniteScroll
|
||||
dataLength={itemsLength}
|
||||
next={fetchMoreData}
|
||||
hasMore={hasMoreData}
|
||||
loader={
|
||||
<span className="mt-2 left-[50%] right-[50%] absolute">
|
||||
<Spinner />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 mt-3">
|
||||
{data.map((c) => (
|
||||
<ServerCard server={c} key={c.staticInfo._id} />
|
||||
))}
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</ServerRandomServerProvider>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -28,61 +28,40 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
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" },
|
||||
],
|
||||
},
|
||||
];
|
||||
"use client";
|
||||
|
||||
export type Docs = {
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import type { OnlineServer } from "@/lib/types/mh-server";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { MOTDRenderer } from "../server-page/motd/motd-renderer";
|
||||
import IconDisplay from "../icons/minecraft-icon-display";
|
||||
import ServerCard, { TagShower } from "./server-card";
|
||||
|
||||
export type DocsFolder = {
|
||||
name: string;
|
||||
docs: Array<Docs>;
|
||||
};
|
||||
export function ServerRandomServerProvider({
|
||||
servers,
|
||||
children,
|
||||
}: { 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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -73,7 +73,6 @@ export function Statistics({
|
||||
setAverages(fetchJson);
|
||||
})();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setError(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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;
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
139
apps/www/src/components/feat/server-page/icons/icons-row.tsx
Normal file
139
apps/www/src/components/feat/server-page/icons/icons-row.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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,22 +39,13 @@ import { Material } from "@/components/ui/material";
|
||||
import { useState } from "react";
|
||||
import { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
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({
|
||||
server,
|
||||
mhsfData,
|
||||
}: { server: ServerResponse; mhsfData: ReturnType<typeof useMHSFServer> }) {
|
||||
const clipboard = useClipboard();
|
||||
const [tab, setTab] = useState("motd");
|
||||
const [tab, setTab] = useState(mhsfData.server?.customizationData.description !== undefined ? "description" : "motd");
|
||||
|
||||
return (
|
||||
<Material className="p-4 relative h-[250px]">
|
||||
@ -81,7 +72,7 @@ export function MOTDRow({
|
||||
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
|
||||
"rounded-xl px-2 flex items-center gap-2",
|
||||
tab === "description" &&
|
||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
|
||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium",
|
||||
)}
|
||||
onClick={() => setTab("description")}
|
||||
>
|
||||
@ -118,8 +109,8 @@ export function MOTDRow({
|
||||
</>
|
||||
)}
|
||||
{tab === "description" && (
|
||||
<div className="prose mt-2 break-words overflow-y-auto max-h-[175px] dark:prose-invert">
|
||||
<Markdown>{mhsfData.server?.customizationData.description}</Markdown>
|
||||
<div className="prose mt-2 break-words overflow-y-auto max-h-[175px] min-w-full dark:prose-invert">
|
||||
<Markdown className="min-w-full">{mhsfData.server?.customizationData.description}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</Material>
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
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,93 +31,96 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ServerResponse } from "@/lib/types/mh-server";
|
||||
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
||||
import { EllipsisVertical, Flag, Heart, Star } from "lucide-react";
|
||||
import { EllipsisVertical, Flag, Heart, Share, Star } from "lucide-react";
|
||||
import { useFavoriteStore } from "@/lib/hooks/use-favorite-store";
|
||||
import { useState } from "react";
|
||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
import NumberFlow from "@number-flow/react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ServerPageButtons({
|
||||
server,
|
||||
mhsfData,
|
||||
server,
|
||||
mhsfData,
|
||||
}: {
|
||||
server: ServerResponse;
|
||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||
server: ServerResponse;
|
||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||
}) {
|
||||
const clerk = useClerk();
|
||||
const favoritesStore = useFavoriteStore(mhsfData);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const clerk = useClerk();
|
||||
const favoritesStore = useFavoriteStore(mhsfData);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<SignedIn>
|
||||
<Button
|
||||
className="flex items-center gap-2 text-sm"
|
||||
variant={favoritesStore.isFavorite ? "secondary" : "default"}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
await favoritesStore.toggleFavorite();
|
||||
setLoading(false);
|
||||
}}
|
||||
disabled={loading || favoritesStore.isFavorite === null}
|
||||
>
|
||||
<Heart
|
||||
size={16}
|
||||
fill={favoritesStore.isFavorite ? "red" : "transparent"}
|
||||
color="red"
|
||||
/>
|
||||
Favorite
|
||||
{favoritesStore.favoriteNumber !== null && (
|
||||
<code>
|
||||
<NumberFlow value={favoritesStore.favoriteNumber} />{" "}
|
||||
</code>
|
||||
)}
|
||||
</Button>
|
||||
</SignedIn>
|
||||
<SignedOut>
|
||||
<Button
|
||||
className="flex items-center gap-2 text-sm"
|
||||
onClick={() => clerk.openSignUp()}
|
||||
>
|
||||
<Star size={16} />
|
||||
Favorite
|
||||
{favoritesStore.favoriteNumber !== null && (
|
||||
<code>{favoritesStore.favoriteNumber}</code>
|
||||
)}
|
||||
</Button>
|
||||
</SignedOut>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
className="flex items-center"
|
||||
size="square-md"
|
||||
variant="secondary"
|
||||
>
|
||||
<EllipsisVertical size={16} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuSeparator>
|
||||
Destructive
|
||||
</DropdownMenuSeparator>
|
||||
<DropdownMenuItem
|
||||
className="text-red-400 flex items-center gap-2"
|
||||
onClick={() => {
|
||||
window.dispatchEvent(new Event("open-report-menu"));
|
||||
}}
|
||||
>
|
||||
<Flag size={16} />
|
||||
Report
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<SignedIn>
|
||||
<Button
|
||||
className="flex items-center gap-2 text-sm"
|
||||
variant={favoritesStore.isFavorite ? "secondary" : "default"}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
await favoritesStore.toggleFavorite();
|
||||
setLoading(false);
|
||||
}}
|
||||
disabled={loading || favoritesStore.isFavorite === null}
|
||||
>
|
||||
<Heart
|
||||
size={16}
|
||||
fill={favoritesStore.isFavorite ? "red" : "transparent"}
|
||||
color="red"
|
||||
/>
|
||||
Favorite
|
||||
{favoritesStore.favoriteNumber !== null && (
|
||||
<code>
|
||||
<NumberFlow value={favoritesStore.favoriteNumber} />{" "}
|
||||
</code>
|
||||
)}
|
||||
</Button>
|
||||
</SignedIn>
|
||||
<SignedOut>
|
||||
<Button
|
||||
className="flex items-center gap-2 text-sm"
|
||||
onClick={() => clerk.openSignUp()}
|
||||
>
|
||||
<Star size={16} />
|
||||
Favorite
|
||||
{favoritesStore.favoriteNumber !== null && (
|
||||
<code>{favoritesStore.favoriteNumber}</code>
|
||||
)}
|
||||
</Button>
|
||||
</SignedOut>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
className="flex items-center"
|
||||
size="square-md"
|
||||
variant="secondary"
|
||||
>
|
||||
<EllipsisVertical size={16} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuSeparator>Server</DropdownMenuSeparator>
|
||||
<DropdownMenuItem className="flex items-center gap-2">
|
||||
<Share size={16} />
|
||||
Share
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator>Destructive</DropdownMenuSeparator>
|
||||
<DropdownMenuItem
|
||||
className="text-red-400 flex items-center gap-2"
|
||||
onClick={() => {
|
||||
window.dispatchEvent(new Event("open-report-menu"));
|
||||
}}
|
||||
>
|
||||
<Flag size={16} />
|
||||
Report
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,71 +11,77 @@ import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DebugProvider } from "./debug/debug-provider";
|
||||
import { ReportingProvider } from "./reporting/reporting-provider";
|
||||
import { ServerEditorProvider } from "./server-editor/server-editor-provider";
|
||||
|
||||
export function ServerProvider({ serverId }: { serverId: string }) {
|
||||
const { server, error, loading } = useServer({ id: serverId });
|
||||
const settings = useSettingsStore();
|
||||
const mhsf = useMHSFServer(serverId);
|
||||
const { server, error, loading } = useServer({ id: serverId });
|
||||
const settings = useSettingsStore();
|
||||
const mhsf = useMHSFServer(serverId);
|
||||
|
||||
if (error !== null)
|
||||
return (
|
||||
<div className="absolute top-[50%] left-[50%]">
|
||||
<Placeholder
|
||||
icon={<X />}
|
||||
title="Error while fetching server"
|
||||
description={
|
||||
<>
|
||||
Try again later <br /> If this occurs again, please contact
|
||||
support or make a GitHub issue. <br /> {error}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
if (error !== null)
|
||||
return (
|
||||
<div className="absolute top-[50%] left-[50%]">
|
||||
<Placeholder
|
||||
icon={<X />}
|
||||
title="Error while fetching server"
|
||||
description={
|
||||
<>
|
||||
Try again later <br /> If this occurs again, please contact
|
||||
support or make a GitHub issue. <br /> {error}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<DebugProvider
|
||||
debugOptions={{
|
||||
serverName: (server ?? { name: "" }).name,
|
||||
serverId: serverId,
|
||||
mhsfData: mhsf.server,
|
||||
serverData: server,
|
||||
onlineServerData: null,
|
||||
}}
|
||||
>
|
||||
{loading || mhsf.loading ? (
|
||||
<div className="absolute top-[50%] left-[50%] transform -translate-x-1/2 -translate-y-1/2 block justify-center text-center gap-2">
|
||||
<span className="w-full flex justify-center">
|
||||
<Spinner />
|
||||
</span>
|
||||
return (
|
||||
<DebugProvider
|
||||
debugOptions={{
|
||||
serverName: (server ?? { name: "" }).name,
|
||||
serverId: serverId,
|
||||
mhsfData: mhsf.server,
|
||||
serverData: server,
|
||||
onlineServerData: null,
|
||||
}}
|
||||
>
|
||||
{loading || mhsf.loading ? (
|
||||
<div className="absolute top-[50%] left-[50%] transform -translate-x-1/2 -translate-y-1/2 block justify-center text-center gap-2">
|
||||
<span className="w-full flex justify-center">
|
||||
<Spinner />
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<AnimatedText
|
||||
text={
|
||||
loading && mhsf.loading
|
||||
? "Loading server and MHSF data..."
|
||||
: loading
|
||||
? "Loading server data..."
|
||||
: "Loading MHSF data..."
|
||||
}
|
||||
className="text-center w-full mt-2"
|
||||
/>
|
||||
</span>
|
||||
{settings.get("debug-mode") === "true" && (
|
||||
<Button
|
||||
onClick={() => window.dispatchEvent(new Event("open-debug-menu"))}
|
||||
>
|
||||
Debug Stack
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-10">
|
||||
<ReportingProvider server={mhsf}>
|
||||
<ServerMainPage server={server as ServerResponse} mhsfData={mhsf} />
|
||||
</ReportingProvider>
|
||||
</div>
|
||||
)}
|
||||
</DebugProvider>
|
||||
);
|
||||
<span>
|
||||
<AnimatedText
|
||||
text={
|
||||
loading && mhsf.loading
|
||||
? "Loading server and MHSF data..."
|
||||
: loading
|
||||
? "Loading server data..."
|
||||
: "Loading MHSF data..."
|
||||
}
|
||||
className="text-center w-full mt-2"
|
||||
/>
|
||||
</span>
|
||||
{settings.get("debug-mode") === "true" && (
|
||||
<Button
|
||||
onClick={() => window.dispatchEvent(new Event("open-debug-menu"))}
|
||||
>
|
||||
Debug Stack
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-10">
|
||||
<ServerEditorProvider>
|
||||
<ReportingProvider server={mhsf}>
|
||||
<ServerMainPage
|
||||
server={server as ServerResponse}
|
||||
mhsfData={mhsf}
|
||||
/>
|
||||
</ReportingProvider>
|
||||
</ServerEditorProvider>
|
||||
</div>
|
||||
)}
|
||||
</DebugProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -33,14 +33,25 @@ import useClipboard from "@/lib/useClipboard";
|
||||
import { MOTDRow } from "./motd/motd-row";
|
||||
import { StatisticsMainRow } from "./stats/stats-main-row";
|
||||
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> }) {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return (
|
||||
<span className="lg:grid lg:grid-cols-2 w-full gap-3">
|
||||
{affiliates.includes(server.name) && <AffiliateRow />}
|
||||
<MOTDRow 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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -126,9 +126,8 @@ export function StatisticsChart({
|
||||
data: any;
|
||||
mainDataPoint: string;
|
||||
}) {
|
||||
console.log(data);
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="max-h-[202px] min-w-full">
|
||||
<ChartContainer config={chartConfig} className="max-h-[202px] max-lg:max-h-[177px] min-w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={data.slice(data.length - 30, data.length)}
|
||||
|
||||
@ -40,6 +40,8 @@ export function convert(value: number) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export const affiliates = ["CoreBoxx"]
|
||||
|
||||
export const loadingList = [
|
||||
"Making gamer's safer",
|
||||
"Finding why Apple is so expensive",
|
||||
|
||||
@ -12,7 +12,7 @@ export function StatusButton() {
|
||||
if (loading) return <Spinner />;
|
||||
|
||||
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">
|
||||
<span
|
||||
className={cn(
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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,
|
||||
useSignIn,
|
||||
useUser,
|
||||
} from "@clerk/nextjs";
|
||||
import type { CreateExternalAccountParams, OAuthStrategy } from "@clerk/types";
|
||||
import { UserInformation } from "./waitlist-page";
|
||||
import { useEffect, 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 { isLoaded, 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
189
apps/www/src/components/feat/waitlist/waitlist-page.tsx
Normal file
189
apps/www/src/components/feat/waitlist/waitlist-page.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
106
apps/www/src/components/feat/waitlist/waitlist-referral-beta.tsx
Normal file
106
apps/www/src/components/feat/waitlist/waitlist-referral-beta.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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>;
|
||||
}
|
||||
90
apps/www/src/components/feat/waitlist/waitlist-success.tsx
Normal file
90
apps/www/src/components/feat/waitlist/waitlist-success.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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
|
||||
ring-purple-400 dark:ring-purple-500/30`,
|
||||
rainbow:
|
||||
"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 ",
|
||||
"text-white ring-transparent z-10 bg-blur-[15px] rainbow-tag",
|
||||
custom: "",
|
||||
},
|
||||
allowIconOnly: {
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
function Placeholder({
|
||||
@ -35,14 +36,16 @@ function Placeholder({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className
|
||||
}: {
|
||||
icon?: ReactNode;
|
||||
title?: ReactNode | string;
|
||||
description?: ReactNode | string;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="text-slate-700 dark:text-zinc-300 flex flex-col justify-center items-center gap-2">
|
||||
<div className={cn("text-slate-700 dark:text-zinc-300 flex flex-col justify-center items-center gap-2", className)}>
|
||||
{icon && (
|
||||
<div className="border border-slate-200 dark:border-zinc-700 dark:bg-zinc-800 p-3 rounded-full">
|
||||
{icon}
|
||||
|
||||
@ -37,45 +37,69 @@ import { usePathname } from "next/navigation";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
const roboto = Roboto({
|
||||
subsets: ["latin"],
|
||||
weight: ["100", "300", "400", "500", "700", "900"],
|
||||
subsets: ["latin"],
|
||||
weight: ["100", "300", "400", "500", "700", "900"],
|
||||
});
|
||||
const overflowXHiddenPages = ["/home"];
|
||||
|
||||
export function FontBoundary({
|
||||
children,
|
||||
className
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: ReactNode | ReactNode[];
|
||||
className?: string;
|
||||
children?: ReactNode | ReactNode[];
|
||||
className?: string;
|
||||
}) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const [fontFamily, setFontFamily] = useState("inter");
|
||||
const pathname = usePathname();
|
||||
const settingsStore = useSettingsStore();
|
||||
const [fontFamily, setFontFamily] = useState("inter");
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
||||
window.addEventListener("font-family-change", () => {
|
||||
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
||||
});
|
||||
}, [settingsStore]);
|
||||
useEffect(() => {
|
||||
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
||||
window.addEventListener("font-family-change", () => {
|
||||
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
||||
});
|
||||
}, [settingsStore]);
|
||||
|
||||
return (
|
||||
<body
|
||||
className={`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";
|
||||
}
|
||||
})()} ${pathname !== null && overflowXHiddenPages.includes(pathname) ? "overflow-x-hidden" : ""} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
);
|
||||
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 (
|
||||
<div
|
||||
className={`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";
|
||||
}
|
||||
})()} overflow-x-hidden ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ export function Link(
|
||||
props: LinkProps & {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
noExtraIcons?: boolean;
|
||||
noextraicons?: boolean;
|
||||
target?: string;
|
||||
}
|
||||
) {
|
||||
@ -43,7 +43,7 @@ export function Link(
|
||||
|
||||
return (
|
||||
<NextLink {...props} href={pageFind(href || "") || "#"} title={href}>
|
||||
{!props.noExtraIcons && (
|
||||
{!props.noextraicons && (
|
||||
<>
|
||||
{(href || "").startsWith("Docs:") && (
|
||||
<Book size={16} className="mr-[2px] inline-flex" />
|
||||
@ -56,7 +56,7 @@ export function Link(
|
||||
|
||||
{props.children}
|
||||
|
||||
{!props.noExtraIcons && (href || "").startsWith("https") && (
|
||||
{!props.noextraicons && (href || "").startsWith("https") && (
|
||||
<ExternalLink size={12} className="ml-[2px] mb-[3px] inline-flex" />
|
||||
)}
|
||||
</NextLink>
|
||||
|
||||
@ -1,93 +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 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;
|
||||
},
|
||||
];
|
||||
@ -30,68 +30,132 @@
|
||||
|
||||
import {
|
||||
ArrowDownUpIcon,
|
||||
Database,
|
||||
DatabaseZap,
|
||||
HardDriveDownload,
|
||||
List,
|
||||
ServerCog,
|
||||
SlidersHorizontal,
|
||||
UserPlus,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { type Filter } from "../lib/types/filter";
|
||||
import type { Sort } from "../lib/types/sort";
|
||||
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 = {
|
||||
displayTitle: string;
|
||||
description: string;
|
||||
__custom?: boolean;
|
||||
__custom?: boolean;
|
||||
entries: {
|
||||
name: string;
|
||||
icon: LucideIcon;
|
||||
color: string;
|
||||
color: string;
|
||||
value: Filter | Sort | { customAction: string };
|
||||
description: string;
|
||||
description: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const serverModDB: ModDBCategory[] = [
|
||||
{
|
||||
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.`,
|
||||
entries: [
|
||||
{
|
||||
name: "Create Sort",
|
||||
icon: ArrowDownUpIcon,
|
||||
value: { customAction: "custom-sort" },
|
||||
color: "#a3a68b",
|
||||
description: "Create a new custom sort system using TypeScript, completely from the comfort of your own browser."
|
||||
color: "#a3a68b",
|
||||
description:
|
||||
"Create a new custom sort system using TypeScript, completely from the comfort of your own browser.",
|
||||
},
|
||||
{
|
||||
name: "Create Filter",
|
||||
icon: SlidersHorizontal,
|
||||
value: { customAction: "custom-filter" },
|
||||
color: "#a3a68b",
|
||||
description: "Create a new custom filtering system using TypeScript, completely from the comfort of your own browser."
|
||||
color: "#a3a68b",
|
||||
description:
|
||||
"Create a new custom filtering system using TypeScript, completely from the comfort of your own browser.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayTitle: "Custom Files",
|
||||
description: "These are all of your activated modifications made in the editor.",
|
||||
description:
|
||||
"These are all of your activated modifications made in the editor.",
|
||||
__custom: true,
|
||||
// Entries are already pre-loaded.
|
||||
entries: []
|
||||
entries: [],
|
||||
},
|
||||
{
|
||||
displayTitle: "Tag Filters",
|
||||
description: "These are filters that are associated with an assortment of tags.",
|
||||
description:
|
||||
"These are filters that are associated with an assortment of tags.",
|
||||
entries: [
|
||||
{
|
||||
name: "Always Online",
|
||||
description: "All servers that are always online.",
|
||||
color: "#a380e0",
|
||||
value: new TagFilter(2, false),
|
||||
icon: ServerCog
|
||||
}
|
||||
]
|
||||
}
|
||||
value: new TagFilter(3, false),
|
||||
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,9 +29,10 @@
|
||||
*/
|
||||
|
||||
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 type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
||||
import { Cake, ServerCog } from "lucide-react";
|
||||
import { Cake, HardDriveDownload, ServerCog } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const serverCache: any = {};
|
||||
@ -49,168 +50,193 @@ const serverCache: any = {};
|
||||
//
|
||||
// You may also use `requestServer()` to grab the offline version of the server from the API, which may get you more information about the server (ServerResponse)
|
||||
export const allTags: Array<{
|
||||
name: (server: {
|
||||
online?: OnlineServer;
|
||||
server?: ServerResponse;
|
||||
}) => Promise<string | ReactNode>;
|
||||
condition?: (server: {
|
||||
online?: OnlineServer;
|
||||
server?: ServerResponse;
|
||||
mhsfData?: MHSFData;
|
||||
}) => Promise<boolean>;
|
||||
tooltipDesc: string;
|
||||
htmlDocs: string;
|
||||
docsName: string;
|
||||
role?: BadgeColor;
|
||||
__disab?: boolean;
|
||||
__filter?: boolean;
|
||||
name: (server: {
|
||||
online?: OnlineServer;
|
||||
server?: ServerResponse;
|
||||
}) => Promise<string | ReactNode>;
|
||||
condition?: (server: {
|
||||
online?: OnlineServer;
|
||||
server?: ServerResponse;
|
||||
mhsfData?: MHSFData;
|
||||
}) => Promise<boolean>;
|
||||
tooltipDesc: string;
|
||||
htmlDocs: string;
|
||||
docsName: string;
|
||||
role?: BadgeColor;
|
||||
__disab?: boolean;
|
||||
__filter?: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: async (c) => (
|
||||
<>
|
||||
<div
|
||||
className="items-center bg-green-700 dark:bg-green-400"
|
||||
style={{
|
||||
width: ".4rem",
|
||||
height: ".4rem",
|
||||
borderRadius: "9999px",
|
||||
}}
|
||||
/>
|
||||
{String(
|
||||
c.online === undefined
|
||||
? c.server?.playerCount
|
||||
: c.online.playerData.playerCount
|
||||
)}{" "}
|
||||
online
|
||||
</>
|
||||
),
|
||||
condition: async (c) =>
|
||||
(c.online === undefined
|
||||
? c.server?.playerCount
|
||||
: c.online.playerData.playerCount) !== 0,
|
||||
htmlDocs:
|
||||
"'Players Online' specifies the amount of players currently online. If this server is a network, the amount of players may not be accurate as this counter only counts the number of players coming directly from Minehut",
|
||||
tooltipDesc:
|
||||
"'Players Online' specifies the amount of players currently online.",
|
||||
{
|
||||
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) => (
|
||||
<>
|
||||
<div
|
||||
className="items-center bg-green-700 dark:bg-green-400"
|
||||
style={{
|
||||
width: ".4rem",
|
||||
height: ".4rem",
|
||||
borderRadius: "9999px",
|
||||
}}
|
||||
/>
|
||||
{String(
|
||||
c.online === undefined
|
||||
? c.server?.playerCount
|
||||
: c.online.playerData.playerCount,
|
||||
)}{" "}
|
||||
online
|
||||
</>
|
||||
),
|
||||
condition: async (c) =>
|
||||
(c.online === undefined
|
||||
? c.server?.playerCount
|
||||
: c.online.playerData.playerCount) !== 0,
|
||||
htmlDocs:
|
||||
"'Players Online' specifies the amount of players currently online. If this server is a network, the amount of players may not be accurate as this counter only counts the number of players coming directly from Minehut",
|
||||
tooltipDesc:
|
||||
"'Players Online' specifies the amount of players currently online.",
|
||||
|
||||
role: "green-subtle",
|
||||
docsName: "Players Online",
|
||||
__filter: true,
|
||||
},
|
||||
{
|
||||
name: async (c) => (
|
||||
<>
|
||||
<div
|
||||
className="items-center bg-gray-700 dark:bg-gray-300"
|
||||
style={{
|
||||
width: ".4rem",
|
||||
height: ".4rem",
|
||||
borderRadius: "9999px",
|
||||
}}
|
||||
/>{" "}
|
||||
0 online
|
||||
</>
|
||||
),
|
||||
condition: async (c) =>
|
||||
(c.online === undefined ? c.server?.playerCount: c.online.playerData.playerCount) ===
|
||||
0,
|
||||
htmlDocs: "Nobody is online this server.",
|
||||
tooltipDesc: "Nobody is online this server.",
|
||||
role: "gray-subtle",
|
||||
docsName: "Nobody Online",
|
||||
__filter: true,
|
||||
},
|
||||
{
|
||||
name: async () => (
|
||||
<>
|
||||
<ServerCog size={16} />
|
||||
Always Online
|
||||
</>
|
||||
),
|
||||
condition: async (b) =>
|
||||
b.online !== undefined && b.online.staticInfo?.alwaysOnline,
|
||||
tooltipDesc:
|
||||
'"Always online" means that the server will not shut down until the plan associated with it expires.',
|
||||
htmlDocs: `
|
||||
role: "green-subtle",
|
||||
docsName: "Players Online",
|
||||
__filter: true,
|
||||
},
|
||||
{
|
||||
name: async (c) => (
|
||||
<>
|
||||
<div
|
||||
className="items-center bg-gray-700 dark:bg-gray-300"
|
||||
style={{
|
||||
width: ".4rem",
|
||||
height: ".4rem",
|
||||
borderRadius: "9999px",
|
||||
}}
|
||||
/>{" "}
|
||||
0 online
|
||||
</>
|
||||
),
|
||||
condition: async (c) =>
|
||||
(c.online === undefined
|
||||
? c.server?.playerCount
|
||||
: c.online.playerData.playerCount) === 0,
|
||||
htmlDocs: "Nobody is online this server.",
|
||||
tooltipDesc: "Nobody is online this server.",
|
||||
role: "gray-subtle",
|
||||
docsName: "Nobody Online",
|
||||
__filter: true,
|
||||
},
|
||||
{
|
||||
name: async () => (
|
||||
<>
|
||||
<ServerCog size={16} />
|
||||
Always Online
|
||||
</>
|
||||
),
|
||||
condition: async (b) =>
|
||||
b.online !== undefined && b.online.staticInfo?.alwaysOnline,
|
||||
tooltipDesc:
|
||||
'"Always online" means that the server will not shut down until the plan associated with it expires.',
|
||||
htmlDocs: `
|
||||
This tag appears on servers where the plan they are under allows the server to be always online. However, if the plan associated with the tag expires, the server will no longer be Always Online. <em>This is in servers with one of the more expensive plans, or just a server that is external.</em>
|
||||
`,
|
||||
|
||||
docsName: "Always Online",
|
||||
role: "blue-subtle",
|
||||
__disab: true,
|
||||
},
|
||||
{
|
||||
name: async (s) =>
|
||||
(s.online !== undefined
|
||||
? s.online.staticInfo.planMaxPlayers
|
||||
: s.server?.maxPlayers) + " max players",
|
||||
condition: async (s) =>
|
||||
s.online !== undefined
|
||||
? s.online.staticInfo.planMaxPlayers != null
|
||||
: s.server?.maxPlayers != null,
|
||||
tooltipDesc:
|
||||
"This tag represents the maximum amount of players the server can have at one time.",
|
||||
docsName: "Max Players",
|
||||
htmlDocs:
|
||||
"This tag represents the maximum amount of players the server can have at one time. This doesn't mean the amount of players before the server crashes, it means the amount Minehut said the server can handle or the plan the server is on. <em>However, sometimes it might not appear because the server is external.</em>",
|
||||
docsName: "Always Online",
|
||||
role: "blue-subtle",
|
||||
__disab: true,
|
||||
},
|
||||
{
|
||||
name: async (s) =>
|
||||
(s.online !== undefined
|
||||
? s.online.staticInfo.planMaxPlayers
|
||||
: s.server?.maxPlayers) + " max players",
|
||||
condition: async (s) =>
|
||||
s.online !== undefined
|
||||
? s.online.staticInfo.planMaxPlayers != null
|
||||
: s.server?.maxPlayers != null,
|
||||
tooltipDesc:
|
||||
"This tag represents the maximum amount of players the server can have at one time.",
|
||||
docsName: "Max Players",
|
||||
htmlDocs:
|
||||
"This tag represents the maximum amount of players the server can have at one time. This doesn't mean the amount of players before the server crashes, it means the amount Minehut said the server can handle or the plan the server is on. <em>However, sometimes it might not appear because the server is external.</em>",
|
||||
|
||||
role: "blue",
|
||||
__filter: true,
|
||||
},
|
||||
{
|
||||
name: async () => "Partner",
|
||||
condition: async (s) =>
|
||||
(s.server ?? s.online ?? { name: "" }).name === "CoreBoxx",
|
||||
tooltipDesc: "This server is a partner with MHSF.",
|
||||
docsName: "Partner",
|
||||
htmlDocs: "This tag represents that this server is a partner with MHSF.",
|
||||
role: "rainbow",
|
||||
},
|
||||
{
|
||||
name: async (s) => (
|
||||
<span className="capitalize">
|
||||
{(s.online !== undefined
|
||||
? s.online.staticInfo.serverPlan
|
||||
: (s.server?.server_plan ?? "")
|
||||
)
|
||||
.split(" ")[0]
|
||||
.split("_")[0]
|
||||
.toLocaleLowerCase()}
|
||||
</span>
|
||||
),
|
||||
tooltipDesc: "This tag represents the server plan this server is using.",
|
||||
docsName: "Server Plan",
|
||||
htmlDocs:
|
||||
"This tag represents the maximum amount of players the server can have at one time. This doesn't mean the amount of players before the server crashes, it means the amount Minehut said the server can handle or the plan the server is on. <em>However, sometimes it might not appear because the server is external.</em>",
|
||||
role: "blue",
|
||||
__filter: true,
|
||||
},
|
||||
{
|
||||
name: async () => "Partner",
|
||||
condition: async (s) =>
|
||||
affiliates.includes((s.server ?? s.online ?? { name: "" }).name),
|
||||
tooltipDesc: "This server is a partner with MHSF.",
|
||||
docsName: "Partner",
|
||||
htmlDocs: "This tag represents that this server is a partner with MHSF.",
|
||||
role: "rainbow",
|
||||
},
|
||||
{
|
||||
name: async (s) => (
|
||||
<span className="capitalize">
|
||||
{(s.online !== undefined
|
||||
? s.online.staticInfo.serverPlan
|
||||
: (s.server?.server_plan ?? "")
|
||||
)
|
||||
.split(" ")[0]
|
||||
.split("_")[0]
|
||||
.toLocaleLowerCase()}
|
||||
</span>
|
||||
),
|
||||
tooltipDesc: "This tag represents the server plan this server is using.",
|
||||
docsName: "Server Plan",
|
||||
htmlDocs:
|
||||
"This tag represents the maximum amount of players the server can have at one time. This doesn't mean the amount of players before the server crashes, it means the amount Minehut said the server can handle or the plan the server is on. <em>However, sometimes it might not appear because the server is external.</em>",
|
||||
|
||||
role: "red-subtle",
|
||||
__filter: true,
|
||||
},
|
||||
{
|
||||
name: async (s) => (
|
||||
<span className="flex items-center gap-2">
|
||||
<Cake size={16} /> Created {timeConverter(s.server?.creation)}
|
||||
</span>
|
||||
),
|
||||
condition: async (s) => s.server !== undefined,
|
||||
tooltipDesc: "This tag represents the date this server was created.",
|
||||
docsName: "Creation Date",
|
||||
htmlDocs: "This tag represents the date this server was created.",
|
||||
role: "gray",
|
||||
},
|
||||
{
|
||||
name: async (s) => "Favorited",
|
||||
condition: async (s) =>
|
||||
(s.mhsfData ?? { favoriteData: { favoritedByAccount: false } })
|
||||
.favoriteData.favoritedByAccount ?? false,
|
||||
tooltipDesc: "This tag represents that you favorited this server.",
|
||||
docsName: "Favorited",
|
||||
htmlDocs:
|
||||
"This tag shows that you favorited this server in MHSF. The amount of favorites is publicly shown to other users using MHSF. We do not provide server owners with data about who favorites a server, unlike traditional voting systems.",
|
||||
role: "red",
|
||||
},
|
||||
// deprecated
|
||||
/**{
|
||||
role: "red-subtle",
|
||||
__filter: true,
|
||||
},
|
||||
{
|
||||
name: async (s) => (
|
||||
<span className="flex items-center gap-2">
|
||||
<Cake size={16} /> Created {timeConverter(s.server?.creation)}
|
||||
</span>
|
||||
),
|
||||
condition: async (s) => s.server !== undefined,
|
||||
tooltipDesc: "This tag represents the date this server was created.",
|
||||
docsName: "Creation Date",
|
||||
htmlDocs: "This tag represents the date this server was created.",
|
||||
role: "gray",
|
||||
},
|
||||
{
|
||||
name: async (s) => "Favorited",
|
||||
condition: async (s) =>
|
||||
(s.mhsfData ?? { favoriteData: { favoritedByAccount: false } })
|
||||
.favoriteData.favoritedByAccount ?? false,
|
||||
tooltipDesc: "This tag represents that you favorited this server.",
|
||||
docsName: "Favorited",
|
||||
htmlDocs:
|
||||
"This tag shows that you favorited this server in MHSF. The amount of favorites is publicly shown to other users using MHSF. We do not provide server owners with data about who favorites a server, unlike traditional voting systems.",
|
||||
role: "red",
|
||||
},
|
||||
// deprecated
|
||||
/**{
|
||||
name: async () => "Velocity",
|
||||
condition: async (s) => {
|
||||
var type = await requestServer(s);
|
||||
@ -227,179 +253,179 @@ export const allTags: Array<{
|
||||
];
|
||||
|
||||
export const allCategories: Array<{
|
||||
name: string;
|
||||
condition: (server: OnlineServer) => Promise<boolean>;
|
||||
role?: BadgeColor;
|
||||
name: string;
|
||||
condition: (server: OnlineServer) => Promise<boolean>;
|
||||
role?: BadgeColor;
|
||||
}> = [
|
||||
{
|
||||
name: "Farming",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("farming");
|
||||
},
|
||||
{
|
||||
name: "Farming",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("farming");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "SMP",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("smp");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "SMP",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("smp");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Factions",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("factions");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Factions",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("factions");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Meme",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("meme");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Meme",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("meme");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Puzzle",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("puzzle");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Puzzle",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("puzzle");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Box",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("box");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Box",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("box");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Minigames",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("minigames");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Minigames",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("minigames");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "RPG",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("rpg");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "RPG",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("rpg");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Parkour",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("parkour");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Parkour",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("parkour");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Lifesteal",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("lifesteal");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Lifesteal",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("lifesteal");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Prison",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("prison");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Prison",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("prison");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Gens",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("gens");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Gens",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("gens");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Skyblock",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("skyblock");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Skyblock",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("skyblock");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Roleplay",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("roleplay");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Roleplay",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("roleplay");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "PvP",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("pvp");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "PvP",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("pvp");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Modded",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("modded");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Modded",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("modded");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Creative",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("creative");
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
name: "Creative",
|
||||
condition: async (b: any) => {
|
||||
return b.allCategories.includes("creative");
|
||||
},
|
||||
|
||||
role: "default",
|
||||
},
|
||||
role: "default",
|
||||
},
|
||||
];
|
||||
|
||||
async function requestServer(s: OnlineServer): Promise<ServerResponse> {
|
||||
if (serverCache[s.name] === undefined) {
|
||||
const re = await fetch(
|
||||
"https://api.minehut.com/server/" + s.name + "?byName=true"
|
||||
);
|
||||
const json = await re.json();
|
||||
serverCache[s.name] = json.server;
|
||||
return json.server;
|
||||
}
|
||||
return serverCache[s.name];
|
||||
if (serverCache[s.name] === undefined) {
|
||||
const re = await fetch(
|
||||
"https://api.minehut.com/server/" + s.name + "?byName=true",
|
||||
);
|
||||
const json = await re.json();
|
||||
serverCache[s.name] = json.server;
|
||||
return json.server;
|
||||
}
|
||||
return serverCache[s.name];
|
||||
}
|
||||
|
||||
function timeConverter(UNIX_timestamp: any) {
|
||||
const a = new Date(UNIX_timestamp);
|
||||
const months = [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
];
|
||||
const year = a.getFullYear();
|
||||
const month = months[a.getMonth()];
|
||||
const date = a.getDate();
|
||||
const time = month + "/" + date + "/" + year;
|
||||
return time;
|
||||
const a = new Date(UNIX_timestamp);
|
||||
const months = [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
];
|
||||
const year = a.getFullYear();
|
||||
const month = months[a.getMonth()];
|
||||
const date = a.getDate();
|
||||
const time = month + "/" + date + "/" + year;
|
||||
return time;
|
||||
}
|
||||
|
||||
@ -67,7 +67,6 @@ export async function getBackendProcedure(request: NextApiRequest): Promise<Back
|
||||
|
||||
if (detectedIp !== null) {
|
||||
const collection = defaultDatabase.collection("blocked-ips");
|
||||
console.log(await collection.findOne({ ip: detectedIp }), detectedIp)
|
||||
|
||||
if (await collection.findOne({ ip: detectedIp }) !== null) {
|
||||
await mongoClient.close()
|
||||
|
||||
60
apps/www/src/lib/hooks/use-embed-generator.tsx
Normal file
60
apps/www/src/lib/hooks/use-embed-generator.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
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 type { ClerkCustomActivatedModification } from "@/components/feat/server-list/modification/modification-file-creation-dialog";
|
||||
import { ClerkEmbeddedFilter } from "@/components/feat/server-list/modification/modification-action";
|
||||
import { supportedFilters } from "../types/filter";
|
||||
import { supportedFilters } from "../types/supportedFilters";
|
||||
|
||||
type EmbeddedFilter = {
|
||||
identifier: string;
|
||||
@ -56,6 +56,7 @@ export function useFilters(data: OnlineServer[]) {
|
||||
const [testModeLoading, setTestModeLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState<EmbeddedFilter[]>([]);
|
||||
const [tagStrings, setTagStrings] = useState<string[]>([]);
|
||||
const [sort, setSort] = useState<SortFunction<OnlineServer> | null>(null);
|
||||
const { user, isSignedIn } = useUser();
|
||||
|
||||
@ -68,8 +69,6 @@ export function useFilters(data: OnlineServer[]) {
|
||||
);
|
||||
const sortedData = sort === null ? resultData : resultData.sort(sort);
|
||||
|
||||
console.log({ sortedData, modificationMap, resultData, data, newFilters });
|
||||
|
||||
if (sortedData.length !== 0) setFilteredData(sortedData);
|
||||
};
|
||||
|
||||
@ -167,11 +166,6 @@ export function useFilters(data: OnlineServer[]) {
|
||||
if (type === "sort") {
|
||||
newServers = data.sort((a, b) => filterFunc(a, b));
|
||||
setTestModeStatus(`Sorted ${newServers.length} servers.`);
|
||||
console.log(
|
||||
newServers,
|
||||
data.sort((a, b) => filterFunc(a, b)),
|
||||
);
|
||||
console.log(filterFunc);
|
||||
setFilteredData(() => [...newServers]);
|
||||
}
|
||||
|
||||
@ -209,15 +203,16 @@ export function useFilters(data: OnlineServer[]) {
|
||||
|
||||
// biome-ignore lint: I'm gonna turn this off :sob:
|
||||
useEffect(() => {
|
||||
window.addEventListener("start-loading-server-view", () => setLoading(true))
|
||||
if (!t)
|
||||
window.addEventListener("update-modification-stack", async () => {
|
||||
await user?.reload();
|
||||
setLoading(true);
|
||||
let newFilters: EmbeddedFilter[] = [];
|
||||
const filters =
|
||||
((isSignedIn ? user.unsafeMetadata.filters : JSON.parse(localStorage.getItem("mhsf__filters") ?? "[]")) as Array<
|
||||
ClerkEmbeddedFilter<unknown>
|
||||
>) ?? [];
|
||||
setTagStrings([]);
|
||||
|
||||
if (isSignedIn) {
|
||||
const activatedModifications =
|
||||
@ -309,15 +304,13 @@ export function useFilters(data: OnlineServer[]) {
|
||||
newFilters.push({
|
||||
identifier: filterType?.ns + (Math.random() * Math.random() * Math.random()).toString(),
|
||||
functionFilter: (server: OnlineServer) => parsedFilter?.applyToServer({ online: server }) ?? true
|
||||
})
|
||||
});
|
||||
setTagStrings((c) => [...c, ...(parsedFilter?.getTagStrings() as string[])])
|
||||
});
|
||||
|
||||
console.log(newFilters);
|
||||
|
||||
await updateServers(newFilters);
|
||||
});
|
||||
}, [data]);
|
||||
console.log(filters);
|
||||
|
||||
return {
|
||||
filteredData,
|
||||
@ -328,5 +321,6 @@ export function useFilters(data: OnlineServer[]) {
|
||||
filters.filter((item, index, array) => array.indexOf(item) === index)
|
||||
.length + (sort === null ? 1 : 0),
|
||||
loading,
|
||||
tagStrings
|
||||
};
|
||||
}
|
||||
|
||||
42
apps/www/src/lib/hooks/use-icons.tsx
Normal file
42
apps/www/src/lib/hooks/use-icons.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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,8 +40,6 @@ export function useMHSFServer(id: string) {
|
||||
const response = await fetch("/api/v1/server/get/" + id);
|
||||
const json = await response.json();
|
||||
|
||||
console.log(json.server);
|
||||
|
||||
setServer(json.server);
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
@ -14,7 +14,6 @@ export function useServer(serverSpecifier: { id?: string; name?: string }) {
|
||||
`https://api.minehut.com/server/${serverSpecifier.id || serverSpecifier.name}${serverSpecifier.name ? "?byName=true" : ""}`
|
||||
);
|
||||
const json = await res.json();
|
||||
console.log(json);
|
||||
if (json.server === null) throw new Error("Server not found");
|
||||
|
||||
setServer(json.server);
|
||||
|
||||
@ -61,6 +61,23 @@ export function useServers() {
|
||||
serverCount,
|
||||
loading,
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
13
apps/www/src/lib/types/filter-registry.ts
Normal file
13
apps/www/src/lib/types/filter-registry.ts
Normal file
@ -0,0 +1,13 @@
|
||||
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,36 +31,22 @@
|
||||
import { allTags } from "@/config/tags";
|
||||
import type { OnlineServer, ServerResponse } from "./mh-server";
|
||||
import type { MHSFData } from "./data";
|
||||
import { TagFilter } from "./filters/tag-filter";
|
||||
import { CategoryFilter } from "./filters/category-filter";
|
||||
|
||||
export type FilterIdentifier = {
|
||||
[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 */
|
||||
export interface Filter {
|
||||
type(): "filter";
|
||||
toIdentifier(): { [key: string]: string | number | boolean };
|
||||
toIdentifier(): FilterIdentifier;
|
||||
getSpecificFilterId(): string;
|
||||
fromIdentifier(identifier: {
|
||||
[key: string]: string | number | boolean;
|
||||
}): Filter;
|
||||
fromIdentifier(identifier: FilterIdentifier): Filter;
|
||||
applyToServer(server: {
|
||||
online?: OnlineServer;
|
||||
server?: ServerResponse;
|
||||
mhsfData?: MHSFData;
|
||||
}): 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 type { MHSFData } from "../data";
|
||||
import type { Filter } from "../filter";
|
||||
import type { Filter, FilterIdentifier } from "../filter";
|
||||
import type { OnlineServer, ServerResponse } from "../mh-server";
|
||||
|
||||
export class CategoryFilter implements Filter {
|
||||
@ -40,11 +40,11 @@ export class CategoryFilter implements Filter {
|
||||
return "filter";
|
||||
}
|
||||
|
||||
toIdentifier(): { [key: string]: string | number | boolean } {
|
||||
toIdentifier(): FilterIdentifier {
|
||||
return { categoryIndex: this.categoryIndex };
|
||||
}
|
||||
|
||||
fromIdentifier(identifier: { [key: string]: string | number | boolean; }): Filter {
|
||||
fromIdentifier(identifier: FilterIdentifier): Filter {
|
||||
return new CategoryFilter(identifier.categoryIndex as number);
|
||||
}
|
||||
|
||||
@ -61,4 +61,8 @@ export class CategoryFilter implements Filter {
|
||||
constructor(categoryIndex: number) {
|
||||
this.categoryIndex = categoryIndex;
|
||||
}
|
||||
|
||||
getTagStrings(): string[] {
|
||||
return [`Server is a ${allCategories[this.categoryIndex].name} server`]
|
||||
}
|
||||
}
|
||||
80
apps/www/src/lib/types/filters/combination-filter.ts
Normal file
80
apps/www/src/lib/types/filters/combination-filter.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
91
apps/www/src/lib/types/filters/player-range-filter.ts
Normal file
91
apps/www/src/lib/types/filters/player-range-filter.ts
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 type { MHSFData } from "../data";
|
||||
import type { OnlineServer, ServerResponse } from "../mh-server";
|
||||
import type { Filter } from "../filter";
|
||||
import type { Filter, FilterIdentifier } from "../filter";
|
||||
|
||||
export class TagFilter implements Filter {
|
||||
tagId: string;
|
||||
@ -41,7 +41,7 @@ export class TagFilter implements Filter {
|
||||
return "filter";
|
||||
}
|
||||
|
||||
toIdentifier(): { [key: string]: string | number | boolean } {
|
||||
toIdentifier(): FilterIdentifier {
|
||||
return { tagId: this.tagId, opposite: this.opposite };
|
||||
}
|
||||
|
||||
@ -49,9 +49,7 @@ export class TagFilter implements Filter {
|
||||
return "app.mhsf.filter.tagFilter";
|
||||
}
|
||||
|
||||
fromIdentifier(identifier: {
|
||||
[key: string]: string | number | boolean;
|
||||
}): Filter {
|
||||
fromIdentifier(identifier: FilterIdentifier): Filter {
|
||||
return new TagFilter(identifier.tagId as string, identifier.opposite as boolean);
|
||||
}
|
||||
|
||||
@ -74,12 +72,6 @@ export class TagFilter implements Filter {
|
||||
).condition ?? (() => true)
|
||||
)(server);
|
||||
|
||||
console.log(result, server.online?.name, (
|
||||
allTags.find((c) => btoa(c.docsName) === this.tagId) ?? {
|
||||
condition: () => true,
|
||||
}
|
||||
));
|
||||
|
||||
if (typeof result === "boolean")
|
||||
return new Promise((r) => r(this.opposite ? !result : result))
|
||||
|
||||
@ -87,4 +79,10 @@ export class TagFilter implements Filter {
|
||||
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,7 +30,6 @@
|
||||
|
||||
export async function getMinehutIcons(): Promise<MinehutIcon[] | undefined> {
|
||||
const icons = await fetch("https://api.minehut.com/servers/icons");
|
||||
console.log(icons);
|
||||
if (!icons.ok) return undefined;
|
||||
return await icons.json();
|
||||
}
|
||||
|
||||
@ -38,4 +38,5 @@ export interface Sort {
|
||||
[key: string]: string | number | boolean;
|
||||
}): Sort;
|
||||
sortToServers(serverA: OnlineServer, serverB: OnlineServer): number;
|
||||
getTagStrings(): string[];
|
||||
}
|
||||
7
apps/www/src/lib/types/supportedFilters.ts
Normal file
7
apps/www/src/lib/types/supportedFilters.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { FilterIdentifier, Filter } from "./filter";
|
||||
import { filterRegistry } from "./filter-registry";
|
||||
|
||||
export const supportedFilters = Array.from(filterRegistry.entries()).map(([ns, fi]) => ({
|
||||
ns,
|
||||
fi
|
||||
}));
|
||||
@ -43,7 +43,11 @@ const isOldServerRoute = createRouteMatcher([
|
||||
"/server/:serverName",
|
||||
"/server/:serverName/statistics",
|
||||
]);
|
||||
const apiRoute = createRouteMatcher(["/api/(.*)"]);
|
||||
const isWaitlistPage = createRouteMatcher(["/waitlist", "/waitlist(.*)"]);
|
||||
const apiWaitlistPage = createRouteMatcher([
|
||||
"/api/v1/user/waitlist(.*)",
|
||||
"/api/v1/get-status",
|
||||
]);
|
||||
|
||||
export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
|
||||
? clerkMiddleware(async (auth, req) => {
|
||||
@ -52,6 +56,19 @@ export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
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)) {
|
||||
switch (authRes.userId === null) {
|
||||
case false:
|
||||
@ -72,7 +89,7 @@ export default process.env.NEXT_PUBLIC_IS_AUTH === "true"
|
||||
new URL(`/server/v2/minehut/${minehutRes.server._id}`, req.url),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
|
||||
@ -95,7 +95,6 @@ export default serve({
|
||||
server: server.name,
|
||||
date: new Date(),
|
||||
});
|
||||
console.log(i, mh.servers.length);
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
@ -95,7 +95,6 @@ export default async function handler(
|
||||
|
||||
res.send({ result: dailyAverages });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ message: "An error occurred while fetching data" });
|
||||
} finally {
|
||||
await client.close();
|
||||
|
||||
@ -114,7 +114,6 @@ export default async function handler(
|
||||
|
||||
res.send({ data });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ message: "An error occurred while fetching data" });
|
||||
} finally {
|
||||
await client.close();
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
|
||||
import { getBackendProcedure } from "@/lib/backend-procedure";
|
||||
import type { MHSFData } from "@/lib/types/data";
|
||||
import { clerkClient, getAuth } from "@clerk/nextjs/server";
|
||||
import { MongoClient } from "mongodb";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
@ -83,17 +84,17 @@ export default async function handler(
|
||||
await mongo.connect();
|
||||
const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const stats = mongo.db("mhsf")
|
||||
const userId = req.cookies.userId;
|
||||
const {userId} = getAuth(req);
|
||||
|
||||
// Run queries in parallel
|
||||
const [favoriteData, customizationData, playerData, achievements] =
|
||||
await Promise.all([
|
||||
findFavoriteData(serverData.name, userId, stats, {
|
||||
findFavoriteData(serverData.name, userId ?? undefined, stats, {
|
||||
maxFavoriteEntries,
|
||||
favoriteTimespanStart,
|
||||
favoriteTimespanEnd,
|
||||
}),
|
||||
findCustomizationData(serverData.name, userId, db),
|
||||
findCustomizationData(serverData.name, userId ?? undefined, stats),
|
||||
findPlayerData(serverData.name, stats, {
|
||||
maxPlayerEntries,
|
||||
playerTimespanStart,
|
||||
@ -147,6 +148,7 @@ async function findCustomizationData(
|
||||
isOwned: boolean;
|
||||
isOwnedByUser: boolean;
|
||||
}> {
|
||||
const clerk = await clerkClient();
|
||||
// Run queries in parallel
|
||||
const [customizationData, ownedServerData] = await Promise.all([
|
||||
db.collection("customization").findOne({ server: serverName }),
|
||||
@ -160,6 +162,7 @@ async function findCustomizationData(
|
||||
...(customizationData as any),
|
||||
isOwned: true,
|
||||
isOwnedByUser: ownedServerData?.author === userId,
|
||||
userProfilePicture: userId ? (await clerk.users.getUser(ownedServerData?.author)).imageUrl : 'no user'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
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 });
|
||||
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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