feat(v2): i forgot

This commit is contained in:
dvelo 2025-03-02 18:28:20 -06:00
parent e4dc5f86de
commit 8541994c4d
43 changed files with 1368 additions and 640 deletions

@ -26,10 +26,11 @@
"@radix-ui/react-collapsible": "1.1.1",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-hover-card": "1.1.1",
"@radix-ui/react-icons": "1.3.0",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-menubar": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-select": "2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "1.1.0",
"@radix-ui/react-tabs": "^1.1.3",
"@types/react": "^19.0.8",
@ -38,6 +39,7 @@
"@unocss/postcss": "^0.61.5",
"@unocss/transformer-directives": "^0.61.5",
"@unocss/webpack": "^0.61.5",
"@vercel/functions": "^2.0.0",
"ag-grid-react": "^33.0.3",
"contentlayer": "^0.3.4",
"cron": "^3.1.7",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -42,6 +42,7 @@ import { ThemeProvider } from "@/components/util/theme-provider";
import { FontBoundary } from "@/components/util/font-boundary";
import { ClerkProvider } from "@/components/util/clerk-provider";
import { Toaster } from "sonner";
import { Footer } from "@/components/feat/footer/footer";
export default function RootLayout({
children,
@ -79,7 +80,8 @@ export default function RootLayout({
<Toaster richColors position="top-center" />
<ClerkProvider>
<NavBar />
<div className="pt-16">{children}</div>
<div className="pt-16 min-h-screen">{children}</div>
<Footer />
</ClerkProvider>
</TooltipProvider>
</FontBoundary>

@ -0,0 +1,25 @@
import { ServerResponse } from "@/lib/types/mh-server";
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ server: string }>;
}): Promise<Metadata> {
const id = (await params).server;
const { server }: { server: ServerResponse } = await (
await fetch("https://api.minehut.com/server/" + id)
).json();
return {
applicationName: "MHSF",
title: `${server.name} | MHSF`,
openGraph: {
title: server.name,
description: "A server on Minehut, find it on MHSF!",
},
description: "A server on Minehut, find it on MHSF!",
};
}
export default function ServerPage() {}

@ -0,0 +1,15 @@
import { Support } from "@/components/feat/support/support";
import type { Metadata } from "next";
export const metadata: Metadata = {
applicationName: "MHSF",
title: "Support · MHSF",
};
export default function SupportPage() {
return (
<div>
<Support />
</div>
);
}

@ -56,7 +56,7 @@
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-muted-foreground: hsl(0 0% 45.1%);
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
@ -9286,3 +9286,14 @@ body {
.icon-minecraft-sm.icon-minecraft-mob-zombified-piglin-face {
background-position: -144px -736px;
}
/*
---break---
*/
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

@ -0,0 +1,69 @@
import { ComponentPropsWithoutRef, ReactNode } from "react";
import { cn } from "@/lib/utils";
interface BentoGridProps extends ComponentPropsWithoutRef<"div"> {
children: ReactNode;
className?: string;
}
interface BentoCardProps extends ComponentPropsWithoutRef<"div"> {
name: string;
className: string;
background: ReactNode;
Icon: React.ElementType;
description: string;
href: string;
cta: string;
}
const BentoGrid = ({ children, className, ...props }: BentoGridProps) => {
return (
<div
className={cn(
"grid w-full auto-rows-[22rem] grid-cols-3 gap-4",
className
)}
{...props}
>
{children}
</div>
);
};
const BentoCard = ({
name,
className,
background,
Icon,
description,
href,
cta,
...props
}: BentoCardProps) => (
<div
key={name}
className={cn(
"group relative col-span-3 flex flex-col justify-between overflow-hidden rounded-xl",
// light styles
"bg-background [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]",
// dark styles
"transform-gpu dark:bg-background dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]",
className
)}
{...props}
>
<div>{background}</div>
<div className="pointer-events-none z-10 flex transform-gpu flex-col gap-1 p-6 transition-all duration-300 group-hover:-translate-y-10">
<Icon className="h-12 w-12 origin-left transform-gpu text-neutral-700 transition-all duration-300 ease-in-out group-hover:scale-75" />
<h3 className="text-xl font-semibold text-neutral-700 dark:text-neutral-300">
{name}
</h3>
<p className="max-w-lg text-neutral-400">{description}</p>
</div>
<div className="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10" />
</div>
);
export { BentoCard, BentoGrid };

@ -0,0 +1,61 @@
import { BrandingGenericIcon } from "../icons/branding-icons";
import { Link } from "../../util/link";
import { FooterStatus } from "./status";
export function Footer() {
return (
<footer className="w-full border-t p-[20px] mt-15">
<div className="flex justify-between items-center">
<span className="flex items-center gap-4 text-muted-foreground">
<Link href="Special:Root">
<BrandingGenericIcon className="max-w-[32px] max-h-[32px]" />
</Link>
<ul className="grid grid-cols-2 md:flex gap-4 p-0 m-0 w-full md:items-center items-start list-none">
<li className="text-sm">
<Link
href="/home"
className="text-muted-foreground hover:text-shadcn-primary transition-colors"
>
Home
</Link>
</li>
<li className="text-sm">
<Link
href="/servers"
className="text-muted-foreground hover:text-shadcn-primary transition-colors"
>
Servers
</Link>
</li>
<li className="text-sm">
<Link
href="/support"
className="text-muted-foreground hover:text-shadcn-primary transition-colors"
>
Contact
</Link>
</li>
</ul>
</span>
<FooterStatus />
</div>
<span className="block mt-4">
<small className="text-muted-foreground 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,
or GamerSafer in any way. <br className="spacing-3" />
Spamming, abusing or misusing the Minehut API and/or MHSF will get
your IP blocked, we are not responsible for IP blocks.{" "}
<strong>You have been warned.</strong>
<br className="spacing-3" />
If you're worried, please review the{" "}
<Link href="https://support.minehut.com/hc/en-us/articles/27075816947731-Minehut-Rules">
Rules
</Link>
, <Link href="https://minehut.com/terms-of-service">ToS</Link> &{" "}
<Link href="https://support.mhsf.app/ECA">ECA</Link>.
</small>
</span>
</footer>
);
}

@ -0,0 +1,38 @@
"use client";
import { Button } from "@/components/ui/button";
import { Link } from "@/components/util/link";
import useStatus from "@/lib/hooks/use-status";
import { cn } from "@/lib/utils";
export function FooterStatus() {
const { loading, incidents, statusURL } = useStatus();
if (!loading)
return (
<Link
href={`https://${statusURL as string}`}
noExtraIcons
target="_blank"
>
<Button variant="tertiary">
<span
className={cn(
"text-sm flex items-center gap-2 font-normal",
"text-blue-600"
)}
>
<div
className="items-center bg-blue-600 dark:bg-blue-600"
style={{
width: ".5rem",
height: ".5rem",
borderRadius: "9999px",
}}
/>
All systems normal
</span>
</Button>
</Link>
);
}

@ -37,6 +37,21 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { toast } from "sonner";
import { useEffectOnce } from "@/lib/useEffectOnce";
import { allTags } from "@/config/tags";
import { ReactNode, useState } from "react";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Copy } from "lucide-react";
import useClipboard from "@/lib/useClipboard";
export default function ServerCard({
server,
@ -45,6 +60,8 @@ export default function ServerCard({
server: OnlineServer;
motd: string | undefined;
}) {
const clipboard = useClipboard();
return (
<Material
key={server.name}
@ -66,6 +83,30 @@ export default function ServerCard({
<span className="flex gap-2 items-center">
<IconDisplay server={server} />
<strong className="text-lg">{server.name}</strong>
<Tooltip>
<TooltipTrigger>
<Button
variant="secondary"
size="square-md"
className="flex items-center"
onClick={(e) => {
e.stopPropagation();
toast.success("Copied IP address to clipboard.");
clipboard.writeText(`${server.name}.mhsf.minehut.gg`);
}}
>
<Copy size={16} />
</Button>
</TooltipTrigger>
<TooltipContent
className="max-w-[390px] break-words"
onClick={(e) => e.stopPropagation()}
>
Copy the server address to your clipboard. MHSF automatically adds
.mhsf in-between the server name and minehut.gg to tell server
owners where you came from.
</TooltipContent>
</Tooltip>
</span>
<Tooltip>
<TooltipTrigger>
@ -74,7 +115,10 @@ export default function ServerCard({
</span>
</TooltipTrigger>
<TooltipContent className="max-w-[390px] break-words">
<TooltipContent
className="max-w-[390px] break-words"
onClick={(e) => e.stopPropagation()}
>
{server.author ? (
<span>
{server.name} is owned by{" "}
@ -88,6 +132,7 @@ export default function ServerCard({
)}
</TooltipContent>
</Tooltip>
<TagShower server={server} className="mt-1" />
{motd && (
<span
className="block break-all overflow-hidden mt-3"
@ -98,6 +143,129 @@ export default function ServerCard({
);
}
export type BadgeColor =
| "default"
| "red"
| "green"
| "yellow"
| "gray"
| "blue"
| "purple"
| "red-subtle"
| "green-subtle"
| "yellow-subtle"
| "gray-subtle"
| "blue-subtle"
| "purple-subtle"
| "custom";
export function TagShower(props: {
server: OnlineServer;
className?: string;
unclickable?: boolean;
}) {
const [loading, setLoading] = useState(true);
const [compatiableTags, setCompatiableTags] = useState<
Array<{
name: ReactNode;
docsName?: string;
tooltip: string;
htmlDocs: string;
role: BadgeColor;
}>
>([]);
useEffectOnce(() => {
if (loading) {
allTags.forEach((tag) => {
if (!tag.condition) {
tag.name(props.server).then((n) => {
compatiableTags.push({
name: n,
docsName: tag.docsName,
tooltip: tag.tooltipDesc,
htmlDocs: tag.htmlDocs,
role: tag.role === undefined ? "default" : tag.role,
});
setLoading(false);
});
} else
tag.condition(props.server).then((b) => {
if (b && tag.primary) {
tag.name(props.server).then((n) => {
compatiableTags.push({
name: n,
docsName: tag.docsName,
tooltip: tag.tooltipDesc,
htmlDocs: tag.htmlDocs,
role: tag.role === undefined ? "default" : tag.role,
});
setLoading(false);
});
}
});
});
}
});
if (loading) {
return <></>;
}
return (
<div
className="font-normal tracking-normal flex flex-wrap"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
{compatiableTags.map((t, i) => (
<span key={t.docsName} className="mr-1">
{props.unclickable && (
<Badge variant={t.role} className={props.className}>
{t.name}
</Badge>
)}
{!props.unclickable && (
<Dialog key={t.docsName}>
<DialogTrigger>
<Tooltip>
<TooltipTrigger>
<Badge variant={t.role} className={props.className}>
{t.name}
</Badge>
</TooltipTrigger>
<TooltipContent>
<div className="font-normal">
{t.tooltip}
<br />
Click the tag to learn more about it.
</div>
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{'"'}
{t.docsName == undefined ? t.name : t.docsName}
{'"'} documentation
</DialogTitle>
<DialogDescription
className="font-normal"
dangerouslySetInnerHTML={{
__html: t.htmlDocs,
}}
/>
</DialogHeader>
</DialogContent>
</Dialog>
)}
</span>
))}
</div>
);
}
function RankColoring({ rank, author }: { rank: string; author: string }) {
switch (rank.toLocaleLowerCase()) {
case "default":

@ -0,0 +1,36 @@
"use client";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { Link } from "@/components/util/link";
import useStatus from "@/lib/hooks/use-status";
import { cn } from "@/lib/utils";
export function StatusButton() {
const { loading, incidents, statusURL } = useStatus();
if (loading) return <Spinner />;
return (
<Link href={`https://${statusURL as string}`} noExtraIcons target="_blank">
<Button variant="secondary" className="rounded-xl">
<span
className={cn(
"text-sm flex items-center gap-2 font-normal",
"text-blue-600"
)}
>
<div
className="items-center bg-blue-600 dark:bg-blue-600"
style={{
width: ".5rem",
height: ".5rem",
borderRadius: "9999px",
}}
/>
All systems normal
</span>
</Button>
</Link>
);
}

@ -0,0 +1,70 @@
import { Button } from "@/components/ui/button";
import { GithubNoTheme } from "@/components/ui/github";
import { Material } from "@/components/ui/material";
import { Link } from "@/components/util/link";
import { Inbox, Sticker } from "lucide-react";
import { StatusButton } from "./status-button";
import { Separator } from "@/components/ui/separator";
export function Support() {
return (
<article className="2xl:px-[30rem] xl:px-[15rem] lg:px-[10rem] px-4 border-l border-r">
<Material className="w-full py-10">
<h1 className="text-4xl font-bold w-full text-center justify-center flex items-center">
Contact
<span className="text-[0.4rem]">🧇</span>
</h1>
</Material>
<br />
<span className="grid grid-cols-2 gap-4">
<Material>
<GithubNoTheme className="w-[32px] h-[32px] dark:fill-white" />
<h2 className="text-xl font-bold">GitHub Issues</h2>
<span>
If you have a bug issue that is reproducible or suggestion please
make a new issue on{" "}
<Link href="GitHub:Issues" target="_blank">
GitHub Issues
</Link>
</span>
<br />
<Link href="GitHub:Issues">
<Button className="mt-2">Open an issue</Button>
</Link>
</Material>
<Material>
<Inbox className="w-[32px] h-[32px]" />
<h2 className="text-xl font-bold">Contact Support</h2>
<span>
If you do not have a GitHub account and/or need help with something
MHSF-related, feel free to contact{" "}
<Link href="mailto:support@mhsf.app" className="underline">
support@mhsf.app
</Link>
</span>
<br />
<span className="flex items-center mt-2 gap-2">
<Link href="mailto:support@mhsf.app">
<Button>Contact</Button>
</Link>
<StatusButton />
</span>
</Material>
</span>
<Material className="mt-4">
<Sticker className="w-[32px] h-[32px]" />
<h2 className="text-xl font-bold">Ask</h2>
<span>
For anything else where you'll need to contact somebody, contact me @{" "}
<Link href="mailto:dvelo@mhsf.app" className="underline">
dvelo@mhsf.app
</Link>
</span>
<Separator className="my-4" />
<Link href="mailto:dvelo@mhsf.app">
<Button>Give me an email!</Button>
</Link>
</Material>
</article>
);
}

@ -40,6 +40,8 @@ const badgeVariants = cva(
{
variants: {
variant: {
default:
"bg-shadcn-primary text-shadcn-primary-foreground ring-transparent",
red: "bg-red-500 text-white ring-transparent",
green: "bg-green-500 text-white dark:text-black ring-transparent",
yellow: "bg-yellow-500 text-black ring-transparent",

@ -47,4 +47,19 @@ const Github = (props: SVGProps<SVGSVGElement>) => {
</svg>
);
};
export const GithubNoTheme = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
viewBox="0 0 256 250"
width="1em"
height="1em"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
{...props}
>
<path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" />
</svg>
);
};
export default Github;

@ -35,20 +35,28 @@ export function Link(
props: LinkProps & {
children?: React.ReactNode;
className?: string;
noExtraIcons?: boolean;
target?: string;
}
) {
const href = props.href as string;
return (
<NextLink {...props} href={pageFind(href || "") || "#"} title={href}>
{(href || "").startsWith("Docs:") && (
<Book size={16} className="mr-[2px] inline-flex" />
)}
{(href || "").startsWith("Wiki:") && (
<NotebookText size={14} className="mr-[2px] mb-[3px] inline-flex" />
{!props.noExtraIcons && (
<>
{(href || "").startsWith("Docs:") && (
<Book size={16} className="mr-[2px] inline-flex" />
)}
{(href || "").startsWith("Wiki:") && (
<NotebookText size={14} className="mr-[2px] mb-[3px] inline-flex" />
)}
</>
)}
{props.children}
{(href || "").startsWith("https") && (
{!props.noExtraIcons && (href || "").startsWith("https") && (
<ExternalLink size={12} className="ml-[2px] mb-[3px] inline-flex" />
)}
</NextLink>
@ -69,6 +77,8 @@ export const pageFind = (text: string) => {
if (text.startsWith("Server:")) return "/server/" + text.substring(7);
if (text.startsWith("Wiki:"))
return "https://minehut.wiki.gg/wiki/" + text.substring(5);
if (text === "GitHub:Issues")
return "https://github.com/DeveloLongScript/MHSF/issues/new/choose";
if (text.startsWith("GitHub:"))
return "https://github.com/" + text.substring(7);
if (text === "Special:GitHub")

@ -28,7 +28,10 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { BadgeColor } from "@/components/feat/server-list/server-card";
import { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
import { ServerCog } from "lucide-react";
import { ReactNode } from "react";
const serverCache: any = {};
@ -46,36 +49,70 @@ 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: OnlineServer) => Promise<string>;
condition: (server: OnlineServer) => Promise<boolean>;
name: (server: OnlineServer) => Promise<string | ReactNode>;
condition?: (server: OnlineServer) => Promise<boolean>;
listCondition?: (server: ServerResponse) => Promise<boolean>;
tooltipDesc: string;
htmlDocs: string;
docsName: string;
primary: boolean;
role?:
| "default"
| "destructive"
| "outline"
| "secondary"
| "red"
| "orange"
| "yellow"
| "green"
| "lime"
| "blue"
| "teal"
| "cyan"
| "violet"
| "indigo"
| "purple"
| "fuchsia"
| "pink";
role?: BadgeColor;
__disab?: boolean;
__filter?: boolean;
}> = [
{
name: async () => "Always Online",
name: async (c) => (
<>
<div
className="items-center bg-green-700 dark:bg-green-400"
style={{
width: ".4rem",
height: ".4rem",
borderRadius: "9999px",
}}
/>
{c.playerData.playerCount} online
</>
),
condition: async (c) => c.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.",
primary: true,
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.playerData.playerCount === 0,
htmlDocs: "Nobody is online this server.",
tooltipDesc: "Nobody is online this server.",
primary: true,
role: "gray-subtle",
docsName: "Nobody Online",
__filter: true,
},
{
name: async () => (
<>
<ServerCog size={16} />
Always Online
</>
),
condition: async (b: any) => b.staticInfo.alwaysOnline,
tooltipDesc:
'"Always online" means that the server will not shut down until the plan associated with it expires.',
@ -84,7 +121,7 @@ export const allTags: Array<{
`,
primary: true,
docsName: "Always Online",
role: "secondary",
role: "blue",
__disab: true,
},
{
@ -96,7 +133,7 @@ export const allTags: Array<{
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>",
primary: true,
role: "secondary",
role: "default",
__filter: true,
},
{
@ -108,6 +145,16 @@ export const allTags: Array<{
primary: true,
role: "purple",
},
{
name: async (s) => <>{s.staticInfo.serverPlan.split(" ")[0]}</>,
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>",
primary: true,
role: "red-subtle",
__filter: true,
},
// deprecated
/**{
name: async () => "Velocity",

@ -33,7 +33,7 @@ import { useEffect, useState } from "react";
export default function useStatus() {
const [loading, setLoading] = useState(true);
const [incidents, setIncidents] = useState(null);
const [statusURL, setStatusURL] = useState(null);
const [statusURL, setStatusURL] = useState<string | null>(null);
useEffect(() => {
fetch("/api/v1/get-status")

@ -31,44 +31,47 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getAuth, clerkClient } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
const { code } = req.body;
const { userId } = getAuth(req);
const { code } = req.body;
if (code == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (code == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("auth_codes");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("auth_codes");
const entry = await collection.findOne({ code });
if (entry == null) {
res.status(400).send({ message: "Couldn't find code" });
return;
}
collection.findOneAndDelete({ code });
const users = db.collection("claimed-users");
await users.insertOne({ player: entry.player, userId });
const entry = await collection.findOne({ code });
if (entry == null) {
res.status(400).send({ message: "Couldn't find code" });
return;
}
collection.findOneAndDelete({ code });
const users = db.collection("claimed-users");
await users.insertOne({ player: entry.player, userId });
(await clerkClient()).users.updateUserMetadata(userId, {
publicMetadata: {
player: entry.player,
},
});
(await clerkClient()).users.updateUserMetadata(userId, {
publicMetadata: {
player: entry.player,
},
});
res.send({ player: entry.player });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
client.close();
res.send({ player: entry.player });
}

@ -30,25 +30,28 @@
import { NextApiRequest, NextApiResponse } from "next";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { server } = req.body;
const { server } = req.body;
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
res.send({ owned: (await collection.findOne({ server })) != undefined });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
client.close();
res.send({ owned: (await collection.findOne({ server })) != undefined });
}

@ -32,82 +32,96 @@ import { NextApiRequest, NextApiResponse } from "next";
import { clerkClient, getAuth } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb";
import { OnlineServer } from "@/lib/types/mh-server";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
const { server } = req.body;
const { userId } = getAuth(req);
const { server } = req.body;
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
if (
(await (await clerkClient()).users.getUser(userId)).publicMetadata.player ==
undefined
) {
return res.status(401).json({ error: "Account not linked" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
if (
(await (await clerkClient()).users.getUser(userId)).publicMetadata.player ==
undefined
) {
return res.status(401).json({ error: "Account not linked" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
if ((await collection.findOne({ server: server })) == undefined) {
const mh = await fetch(
process.env.MHSF_BACKEND_API_LOCATION ??
"https://api.minehut.com/servers",
{
headers: {
accept: "*/*",
"accept-language": Math.random().toString(),
priority: "u=1, i",
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
Referer: "http://localhost:3000/",
"Referrer-Policy": "strict-origin-when-cross-origin",
Authentication: `MHSF-Backend-Server ${process.env.MHSF_BACKEND_API_LOCATION ? process.env.MHSF_BACKEND_SECRET : "Sorry Minehut Devs."}`,
},
body: null,
method: "GET",
}
);
const servers: Array<OnlineServer> = (await mh.json()).servers;
if ((await collection.findOne({ server: server })) == undefined) {
const mh = await fetch(
process.env.MHSF_BACKEND_API_LOCATION ??
"https://api.minehut.com/servers",
{
headers: {
accept: "*/*",
"accept-language": Math.random().toString(),
priority: "u=1, i",
"sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
Referer: "http://localhost:3000/",
"Referrer-Policy": "strict-origin-when-cross-origin",
Authentication: `MHSF-Backend-Server ${process.env.MHSF_BACKEND_API_LOCATION ? process.env.MHSF_BACKEND_SECRET : "Sorry Minehut Devs."}`,
},
body: null,
method: "GET",
},
);
const servers: Array<OnlineServer> = (await mh.json()).servers;
servers.forEach(async (c, i) => {
if (c.name == server) {
const MCUsername = (await (await clerkClient()).users.getUser(userId))
.publicMetadata.player;
servers.forEach(async (c, i) => {
if (c.name === server) {
const MCUsername = (await (await clerkClient()).users.getUser(userId))
.publicMetadata.player;
if (MCUsername == c.author) {
await collection.insertOne({ server, author: userId });
res.send({ message: "Successfully owned server!" });
client.close();
} else {
res
.status(400)
.send({ message: "The linked account doesn't own the server." });
client.close();
}
}
if (i == servers.length) {
res.status(400).send({ message: "The server needs to be online." });
client.close();
}
});
} else {
res.status(400).send({ message: "This server has already been owned." });
client.close();
}
if (MCUsername === c.author) {
await collection.insertOne({ server, author: userId });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ message: "Successfully owned server!" });
} else {
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res
.status(400)
.send({ message: "The linked account doesn't own the server." });
}
}
if (i == servers.length) {
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.status(400).send({ message: "The server needs to be online." });
}
});
} else {
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.status(400).send({ message: "This server has already been owned." });
}
}

@ -31,37 +31,40 @@
import { NextApiRequest, NextApiResponse } from "next";
import { clerkClient, getAuth } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
const { server } = req.body;
const { userId } = getAuth(req);
const { server } = req.body;
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
if (
(await (await clerkClient()).users.getUser(userId)).publicMetadata.player ==
undefined
) {
return res.status(401).json({ error: "Account not linked" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
if (
(await (await clerkClient()).users.getUser(userId)).publicMetadata.player ==
undefined
) {
return res.status(401).json({ error: "Account not linked" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
res.send({
result: (await collection.findOne({ server, author: userId })) != null,
});
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
client.close();
res.send({
result: (await collection.findOne({ server, author: userId })) != null,
});
}

@ -31,35 +31,36 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getAuth, clerkClient } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
const { userId } = getAuth(req);
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const users = db.collection("claimed-users");
if ((await users.find({ userId }).toArray()).length === 0) {
res.status(400).send({ result: "Hasn't linked yet!" });
return;
}
await users.findOneAndDelete({ userId });
const user = await (await clerkClient()).users.getUser(userId);
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const users = db.collection("claimed-users");
if ((await users.find({ userId }).toArray()).length === 0) {
res.status(400).send({ result: "Hasn't linked yet!" });
return;
}
await users.findOneAndDelete({ userId });
const user = await (await clerkClient()).users.getUser(userId);
await (
await clerkClient()
).users.updateUserMetadata(userId, {
publicMetadata: { player: null },
});
await (await clerkClient()).users.updateUserMetadata(userId, {
publicMetadata: { player: null },
});
res.send({ result: "Unlinked!" });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
client.close();
res.send({ result: "Unlinked!" });
}

@ -31,43 +31,50 @@
import { NextApiRequest, NextApiResponse } from "next";
import { clerkClient, getAuth } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
const { server } = req.body;
const { userId } = getAuth(req);
const { server } = req.body;
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
if (
(await (await clerkClient()).users.getUser(userId)).publicMetadata.player ==
undefined
) {
return res.status(401).json({ error: "Account not linked" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
if (
(await (await clerkClient()).users.getUser(userId)).publicMetadata.player ==
undefined
) {
return res.status(401).json({ error: "Account not linked" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
const customization = db.collection("customization");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
const customization = db.collection("customization");
if (
(await collection.findOne({ server: server, author: userId })) != undefined
) {
collection.findOneAndDelete({ server });
customization.findOneAndDelete({ server });
res.send({ message: "Un-owned server!" });
} else {
res.status(400).send({ message: "This server hasn't been owned." });
client.close();
}
if (
(await collection.findOne({ server: server, author: userId })) != undefined
) {
collection.findOneAndDelete({ server });
customization.findOneAndDelete({ server });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ message: "Un-owned server!" });
} else {
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.status(400).send({ message: "This server hasn't been owned." });
}
}

@ -28,6 +28,7 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { waitUntil } from "@vercel/functions";
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
@ -44,5 +45,9 @@ export default async function handler(
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("achievements");
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ result: await collection.find({ name: server }).toArray() });
}

@ -30,19 +30,24 @@
import { NextApiRequest, NextApiResponse } from "next";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const server = req.query.server as string;
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const server = req.query.server as string;
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("customization");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("customization");
res.send({ results: await collection.findOne({ server }) });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
client.close();
res.send({ results: await collection.findOne({ server }) });
client.close();
}

@ -31,104 +31,111 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getAuth } from "@clerk/nextjs/server";
import { Document, MongoClient, WithId } from "mongodb";
import { waitUntil } from "@vercel/functions";
const validColors = [
"zinc",
"slate",
"stone",
"gray",
"neutral",
"red",
"rose",
"orange",
"green",
"blue",
"yellow",
"violet",
"zinc",
"slate",
"stone",
"gray",
"neutral",
"red",
"rose",
"orange",
"green",
"blue",
"yellow",
"violet",
];
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
const server = req.query.server as string;
const { userId } = getAuth(req);
const server = req.query.server as string;
const { customization }: { customization: any } = req.body;
if (
customization.description != undefined &&
(!(Array.from(customization.description).length < 1250) ||
!(Array.from(customization.description).length > 2))
)
return res.status(400).send({ message: "Description is incorrect length" });
const { customization }: { customization: any } = req.body;
if (
customization.description != undefined &&
(!(Array.from(customization.description).length < 1250) ||
!(Array.from(customization.description).length > 2))
)
return res.status(400).send({ message: "Description is incorrect length" });
if (
customization.discord != undefined &&
!/^\d*\.?\d*$/.test(customization.discord)
)
return res
.status(400)
.send({ message: "Discord server has invalid chars" });
if (
customization.discord != undefined &&
!/^\d*\.?\d*$/.test(customization.discord)
)
return res
.status(400)
.send({ message: "Discord server has invalid chars" });
if (
customization.colorScheme != undefined &&
!validColors.includes(customization.colorScheme)
)
return res.status(400).send({ message: "Color doesn't exist" });
if (
customization.colorScheme != undefined &&
!validColors.includes(customization.colorScheme)
)
return res.status(400).send({ message: "Color doesn't exist" });
if (customization == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (customization == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
const customizationColl = db.collection("customization");
if (!((await collection.findOne({ server, author: userId })) == undefined)) {
const alreadyExists =
(await customizationColl.findOne({ server })) != undefined;
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
const customizationColl = db.collection("customization");
if (!((await collection.findOne({ server, author: userId })) == undefined)) {
const alreadyExists =
(await customizationColl.findOne({ server })) != undefined;
if (!alreadyExists) {
await customizationColl.insertOne({
server,
colorScheme: customization.colorScheme,
description: customization.description,
banner: customization.banner,
discord: customization.discord,
});
} else {
const find = (await customizationColl.findOne({
server,
})) as WithId<Document>;
if (customization.colorScheme != undefined)
await customizationColl.findOneAndUpdate(
{ server },
{ $set: { colorScheme: customization.colorScheme } }
);
if (customization.description != undefined)
await customizationColl.findOneAndUpdate(
{ server },
{ $set: { description: customization.description } }
);
if (customization.banner != undefined)
await customizationColl.findOneAndUpdate(
{ server },
{ $set: { banner: customization.banner } }
);
if (customization.discord != undefined)
await customizationColl.findOneAndUpdate(
{ server },
{ $set: { discord: customization.discord } }
);
}
res.send({ message: "Done!" });
} else {
res.status(400).send({ message: "You don't own this server." });
}
client.close();
if (!alreadyExists) {
await customizationColl.insertOne({
server,
colorScheme: customization.colorScheme,
description: customization.description,
banner: customization.banner,
discord: customization.discord,
});
} else {
const find = (await customizationColl.findOne({
server,
})) as WithId<Document>;
if (customization.colorScheme != undefined)
await customizationColl.findOneAndUpdate(
{ server },
{ $set: { colorScheme: customization.colorScheme } },
);
if (customization.description != undefined)
await customizationColl.findOneAndUpdate(
{ server },
{ $set: { description: customization.description } },
);
if (customization.banner != undefined)
await customizationColl.findOneAndUpdate(
{ server },
{ $set: { banner: customization.banner } },
);
if (customization.discord != undefined)
await customizationColl.findOneAndUpdate(
{ server },
{ $set: { discord: customization.discord } },
);
}
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ message: "Done!" });
} else {
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.status(400).send({ message: "You don't own this server." });
}
}

@ -30,29 +30,32 @@
import { NextApiRequest, NextApiResponse } from "next";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { server }: { server: Array<string> | undefined } = req.body;
const { server }: { server: Array<string> | undefined } = req.body;
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("customization");
const results: { server: string; customization: any }[] = [];
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("customization");
const results: { server: string; customization: any }[] = [];
server.forEach(async (c) => {
results.push({ server: c, customization: await collection.findOne({ c }) });
});
server.forEach(async (c) => {
results.push({ server: c, customization: await collection.findOne({ c }) });
});
res.send({ results });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
client.close();
res.send({ results });
}

@ -30,59 +30,62 @@
import type { NextApiResponse, NextApiRequest } from "next";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { server } = req.query;
const client = new MongoClient(process.env.MONGO_DB as string);
const { server } = req.query;
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("meta");
const find = await collection.find({ server: server }).toArray();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("meta");
const find = await collection.find({ server: server }).toArray();
if (find.length != 0) {
const entry = find[0];
res.send({ result: entry.favorites });
} else {
res.send({ result: 0 });
}
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
client.close();
if (find.length != 0) {
const entry = find[0];
res.send({ result: entry.favorites });
} else {
res.send({ result: 0 });
}
}
export async function increaseNum(client: MongoClient, server: string) {
const db = client.db("mhsf");
const collection = db.collection("meta");
const find = await collection.find({ server: server }).toArray();
const db = client.db("mhsf");
const collection = db.collection("meta");
const find = await collection.find({ server: server }).toArray();
if (find.length == 0) {
collection.insertOne({ server: server, favorites: 1, date: new Date() });
} else {
const entry = find[0];
collection.findOneAndReplace(
{ server: server },
{ server: server, favorites: entry.favorites + 1, date: new Date() }
);
}
if (find.length == 0) {
collection.insertOne({ server: server, favorites: 1, date: new Date() });
} else {
const entry = find[0];
collection.findOneAndReplace(
{ server: server },
{ server: server, favorites: entry.favorites + 1, date: new Date() },
);
}
}
export async function decreaseNum(client: MongoClient, server: string) {
const db = client.db("mhsf");
const collection = db.collection("meta");
const find = await collection.find({ server: server }).toArray();
const db = client.db("mhsf");
const collection = db.collection("meta");
const find = await collection.find({ server: server }).toArray();
if (find.length == 0) {
return;
// Physically is impossible
} else {
const entry = find[0];
collection.findOneAndReplace(
{ server: server },
{ server: server, favorites: entry.favorites - 1 }
);
}
if (find.length == 0) {
return;
// Physically is impossible
} else {
const entry = find[0];
collection.findOneAndReplace(
{ server: server },
{ server: server, favorites: entry.favorites - 1 },
);
}
}

@ -32,59 +32,74 @@ import type { NextApiResponse, NextApiRequest } from "next";
import { MongoClient, ObjectId } from "mongodb";
import { getAuth } from "@clerk/nextjs/server";
import { decreaseNum, increaseNum } from "./community-favorites";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
const { userId } = getAuth(req);
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const server = req.query.server as string;
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const server = req.query.server as string;
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();
if (find.length == 0) {
collection.insertOne({ user: userId, favorites: [server] });
await increaseNum(client, server);
res.send({ message: "Favorited " + server });
} else {
const collect = find[0];
let existingFavorites: Array<string> = collect.favorites;
console.log(collect);
if (find.length == 0) {
collection.insertOne({ user: userId, favorites: [server] });
await increaseNum(client, server);
if (existingFavorites.includes(server)) {
// remove that favorite from the list
const index = existingFavorites.indexOf(server);
await decreaseNum(client, server);
if (index > -1) {
existingFavorites.splice(index, 1);
}
collection.replaceOne(
{ _id: new ObjectId(collect._id) },
{
user: userId,
favorites: existingFavorites,
}
);
res.send({ message: "Unfavorited " + server });
} else {
existingFavorites.push(server);
await increaseNum(client, server);
collection.replaceOne(
{ _id: new ObjectId(collect._id) },
{
user: userId,
favorites: existingFavorites,
}
);
res.send({ message: "Favorited " + server });
}
}
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ message: "Favorited " + server });
} else {
const collect = find[0];
let existingFavorites: Array<string> = collect.favorites;
console.log(collect);
if (existingFavorites.includes(server)) {
// remove that favorite from the list
const index = existingFavorites.indexOf(server);
await decreaseNum(client, server);
if (index > -1) {
existingFavorites.splice(index, 1);
}
collection.replaceOne(
{ _id: new ObjectId(collect._id) },
{
user: userId,
favorites: existingFavorites,
},
);
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ message: "Unfavorited " + server });
} else {
existingFavorites.push(server);
await increaseNum(client, server);
collection.replaceOne(
{ _id: new ObjectId(collect._id) },
{
user: userId,
favorites: existingFavorites,
},
);
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ message: "Favorited " + server });
}
}
}

@ -31,26 +31,31 @@
import type { NextApiResponse, NextApiRequest } from "next";
import { MongoClient } from "mongodb";
import { getAuth } from "@clerk/nextjs/server";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
const { userId } = getAuth(req);
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const server = req.query.server as string;
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const server = req.query.server as string;
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();
if (find.length == 0) res.send({ result: false });
else {
res.send({ result: find[0].favorites.includes(server) });
}
client.close();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
if (find.length == 0) res.send({ result: false });
else {
res.send({ result: find[0].favorites.includes(server) });
}
}

@ -31,27 +31,31 @@
import type { NextApiResponse, NextApiRequest } from "next";
import { MongoClient } from "mongodb";
import { getAuth } from "@clerk/nextjs/server";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
const { userId } = getAuth(req);
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();
if (find.length == 0) {
res.send({ result: [] });
} else {
res.send({ result: find[0].favorites });
}
client.close();
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
if (find.length == 0) {
res.send({ result: [] });
} else {
res.send({ result: find[0].favorites });
}
}

@ -30,33 +30,48 @@
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history");
const server = req.query.server as string;
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history");
const server = req.query.server as string;
const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
const result = await Promise.all([1,2,3,4,5,6,7].map(async (c) => {
const results = await db.find({
$and: [
{ server },
{ $expr: { $eq: [{ $dayOfWeek: "$date" }, c] } }
]
}).toArray()
const daysOfWeek = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const result = await Promise.all(
[1, 2, 3, 4, 5, 6, 7].map(async (c) => {
const results = await db
.find({
$and: [{ server }, { $expr: { $eq: [{ $dayOfWeek: "$date" }, c] } }],
})
.toArray();
if (results.length !== 0) {
const averageNums = (results as any as {player_count: number}[]).map((x: {player_count: number}) => x.player_count)
const average = averageNums.reduce((sum, val) => sum + val, 0) / averageNums.length;
if (results.length !== 0) {
const averageNums = (results as any as { player_count: number }[]).map(
(x: { player_count: number }) => x.player_count,
);
const average =
averageNums.reduce((sum, val) => sum + val, 0) / averageNums.length;
return { day: daysOfWeek[c - 1], result: Math.floor(average) };
}
return undefined;
}));
return { day: daysOfWeek[c - 1], result: Math.floor(average) };
}
return undefined;
}),
);
client.close()
res.send({result: result.filter((c) => c !== undefined)});
}
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ result: result.filter((c) => c !== undefined) });
}

@ -30,33 +30,36 @@
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("historical");
const server = req.query.server as string;
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes);
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("historical");
const server = req.query.server as string;
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes);
const allData = await db.find({ server }).toArray();
const data: any[] = [];
const allData = await db.find({ server }).toArray();
const data: any[] = [];
allData.forEach((d) => {
const result: any = {};
scopes.forEach((b) => {
result[b] = d[b];
});
data.push(result);
});
allData.forEach((d) => {
const result: any = {};
scopes.forEach((b) => {
result[b] = d[b];
});
data.push(result);
});
client.close();
res.send({ data });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ data });
}
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
}

@ -30,33 +30,61 @@
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history");
const server = req.query.server as string;
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history");
const server = req.query.server as string;
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
const result = await Promise.all([1,2,3,4,5,6,7,8,9,10,11,12].map(async (c) => {
const results = await db.find({
$and: [
{ server },
{ date: { $gte: new Date(new Date().getFullYear(), c - 1, 1), $lt: new Date(new Date().getFullYear(), c, 1) } }
]
}).toArray()
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const result = await Promise.all(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(async (c) => {
const results = await db
.find({
$and: [
{ server },
{
date: {
$gte: new Date(new Date().getFullYear(), c - 1, 1),
$lt: new Date(new Date().getFullYear(), c, 1),
},
},
],
})
.toArray();
if (results.length !== 0) {
const averageNums = (results as any as {player_count: number}[]).map((x: {player_count: number}) => x.player_count)
const average = averageNums.reduce((sum, val) => sum + val, 0) / averageNums.length;
if (results.length !== 0) {
const averageNums = (results as any as { player_count: number }[]).map(
(x: { player_count: number }) => x.player_count,
);
const average =
averageNums.reduce((sum, val) => sum + val, 0) / averageNums.length;
return { month: months[c - 1], result: Math.floor(average) };
}
return undefined;
}));
return { month: months[c - 1], result: Math.floor(average) };
}
return undefined;
}),
);
client.close()
res.send({result: result.filter((c) => c !== undefined)});
}
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ result: result.filter((c) => c !== undefined) });
}

@ -30,40 +30,43 @@
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history");
const mh = client.db("mhsf").collection("mh");
const server = req.query.server as string;
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history");
const mh = client.db("mhsf").collection("mh");
const server = req.query.server as string;
const allData = await db.find({ server }).toArray();
const data: any[] = [];
if (server === "peww") console.log(allData.slice(-30));
const allData = await db.find({ server }).toArray();
const data: any[] = [];
if (server === "peww") console.log(allData.slice(-30));
for (const d of allData.slice(-30)) {
const dateOfEntry = new Date(d.date);
const result = await mh
.find({
date: {
$gte: new Date(dateOfEntry.getTime() - 1000 * 60 * 60),
$lt: new Date(dateOfEntry.getTime() + 1000 * 60 * 60),
},
})
.toArray();
for (const d of allData.slice(-30)) {
const dateOfEntry = new Date(d.date);
const result = await mh
.find({
date: {
$gte: new Date(dateOfEntry.getTime() - 1000 * 60 * 60),
$lt: new Date(dateOfEntry.getTime() + 1000 * 60 * 60),
},
})
.toArray();
if (result.length > 0) {
const resultedData = result[0];
data.push({
relativePrecentage: d.player_count / resultedData.total_players,
date: dateOfEntry,
});
}
}
if (result.length > 0) {
const resultedData = result[0];
data.push({
relativePrecentage: d.player_count / resultedData.total_players,
date: dateOfEntry,
});
}
}
client.close();
res.send({ data });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ data });
}

@ -30,38 +30,41 @@
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history");
const server = req.query.server as string;
let dataMax = 0;
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes);
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history");
const server = req.query.server as string;
let dataMax = 0;
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes);
const allData = await db.find({ server }).toArray();
const data: any[] = [];
const allData = await db.find({ server }).toArray();
const data: any[] = [];
dataMax = (
await db.find({ server }).sort({ player_count: -1 }).limit(1).toArray()
)[0].player_count;
dataMax = (
await db.find({ server }).sort({ player_count: -1 }).limit(1).toArray()
)[0].player_count;
allData.forEach((d) => {
const result: any = {};
scopes.forEach((b) => {
result[b] = d[b];
});
data.push(result);
});
allData.forEach((d) => {
const result: any = {};
scopes.forEach((b) => {
result[b] = d[b];
});
data.push(result);
});
client.close();
res.send({ data, dataMax });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ data, dataMax });
}
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
}

@ -30,6 +30,7 @@
import { MongoClient, type WithId } from "mongodb";
import type { NextApiRequest, NextApiResponse } from "next";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
@ -50,5 +51,9 @@ export default async function handler(
array.slice(0, 100).reduce((acc, curr) => acc + curr.total_players, 0) /
array.slice(0, 100).length;
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ totalServerAverage, totalPlayerAverage });
}

@ -30,6 +30,7 @@
import { MongoClient } from "mongodb";
import { NextApiRequest, NextApiResponse } from "next";
import { waitUntil } from "@vercel/functions"
export default async function handler(
req: NextApiRequest,
@ -49,7 +50,11 @@ export default async function handler(
});
data.push(result);
});
client.close();
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close())
res.send({ data });
}

@ -30,21 +30,25 @@
import { NextApiRequest, NextApiResponse } from "next";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("meta");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("meta");
const all = await collection.find().toArray();
const sorted = all.sort((a, b) => a.favorites - b.favorites);
sorted.reverse();
res.send({ results: sorted });
const all = await collection.find().toArray();
const sorted = all.sort((a, b) => a.favorites - b.favorites);
sorted.reverse();
client.close();
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ results: sorted });
}

@ -28,8 +28,9 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { NextApiRequest, NextApiResponse } from "next";
import { NextApiRequest, NextApiResponse } from "next";
import { MongoClient } from "mongodb";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
@ -46,6 +47,9 @@ export default async function handler(
const collection = db.collection("claimed-users");
await collection.findOneAndDelete({ userId: id });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ message: "Done!" });
client.close();
}

@ -32,49 +32,52 @@ import { NextApiRequest, NextApiResponse } from "next";
import { getAuth } from "@clerk/nextjs/server";
import { MongoClient } from "mongodb";
import { inngest } from "../inngest";
import { waitUntil } from "@vercel/functions";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { userId } = getAuth(req);
const { server } = req.body;
const { userId } = getAuth(req);
const { server } = req.body;
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
const { reason } = req.body;
if (server == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
const { reason } = req.body;
if (reason == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (reason == null) {
res.status(400).send({ message: "Couldn't find data" });
return;
}
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const collection = db.collection("reports");
const entry = await collection.insertOne({
server: server,
reason: reason,
userId: userId,
});
// Don't wait for this to finish, just continue anyway
inngest.send({
name: "report-server",
data: {
_id: entry.insertedId.toString(),
server,
reason,
userId,
},
});
const db = client.db("mhsf");
const collection = db.collection("reports");
const entry = await collection.insertOne({
server: server,
reason: reason,
userId: userId,
});
// Don't wait for this to finish, just continue anyway
inngest.send({
name: "report-server",
data: {
_id: entry.insertedId.toString(),
server,
reason,
userId,
},
});
client.close();
res.send({ msg: "Successfully reported server!" });
// Close the database, but don't close this
// serverless instance until it happens
waitUntil(client.close());
res.send({ msg: "Successfully reported server!" });
}

@ -28,8 +28,14 @@
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { version } from "@/config/version";
import { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.send({ up: true });
res.send({
up: true,
apiNotice: `MHSF v${version} is an open-source, MIT-licensed Minehut API wrapper.
MHSF is not officially affiliated with with Minehut, Super League Enterprise, or GamerSafer in any way.
Spamming, abusing or misusing the Minehut API and/or MHSF will get your IP blocked, we are not responsible for IP blocks.`,
});
}

@ -1859,12 +1859,7 @@
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-icons@1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69"
integrity sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==
"@radix-ui/react-icons@^1.3.0":
"@radix-ui/react-icons@^1.3.0", "@radix-ui/react-icons@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.2.tgz#09be63d178262181aeca5fb7f7bc944b10a7f441"
integrity sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==
@ -2282,7 +2277,7 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-slot@1.1.2", "@radix-ui/react-slot@^1.1.0":
"@radix-ui/react-slot@1.1.2", "@radix-ui/react-slot@^1.1.0", "@radix-ui/react-slot@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6"
integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==
@ -3283,6 +3278,11 @@
resolved "https://registry.yarnpkg.com/@vercel/analytics/-/analytics-1.5.0.tgz#073f93694897414b21a8495e2619bbf64447dcaa"
integrity sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==
"@vercel/functions@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@vercel/functions/-/functions-2.0.0.tgz#be72057235a667b8c5b2713880fba717cd5a372b"
integrity sha512-BSwIihLHoV18gerKZJyGuqd3rtaYM6rJvET1kOwKktshucyaHXTJel7Cxegs+sdX0NZqsX4LO2MFnMU2jG01Cw==
"@vercel/speed-insights@^1.0.12":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@vercel/speed-insights/-/speed-insights-1.2.0.tgz#1656c3596d4ec02d93d301ca45944c1b9b245186"