mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-15 09:18:01 -05:00
feat: figure it out urself
This commit is contained in:
parent
bbec84c7de
commit
bba5504f5d
@ -28,7 +28,7 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
import type { SVGProps } from "react";
|
||||
const Github = (props: SVGProps<SVGSVGElement>) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
import type { SVGProps } from "react";
|
||||
const Github = (props: SVGProps<SVGSVGElement>) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
@ -21,10 +21,12 @@
|
||||
"@emotion/is-prop-valid": "^1.3.0",
|
||||
"@linear/sdk": "^31.0.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@number-flow/react": "^0.5.7",
|
||||
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||
"@radix-ui/react-avatar": "1.1.1",
|
||||
"@radix-ui/react-collapsible": "1.1.1",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-hover-card": "1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-menubar": "1.1.1",
|
||||
@ -49,7 +51,7 @@
|
||||
"inngest": "^3.21.2",
|
||||
"input-otp": "^1.2.4",
|
||||
"json-beautify": "^1.1.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"lucide-react": "^0.479.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"minimessage-2-html": "1.6.0",
|
||||
"minimessage-js": "^1.1.3",
|
||||
@ -71,6 +73,7 @@
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"react-snowfall": "^2.2.0",
|
||||
"recharts": "^2.15.1",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sonner": "^1.7.0",
|
||||
@ -81,7 +84,8 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-patch": "^4.0.0",
|
||||
"turbo": "^2.4.0",
|
||||
"unplugin-tailwindcss-mangle": "^3.0.1"
|
||||
"unplugin-tailwindcss-mangle": "^3.0.1",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clerk/themes": "^2.1.19",
|
||||
|
||||
@ -249,6 +249,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.shiki {
|
||||
@apply p-4 rounded max-w-[530px] overflow-x-auto;
|
||||
}
|
||||
|
||||
.system-ui-font--font-boundary {
|
||||
font-family:
|
||||
system-ui,
|
||||
|
||||
@ -47,7 +47,6 @@ export default function RootLayout({
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<noscript>{children}</noscript>
|
||||
<script src="https://unpkg.com/react-scan/dist/auto.global.js" />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -53,7 +53,7 @@ export function Footer() {
|
||||
Rules
|
||||
</Link>
|
||||
, <Link href="https://minehut.com/terms-of-service">ToS</Link> &{" "}
|
||||
<Link href="https://support.mhsf.app/ECA">ECA</Link>.
|
||||
<Link href="https://t.mhsf.app/pr">Platform Rules</Link>.
|
||||
</small>
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
@ -34,7 +34,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useClerk, useUser } from "@clerk/nextjs";
|
||||
import { ArrowDown, GalleryVertical } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Gradient } from "stripe-gradient";
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
*/
|
||||
|
||||
"use client";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export const brandingIconClipboard = `<svg width="266" height="265" viewBox="0 0 266 265" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@ -35,8 +35,10 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import Github from "@/components/ui/github";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Link } from "@/components/util/link";
|
||||
import { version } from "@/config/version";
|
||||
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
||||
import { LogIn, LogOut, Settings, Ship, User, UserCog } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@ -44,6 +46,7 @@ import { useRouter } from "next/navigation";
|
||||
export function MenuDropdown() {
|
||||
const clerk = useClerk();
|
||||
const router = useRouter();
|
||||
const settings = useSettingsStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -105,6 +108,23 @@ export function MenuDropdown() {
|
||||
<DropdownMenuSeparator />
|
||||
<li className="flex flex-col px-2 py-1 mx-auto my-1 text-xs w-full">
|
||||
<div className="flex flex-row gap-2 w-full items-center">
|
||||
{settings.get("debug-mode") === "true" ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex-1">
|
||||
<button
|
||||
className="hover:brightness-110 transition-all"
|
||||
type="button"
|
||||
>
|
||||
<Badge variant="blue-subtle">v{version}d</Badge>
|
||||
</button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
You're in debug mode! Are you a dev?
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="flex-1">
|
||||
<button
|
||||
className="hover:brightness-110 transition-all"
|
||||
@ -113,6 +133,8 @@ export function MenuDropdown() {
|
||||
<Badge variant="blue-subtle">v{version}</Badge>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Link href="Special:GitHub">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
|
||||
@ -71,9 +71,9 @@ export function NavBar() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-screen h-[3rem] grid-cols-3 fixed backdrop-blur-xl z-10 flex",
|
||||
"w-screen h-[3rem] grid-cols-3 fixed z-10 flex",
|
||||
"items-center justify-self-start me-auto pl-4 flex-1 transition-all justify-between",
|
||||
showBorder ? "border-b" : "",
|
||||
showBorder ? "border-b backdrop-blur-xl" : "",
|
||||
pathname !== null && animatedTopbarPages.includes(pathname)
|
||||
? "[--animation-delay:1000ms] opacity-0 animate-fade-in"
|
||||
: ""
|
||||
|
||||
@ -36,11 +36,16 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Statistics } from "./statistics";
|
||||
import InfiniteScroll from "react-infinite-scroll-component";
|
||||
import { useInfiniteScrolling } from "@/lib/hooks/use-infinite-scrolling";
|
||||
import { useMHSFServer } from "@/lib/hooks/use-mhsf-multiple";
|
||||
|
||||
export function ServerList() {
|
||||
const { servers, loading, serverCount, playerCount } = useServers();
|
||||
const { itemsLength, fetchMoreData, hasMoreData, data } =
|
||||
useInfiniteScrolling(servers);
|
||||
const mhsfServers = useMHSFServer(
|
||||
servers.map((s) => s.staticInfo._id),
|
||||
true
|
||||
);
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
|
||||
@ -63,7 +63,7 @@ export function Statistics({
|
||||
useEffect(() => {
|
||||
try {
|
||||
(async () => {
|
||||
const fetchRes = await fetch("/api/v0/history/meta-daily-avg");
|
||||
const fetchRes = await fetch("/api/v1/server/minehut/daily-avg");
|
||||
const fetchJson: {
|
||||
totalServerAverage: number;
|
||||
totalPlayerAverage: number;
|
||||
|
||||
187
apps/www/src/components/feat/server-page/debug/debug-menu.tsx
Normal file
187
apps/www/src/components/feat/server-page/debug/debug-menu.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
import { Drawer, DrawerContent, DrawerTitle } from "@/components/ui/drawer";
|
||||
import { Material } from "@/components/ui/material";
|
||||
import type { MHSFData } from "@/lib/types/data";
|
||||
import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
||||
import type { RouteParams } from "@/pages/api/v1/server/get/[server]";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Setting,
|
||||
SettingContent,
|
||||
SettingDescription,
|
||||
SettingMeta,
|
||||
SettingTitle,
|
||||
} from "../../settings/setting";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { codeToHtml } from "shiki";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Wrench } from "lucide-react";
|
||||
import useClipboard from "@/lib/useClipboard";
|
||||
import { DebugShikiParsedDrawer } from "./debug-shiki-parsed";
|
||||
import { convert } from "../util";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
export function DebugMenu({
|
||||
debugOptions,
|
||||
setOpen,
|
||||
open,
|
||||
}: {
|
||||
debugOptions: {
|
||||
serverName: string;
|
||||
serverId: string;
|
||||
mhsfData: (MHSFData & RouteParams) | null;
|
||||
serverData: ServerResponse | null;
|
||||
onlineServerData: OnlineServer | null;
|
||||
};
|
||||
open: boolean;
|
||||
setOpen: (newState: boolean) => void;
|
||||
}) {
|
||||
const [mhsfShikiParsed, setMHSFShikiParsed] = useState("");
|
||||
const [mhShikiParsed, setMHShikiParsed] = useState("");
|
||||
const clipboard = useClipboard();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setMHSFShikiParsed(
|
||||
await codeToHtml(JSON.stringify(debugOptions.mhsfData, null, 2), {
|
||||
lang: "json",
|
||||
theme: resolvedTheme === "dark" ? "vitesse-dark" : "vitesse-light",
|
||||
})
|
||||
);
|
||||
setMHShikiParsed(
|
||||
await codeToHtml(JSON.stringify(debugOptions.serverData, null, 2), {
|
||||
lang: "json",
|
||||
theme: resolvedTheme === "dark" ? "vitesse-dark" : "vitesse-light",
|
||||
})
|
||||
);
|
||||
})();
|
||||
}, [debugOptions]);
|
||||
|
||||
return (
|
||||
<Drawer onOpenChange={setOpen} open={open} direction="right">
|
||||
<DrawerContent className="p-4 min-w-[600px] overflow-x-hidden max-h-screen overflow-y-auto">
|
||||
<DrawerTitle className="text-lg mb-3 flex items-center gap-2">
|
||||
<Wrench size={24} /> Debug Options
|
||||
</DrawerTitle>
|
||||
<span className="m-2 mt-1 text-sm">
|
||||
This data is only designed for developers; it contains every single
|
||||
piece of information MHSF knows about the server. Could be useful for
|
||||
adding new backend options or endpoints.{" "}
|
||||
<strong>
|
||||
This only shows up when Debug Mode is enabled. (or when using
|
||||
Ctrl+Shift+O)
|
||||
</strong>
|
||||
</span>
|
||||
<Material className="mb-2">
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Server name</SettingTitle>
|
||||
<SettingDescription>
|
||||
Name of server after being parsed through Minehut API (aka
|
||||
server.name)
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
{debugOptions.serverName}
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</Material>
|
||||
<Material className="mb-2">
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Server Id</SettingTitle>
|
||||
<SettingDescription>
|
||||
Passed usually through query
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
{debugOptions.serverId}
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</Material>
|
||||
<Material className="mb-2">
|
||||
<strong className="flex items-center gap-2">
|
||||
{debugOptions.serverData === null && <Spinner />} Parsed Minehut
|
||||
data
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
clipboard.writeText(JSON.stringify(debugOptions.serverData))
|
||||
}
|
||||
>
|
||||
Copy (no toast!)
|
||||
</Button>
|
||||
</strong>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{ __html: mhShikiParsed }}
|
||||
className="break-all max-w-[100px]"
|
||||
/>
|
||||
</Material>
|
||||
<Material className="mb-2">
|
||||
<strong className="flex items-center gap-2">
|
||||
{debugOptions.mhsfData === null && <Spinner />} Parsed MHSF data
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
clipboard.writeText(JSON.stringify(debugOptions.mhsfData))
|
||||
}
|
||||
>
|
||||
Copy (no toast!)
|
||||
</Button>
|
||||
</strong>
|
||||
{debugOptions.mhsfData !== null && (
|
||||
<>
|
||||
<Setting className="py-3">
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>See all data</SettingTitle>
|
||||
<SettingDescription>
|
||||
WARNING: this data is MASSIVE. (@keyboard yk what else is
|
||||
massive?)
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<DebugShikiParsedDrawer shikiParsed={mhsfShikiParsed}>
|
||||
<Button>Open data</Button>
|
||||
</DebugShikiParsedDrawer>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting className="py-3">
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Total Statistical Data Count</SettingTitle>
|
||||
<SettingDescription>
|
||||
How many times has MHSF grabbed data about this server?
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
{convert(
|
||||
debugOptions.mhsfData.achievements.historically.length +
|
||||
debugOptions.mhsfData.playerData.historically.length +
|
||||
debugOptions.mhsfData.favoriteData.favoriteHistoricalData
|
||||
.length
|
||||
)}
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
|
||||
<Setting className="py-3">
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>
|
||||
Disable image caching on customization images
|
||||
</SettingTitle>
|
||||
<SettingDescription>
|
||||
Enabling this could result in being tracked but{" "}
|
||||
<strong>very rarely</strong> could render the image
|
||||
faster. (removes wsrv.nl caching)
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Switch />
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</>
|
||||
)}
|
||||
</Material>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { DebugMenu } from "./debug-menu";
|
||||
import type { MHSFData } from "@/lib/types/data";
|
||||
import type { RouteParams } from "@/pages/api/v1/server/get/[server]";
|
||||
import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||
|
||||
export function DebugProvider({
|
||||
debugOptions,
|
||||
children,
|
||||
}: {
|
||||
debugOptions: {
|
||||
serverName: string;
|
||||
serverId: string;
|
||||
mhsfData: (MHSFData & RouteParams) | null;
|
||||
serverData: ServerResponse | null;
|
||||
onlineServerData: OnlineServer | null;
|
||||
};
|
||||
children: ReactNode | ReactNode[];
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
useHotkeys(
|
||||
"ctrl+shift+o",
|
||||
() => {
|
||||
if (settingsStore.get("debug-mode") === "true") setOpen(!open);
|
||||
},
|
||||
[open]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("open-debug-menu", () => {
|
||||
setOpen(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DebugMenu open={open} setOpen={setOpen} debugOptions={debugOptions} />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Wrench } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function DebugShikiParsedDrawer({
|
||||
children,
|
||||
shikiParsed,
|
||||
}: {
|
||||
children: ReactNode | ReactNode[] | undefined;
|
||||
shikiParsed: string;
|
||||
}) {
|
||||
return (
|
||||
<Drawer direction="right">
|
||||
<DrawerContent className="p-4 min-w-[600px] max-h-screen overflow-y-auto">
|
||||
<DrawerTitle className="text-lg mb-3 flex items-center gap-2">
|
||||
<Wrench size={24} /> Debug Options - MHSF Data
|
||||
</DrawerTitle>
|
||||
|
||||
<span dangerouslySetInnerHTML={{ __html: shikiParsed }} />
|
||||
</DrawerContent>
|
||||
<DrawerTrigger>{children}</DrawerTrigger>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@ -35,12 +35,13 @@ import useClipboard from "@/lib/useClipboard";
|
||||
import { miniMessage } from "minimessage-js";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Material } from "@/components/ui/material";
|
||||
|
||||
export function MOTDRow({ server }: { server: ServerResponse }) {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return (
|
||||
<div className="border rounded-xl p-4 relative max-h-[250px] ">
|
||||
<Material className="p-4 relative h-[250px]">
|
||||
<strong className="text-lg">MOTD</strong>
|
||||
<br />
|
||||
<Separator className="my-2" />
|
||||
@ -66,6 +67,6 @@ export function MOTDRow({ server }: { server: ServerResponse }) {
|
||||
click to copy HTML
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
</Material>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import { Alert } from "@/components/ui/alert";
|
||||
import { Drawer, DrawerContent, DrawerTitle } from "@/components/ui/drawer";
|
||||
import { TextArea } from "@/components/ui/text-area";
|
||||
import { Link } from "@/components/util/link";
|
||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
import { useState } from "react";
|
||||
|
||||
export function ReportingDialog({
|
||||
server,
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
server: ReturnType<typeof useMHSFServer>;
|
||||
open: boolean;
|
||||
setOpen: (newState: boolean) => void;
|
||||
}) {
|
||||
const [reason, setReason] = useState("");
|
||||
|
||||
return (
|
||||
<Drawer direction="left" open={open} onOpenChange={setOpen}>
|
||||
<DrawerContent className="p-4 min-w-[600px] overflow-x-hidden max-h-screen overflow-y-auto">
|
||||
<DrawerTitle className="text-lg mb-3 flex items-center gap-2">
|
||||
Report server
|
||||
</DrawerTitle>
|
||||
<Alert variant="warning" className="text-sm">
|
||||
<strong>PLEASE READ BEFORE REPORTING:</strong>
|
||||
<ul className="list-disc pl-8">
|
||||
<li>This will send a notification to MHSF maintainers.</li>
|
||||
<li>
|
||||
This server must be in violation of the{" "}
|
||||
<Link href="/docs/legal/rules" className="underline">
|
||||
Platform Rules
|
||||
</Link>{" "}
|
||||
to be a valid report.
|
||||
</li>
|
||||
<li>
|
||||
Please do not spam this form with mindless reports. If you do,
|
||||
your account will be banned.
|
||||
</li>
|
||||
<li>
|
||||
We are not Minehut support,{" "}
|
||||
<b>
|
||||
we cannot help you with a problem within the Minehut platform
|
||||
</b>{" "}
|
||||
or within the server, we can only moderate the customization of
|
||||
the server. (if the problem is within the server,{" "}
|
||||
<Link href="https://support.minehut.com/hc/en-us/requests/new?tf_subject=Reporting%20Server&tf_27062997154195=reports_appeals&tf_27063229498259=report_server">
|
||||
report it on Minehut
|
||||
</Link>
|
||||
)
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
<br />
|
||||
<TextArea label="Reason for reporting" />
|
||||
<br />
|
||||
<span></span>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@ -28,38 +28,29 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { MongoClient } from "mongodb";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { ReportingDialog } from "./reporting-dialog";
|
||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
|
||||
export default async function handler(
|
||||
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);
|
||||
export function ReportingProvider({
|
||||
children,
|
||||
server,
|
||||
}: {
|
||||
children: ReactNode | ReactNode[];
|
||||
server: ReturnType<typeof useMHSFServer>;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const allData = await db.find({ server }).toArray();
|
||||
const data: any[] = [];
|
||||
|
||||
allData.forEach((d) => {
|
||||
const result: any = {};
|
||||
scopes.forEach((b) => {
|
||||
result[b] = d[b];
|
||||
useEffect(() => {
|
||||
window.addEventListener("open-report-menu", () => {
|
||||
setOpen(true);
|
||||
});
|
||||
data.push(result);
|
||||
});
|
||||
|
||||
// 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;
|
||||
return (
|
||||
<>
|
||||
<ReportingDialog server={server} open={open} setOpen={setOpen} />{" "}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -31,25 +31,38 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ServerResponse } from "@/lib/types/mh-server";
|
||||
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
||||
import { Heart, Star } from "lucide-react";
|
||||
import { EllipsisVertical, Flag, Heart, Star } from "lucide-react";
|
||||
import { useFavoriteStore } from "@/lib/hooks/use-favorite-store";
|
||||
import { useState } from "react";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
import NumberFlow from "@number-flow/react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ServerPageButtons({ server }: { server: ServerResponse }) {
|
||||
export function ServerPageButtons({
|
||||
server,
|
||||
mhsfData,
|
||||
}: {
|
||||
server: ServerResponse;
|
||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||
}) {
|
||||
const clerk = useClerk();
|
||||
const favoritesStore = useFavoriteStore(server.name);
|
||||
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(server.name);
|
||||
await favoritesStore.toggleFavorite();
|
||||
setLoading(false);
|
||||
}}
|
||||
disabled={loading || favoritesStore.isFavorite === null}
|
||||
@ -61,9 +74,10 @@ export function ServerPageButtons({ server }: { server: ServerResponse }) {
|
||||
/>
|
||||
Favorite
|
||||
{favoritesStore.favoriteNumber !== null && (
|
||||
<code>{favoritesStore.favoriteNumber}</code>
|
||||
<code>
|
||||
<NumberFlow value={favoritesStore.favoriteNumber} />{" "}
|
||||
</code>
|
||||
)}
|
||||
{loading && <Spinner />}
|
||||
</Button>
|
||||
</SignedIn>
|
||||
<SignedOut>
|
||||
@ -78,6 +92,28 @@ export function ServerPageButtons({ server }: { server: ServerResponse }) {
|
||||
)}
|
||||
</Button>
|
||||
</SignedOut>
|
||||
</>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
className="flex items-center"
|
||||
size="square-md"
|
||||
variant="secondary"
|
||||
>
|
||||
<EllipsisVertical size={16} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,11 +5,43 @@ import { ServerPageTags } from "./server-page-tags";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ServerRows } from "./server-rows";
|
||||
import { ServerPageButtons } from "./server-page-buttons";
|
||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function ServerMainPage({
|
||||
server,
|
||||
mhsfData,
|
||||
}: {
|
||||
server: ServerResponse;
|
||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (mhsfData.server?.customizationData.banner !== undefined)
|
||||
window.dispatchEvent(new Event("force-dark-mode"));
|
||||
});
|
||||
|
||||
export function ServerMainPage({ server }: { server: ServerResponse }) {
|
||||
return (
|
||||
<div className="pt-[150px] xl:px-[100px]">
|
||||
<span className="flex items-center gap-2 w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"xl:px-[100px]",
|
||||
mhsfData.server?.customizationData.banner === undefined
|
||||
? "pt-[150px]"
|
||||
: "pt-[300px]"
|
||||
)}
|
||||
>
|
||||
{mhsfData.server?.customizationData.banner && (
|
||||
<img
|
||||
src={mhsfData.server?.customizationData.banner}
|
||||
alt="User provided banner for server"
|
||||
className="rounded align-middle block ml-auto mr-auto absolute left-0 z-0 w-full object-fill"
|
||||
style={{
|
||||
maskImage: "linear-gradient(to top, transparent, black)",
|
||||
top: "0",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="flex items-center gap-2 w-full relative">
|
||||
<div className="bg-secondary p-4 rounded-lg ml-4">
|
||||
<IconDisplay server={server} />
|
||||
</div>
|
||||
@ -17,7 +49,7 @@ export function ServerMainPage({ server }: { server: ServerResponse }) {
|
||||
<div className="flex justify-between w-full">
|
||||
<h1 className="text-2xl font-bold">{server.name}</h1>
|
||||
<span>
|
||||
<ServerPageButtons server={server} />
|
||||
<ServerPageButtons server={server} mhsfData={mhsfData} />
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex items-center gap-2 flex-wrap">
|
||||
@ -26,7 +58,7 @@ export function ServerMainPage({ server }: { server: ServerResponse }) {
|
||||
</p>
|
||||
</span>
|
||||
<Separator className="my-6" />
|
||||
<ServerRows server={server} />
|
||||
<ServerRows server={server} mhsfData={mhsfData} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,19 +2,20 @@
|
||||
import { Placeholder } from "@/components/ui/placeholder";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useServer } from "@/lib/hooks/use-server";
|
||||
import type { OnlineServer } from "@/lib/types/mh-server";
|
||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
||||
import { X } from "lucide-react";
|
||||
import { ServerMainPage } from "./server-page";
|
||||
import { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
import { AnimatedText } from "@/components/ui/animated-text";
|
||||
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";
|
||||
|
||||
export function ServerProvider({ serverId }: { serverId: string }) {
|
||||
const { server, error, loading } = useServer({ id: serverId });
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<div className="absolute top-[50%] left-[50%]">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
const settings = useSettingsStore();
|
||||
const mhsf = useMHSFServer(serverId);
|
||||
|
||||
if (error !== null)
|
||||
return (
|
||||
@ -33,8 +34,48 @@ export function ServerProvider({ serverId }: { serverId: string }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="px-10">
|
||||
<ServerMainPage server={server as OnlineServer} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -32,14 +32,15 @@ import type { ServerResponse } from "@/lib/types/mh-server";
|
||||
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";
|
||||
|
||||
export function ServerRows({ server }: { server: ServerResponse }) {
|
||||
export function ServerRows({ server, mhsfData }: { server: ServerResponse, mhsfData: ReturnType<typeof useMHSFServer> }) {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return (
|
||||
<span className="lg:grid lg:grid-cols-3 w-full gap-3">
|
||||
<MOTDRow server={server} />
|
||||
<StatisticsMainRow server={server} />
|
||||
<StatisticsMainRow server={server} mhsfData={mhsfData} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,12 +1,170 @@
|
||||
"use client";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { convert } from "../util";
|
||||
import { Material } from "@/components/ui/material";
|
||||
|
||||
export function StatisticsMainRow({
|
||||
server,
|
||||
mhsfData,
|
||||
}: {
|
||||
server: ServerResponse;
|
||||
mhsfData: ReturnType<typeof useMHSFServer>;
|
||||
}) {
|
||||
const [statisticType, setStatisticType] = useQueryState("st", {
|
||||
defaultValue: "playerCount",
|
||||
});
|
||||
|
||||
export function StatisticsMainRow({ server }: { server: ServerResponse }) {
|
||||
return (
|
||||
<span className="border rounded-xl p-4 relative col-span-2 min-h-[250px] max-h-[250px]">
|
||||
<Material
|
||||
className="relative col-span-2 h-[250px] max-lg:mt-3"
|
||||
padding="none"
|
||||
>
|
||||
<div className="p-4">
|
||||
<span className="flex gap-4 mb-2">
|
||||
<strong className="text-lg">Statistics</strong>
|
||||
<br />
|
||||
<Separator className="my-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",
|
||||
statisticType === "playerCount" &&
|
||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium"
|
||||
)}
|
||||
onClick={() => setStatisticType("playerCount")}
|
||||
>
|
||||
Player Count
|
||||
<Badge className="px-1">
|
||||
<code>{convert(server.joins)}</code>
|
||||
</Badge>
|
||||
</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",
|
||||
statisticType === "favorites" &&
|
||||
"bg-slate-100 dark:bg-zinc-700/30 font-medium"
|
||||
)}
|
||||
onClick={() => setStatisticType("favorites")}
|
||||
>
|
||||
Favorites
|
||||
<Badge className="px-1">
|
||||
<code>
|
||||
{convert(
|
||||
mhsfData.server?.favoriteData.favoriteNumber as number
|
||||
)}
|
||||
</code>
|
||||
</Badge>
|
||||
</button>
|
||||
</span>
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{!mhsfData.loading && (
|
||||
<StatisticsChart
|
||||
data={
|
||||
statisticType === "playerCount"
|
||||
? mhsfData.server?.playerData.historically
|
||||
: mhsfData.server?.favoriteData.favoriteHistoricalData
|
||||
}
|
||||
mainDataPoint={statisticType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Material>
|
||||
);
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
playerCount: {
|
||||
label: "Joins",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
favorites: {
|
||||
label: "Favorites",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function StatisticsChart({
|
||||
data,
|
||||
mainDataPoint,
|
||||
}: {
|
||||
data: any;
|
||||
mainDataPoint: string;
|
||||
}) {
|
||||
console.log(data);
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="max-h-[202px] min-w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={data.slice(data.length - 30, data.length)}
|
||||
margin={{
|
||||
top: 30,
|
||||
}}
|
||||
className="rounded-b-xl"
|
||||
>
|
||||
<CartesianGrid vertical={false} horizontal={false} />
|
||||
<XAxis dataKey="date" tickLine={false} axisLine={false} tick={false} />
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="w-[150px]"
|
||||
nameKey={mainDataPoint}
|
||||
indicator="line"
|
||||
labelFormatter={(value) => {
|
||||
return `${new Date(value).toLocaleDateString("en-US", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
})} ${new Date(value).toLocaleTimeString("en-US", {
|
||||
timeStyle: "short",
|
||||
})}`;
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`fill${mainDataPoint}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="25%"
|
||||
stopColor={`var(--color-${mainDataPoint})`}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={`var(--color-${mainDataPoint})`}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
dataKey={mainDataPoint}
|
||||
type="natural"
|
||||
fill={`url(#fill${mainDataPoint})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${mainDataPoint})`}
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
124
apps/www/src/components/feat/server-page/util.ts
Normal file
124
apps/www/src/components/feat/server-page/util.ts
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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 { version } from "@/config/version";
|
||||
|
||||
export function convert(value: number) {
|
||||
let result: string = value.toString();
|
||||
if (value >= 1000000) {
|
||||
result = Math.floor(value / 1000000) + "m";
|
||||
} else if (value >= 1000) {
|
||||
result = Math.floor(value / 1000) + "k";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const loadingList = [
|
||||
"Making gamer's safer",
|
||||
"Finding why Apple is so expensive",
|
||||
"Finding why MHSF v" + version + " is so bad",
|
||||
"Finding why MHSF is so slow",
|
||||
"Finding why MHSF is loading",
|
||||
"Finding why MHSF is open-source",
|
||||
"Changing the license of MHSF",
|
||||
"Going through the American school system",
|
||||
"Finding how TypeScript is clearly better than Clojure",
|
||||
"Convincing Valerie to use a web framework",
|
||||
"Joining the Minehut Discord server",
|
||||
"Putting the fries in the bag",
|
||||
"Finding why Minehut's auto-mod is garbage",
|
||||
"Convincing Emopedia to travel to America",
|
||||
"Telling people why Next.js is the best web framework",
|
||||
"Asking Tim about MHSF compliance",
|
||||
"Debating Minehut's rules",
|
||||
"Convincing Valerie that Apple is a light-hearted company",
|
||||
"Convincing the average 'I use arch btw' person that Macintosh isn't that bad",
|
||||
"Repeating history",
|
||||
"Inventing a time machine",
|
||||
"Reinventing SSH",
|
||||
"Reinventing MHSF",
|
||||
"Figuring out why 'emacs' is a good editor",
|
||||
"Taking a design class",
|
||||
"Making a passive aggressive comment",
|
||||
"Supporting transgender",
|
||||
"Installing hyfetch",
|
||||
"Upgrading from Apple M2 to M3",
|
||||
"Watching the newest Minehut live stream",
|
||||
"Transitioning MHSF from Vercel to self-hosted",
|
||||
// generic xD
|
||||
"Loading",
|
||||
|
||||
"Opening Spotify",
|
||||
"Pinging another staff member because of a spam message",
|
||||
"Clarifying Minehut rules regarding APIs",
|
||||
"Breaking the Minehut TOS",
|
||||
"Figuring out why Skript is used in the first place",
|
||||
"Creating a new Velocity fork",
|
||||
"Convincing yet another person that Firefox doesn't support all MHSF features",
|
||||
"Reinventing accounts",
|
||||
"Redoing MHSF",
|
||||
"Typing that one Vesktop emoji",
|
||||
"Talking to my besties",
|
||||
"Explaining how I'm clearly a 'girly pop'",
|
||||
"Supporting GamerSafer's company values",
|
||||
"Welcoming a new staff member",
|
||||
"v2?",
|
||||
"Listening to Tyler The Creator",
|
||||
"Explaining to somebody why we don't need to hear your political ideas",
|
||||
"Hearing yet another person complaining about their punishment",
|
||||
"Explaining brainrot to somebody that clearly doesn't need to hear it",
|
||||
"Telling somebody 'ily' who clearly doesn't know you",
|
||||
"Figuring out how to get a random item out of a list",
|
||||
"Watching how your a beautiful person",
|
||||
"Listening to music",
|
||||
"Explaining how I wasn't trying to be mean",
|
||||
"Telling somebody how AI is going to take over the world",
|
||||
"'tolking 2 my frends on da cmputer'",
|
||||
"Explaining how I just get it",
|
||||
"Getting the 'Jamie Fan' role",
|
||||
"Finding where Minehut's peak was",
|
||||
"Making a new ticket on Minehut",
|
||||
"Contacting Minehut",
|
||||
"My bad man",
|
||||
"Adding 'use client' to a Next.js component file",
|
||||
"Blaming being annoying on the depression",
|
||||
"Explaining why Minehut needs to be PG13",
|
||||
"Explaining why Minehut needs a 4 hour limit on free servers",
|
||||
"Explaining how Minehut's API works",
|
||||
"Sleeping",
|
||||
"Complaining about how I can't eat lunch early",
|
||||
"Greeting you",
|
||||
"Creating a new Paper fork",
|
||||
"Creating the modern sever list",
|
||||
"Using all of the buzzwords",
|
||||
"Chatting in #queen-jamie-chat",
|
||||
"Asking for a new phone",
|
||||
"Asking to donate to an open-source project",
|
||||
];
|
||||
@ -52,10 +52,12 @@ export function BrowserSettings() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const [fontFamily, setFontFamily] = useState("inter");
|
||||
const [mcFont, setMcFont] = useState(true);
|
||||
const [debugMode, setDebugMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
||||
setMcFont((settingsStore.get("mc-font") === "true") as boolean);
|
||||
setDebugMode((settingsStore.get("debug-mode") === "true") as boolean);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@ -119,6 +121,22 @@ export function BrowserSettings() {
|
||||
</Select>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>Debug Mode</SettingTitle>
|
||||
<SettingDescription>Enable debug mode to show debug options</SettingDescription>
|
||||
</SettingMeta>
|
||||
<Switch
|
||||
checked={debugMode}
|
||||
onCheckedChange={(c) => {
|
||||
settingsStore.set("debug-mode", c, false);
|
||||
window.dispatchEvent(new Event("debug-mode-change"));
|
||||
setDebugMode(c);
|
||||
}}
|
||||
/>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</Material>
|
||||
);
|
||||
}
|
||||
|
||||
@ -28,45 +28,39 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { MongoClient } from "mongodb";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
import { Material } from "@/components/ui/material";
|
||||
import { Setting, SettingContent, SettingDescription, SettingMeta, SettingTitle } from "./setting";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AnimatedText } from "@/components/ui/animated-text";
|
||||
import { useState } from "react";
|
||||
import { loadingList } from "../server-page/util";
|
||||
|
||||
export default async function handler(
|
||||
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;
|
||||
export function DebugSettings() {
|
||||
const [randomText, setRandomText] = useState("")
|
||||
|
||||
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();
|
||||
|
||||
if (result.length > 0) {
|
||||
const resultedData = result[0];
|
||||
data.push({
|
||||
relativePrecentage: d.player_count / resultedData.total_players,
|
||||
date: dateOfEntry,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Close the database, but don't close this
|
||||
// serverless instance until it happens
|
||||
waitUntil(client.close());
|
||||
res.send({ data });
|
||||
return (
|
||||
<Material className="mt-6 grid gap-4">
|
||||
<h2 className="text-xl font-semibold text-inherit">Debug Settings</h2>
|
||||
<Setting>
|
||||
<SettingContent>
|
||||
<SettingMeta>
|
||||
<SettingTitle>
|
||||
Generate loading text
|
||||
</SettingTitle>
|
||||
<SettingDescription>
|
||||
Generate a random loading text
|
||||
</SettingDescription>
|
||||
</SettingMeta>
|
||||
<div className="block pb-6">
|
||||
<Button onClick={() => {
|
||||
setRandomText(loadingList[Math.floor(Math.random() * loadingList.length)])
|
||||
}}>
|
||||
Generate
|
||||
</Button>
|
||||
<AnimatedText className="font-bold" text={randomText + "..."}/>
|
||||
</div>
|
||||
</SettingContent>
|
||||
</Setting>
|
||||
</Material>
|
||||
);
|
||||
}
|
||||
@ -37,10 +37,22 @@ import { cn } from "@/lib/utils";
|
||||
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
||||
import { ExternalLink, Globe, TabletSmartphone } from "lucide-react";
|
||||
import { BrowserSettings } from "./browser-settings";
|
||||
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||
import { useEffect, useState } from "react";
|
||||
import { DebugSettings } from "./debug-settings";
|
||||
|
||||
export function Settings() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const [debugEnabled, setDebugEnabled] = useState(false);
|
||||
const clerk = useClerk();
|
||||
|
||||
useEffect(() => {
|
||||
setDebugEnabled((settingsStore.get("debug-mode") === "true") as boolean);
|
||||
window.addEventListener("debug-mode-change", () => {
|
||||
setDebugEnabled((settingsStore.get("debug-mode") === "true") as boolean);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="lg:px-[10rem] px-4">
|
||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
||||
@ -62,6 +74,15 @@ export function Settings() {
|
||||
<TabletSmartphone size={16} />
|
||||
User stored settings
|
||||
</TabsTrigger>
|
||||
{debugEnabled && (
|
||||
<TabsTrigger
|
||||
value="debug-settings"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
Debug settings
|
||||
</TabsTrigger>
|
||||
)}
|
||||
|
||||
<SignedIn>
|
||||
<TabsTrigger
|
||||
value="account-settings"
|
||||
@ -75,6 +96,10 @@ export function Settings() {
|
||||
<TabsContent value="browser-settings">
|
||||
<BrowserSettings />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="debug-settings">
|
||||
<DebugSettings />
|
||||
</TabsContent>
|
||||
<TabsContent value="user-settings">
|
||||
<SignedOut>
|
||||
<Material className="mt-6 grid gap-4 py-6">
|
||||
|
||||
@ -48,7 +48,7 @@ function Alert({
|
||||
<Material
|
||||
padding="sm"
|
||||
className={cn(
|
||||
"flex flex-row space-x-2 items-center",
|
||||
"flex flex-row items-center",
|
||||
variant === "error"
|
||||
? "bg-[#fdeded_!important] dark:bg-[#160b0b_!important]"
|
||||
: variant === "warning"
|
||||
@ -62,8 +62,9 @@ function Alert({
|
||||
{icon ? (
|
||||
icon
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<CircleAlert
|
||||
size={18}
|
||||
size={16}
|
||||
className={
|
||||
variant === "error"
|
||||
? "text-[#d76463] dark:text-[#df2317]"
|
||||
@ -74,8 +75,9 @@ function Alert({
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
)}{" "}
|
||||
<p>{children}</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="flex-1">{children}</p>
|
||||
</Material>
|
||||
);
|
||||
}
|
||||
|
||||
34
apps/www/src/components/ui/animated-text.tsx
Normal file
34
apps/www/src/components/ui/animated-text.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AnimatedTextProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AnimatedText({ text, className }: AnimatedTextProps) {
|
||||
const [currentText, setCurrentText] = useState(text);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentText(text);
|
||||
}, [text]);
|
||||
|
||||
return (
|
||||
<div className="relative h-6 min-w-[200px] text-sm">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.span
|
||||
key={currentText}
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: -20, opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className={cn("absolute left-0", className)}
|
||||
>
|
||||
{currentText}
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
353
apps/www/src/components/ui/chart.tsx
Normal file
353
apps/www/src/components/ui/chart.tsx
Normal file
@ -0,0 +1,353 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
}) {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color
|
||||
)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
@ -51,7 +51,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay ref={ref} asChild {...props}>
|
||||
<motion.div
|
||||
className={cn(
|
||||
"fixed bg-[#ffffff]/50 dark:bg-black/50 inset-0 backdrop-blur-xs data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"fixed bg-[#ffffff]/50 dark:bg-black/50 inset-0 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
initial={{ opacity: 0 }}
|
||||
@ -95,12 +95,12 @@ const DialogContent = React.forwardRef<
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn(
|
||||
"top-[50%] left-[50%] max-h-[85vh] translate-x-[-50%] translate-y-[-50%] focus:outline-hidden",
|
||||
"top-[50%] left-[50%] max-h-[85vh] translate-x-[-50%] translate-y-[-50%] focus:outline-none",
|
||||
"w-full border border-slate-200 border-b-slate-300",
|
||||
"dark:border-zinc-900 dark:border-t-zinc-800 dark:border-b-zinc-900",
|
||||
"rounded-2xl max-w-lg box-border mx-auto overscroll-contain shadow-lg overflow-auto",
|
||||
"p-5 flex flex-col gap-2 dark:bg-zinc-950 rounded-xl",
|
||||
"bg-white fixed",
|
||||
"bg-white fixed z-9",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
132
apps/www/src/components/ui/drawer.tsx
Normal file
132
apps/www/src/components/ui/drawer.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
@ -28,7 +28,7 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
import type { SVGProps } from "react";
|
||||
const Github = (props: SVGProps<SVGSVGElement>) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
@ -44,7 +44,7 @@ const TabsList = React.forwardRef<
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-1 p-1 rounded-full bg-white/60 dark:bg-zinc-800/60 backdrop-blur-lg border border-slate-200/60 dark:border-zinc-800 shadow-lg border-opacity-50",
|
||||
"flex flex-row items-center gap-1 max-w-full p-1 rounded-full bg-white/60 dark:bg-zinc-800/60 backdrop-blur-lg border border-slate-200/60 dark:border-zinc-800 shadow-lg border-opacity-50 overflow-x-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -50,7 +50,7 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-shadcn-primary px-3 py-1.5 text-xs text-shadcn-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-120 overflow-hidden rounded-md bg-shadcn-primary px-3 py-1.5 text-xs text-shadcn-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"border dark:border-slate-200 border-zinc-800 dark:border-b-slate-300 border-t-zinc-700",
|
||||
className
|
||||
)}
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
|
||||
import { ClerkProvider as ImportedClerkProvider } from "@clerk/nextjs";
|
||||
import { dark } from "@clerk/themes";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
import { MultisessionAppSupport } from "@clerk/nextjs/internal";
|
||||
|
||||
export const ClerkProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
|
||||
"use client";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
||||
@ -33,15 +33,30 @@
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemeProvider } from "next-themes";
|
||||
import type { ThemeProviderProps } from "next-themes";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
const pathname = usePathname();
|
||||
const [forcedDark, setForcedDark] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
window.addEventListener("force-dark-mode", () => {
|
||||
setForcedDark(true);
|
||||
});
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
setForcedDark(false);
|
||||
}, [pathname]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return <NextThemeProvider {...props}>{children}</NextThemeProvider>;
|
||||
return (
|
||||
<NextThemeProvider forcedTheme={forcedDark ? "dark" : undefined} {...props}>
|
||||
{children}
|
||||
</NextThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
*/
|
||||
|
||||
import type { BadgeColor } from "@/components/feat/server-list/server-card";
|
||||
import { isFavorited } from "@/lib/api";
|
||||
import { MHSFData } from "@/lib/types/data";
|
||||
import type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
||||
import { Cake, ServerCog } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
@ -56,6 +56,7 @@ export const allTags: Array<{
|
||||
condition?: (server: {
|
||||
online?: OnlineServer;
|
||||
server?: ServerResponse;
|
||||
mhsfData?: MHSFData;
|
||||
}) => Promise<boolean>;
|
||||
tooltipDesc: string;
|
||||
htmlDocs: string;
|
||||
@ -200,12 +201,9 @@ export const allTags: Array<{
|
||||
},
|
||||
{
|
||||
name: async (s) => "Favorited",
|
||||
condition: async (s) => {
|
||||
const favorited = await isFavorited(
|
||||
(s.online ?? s.server ?? { name: "" }).name
|
||||
);
|
||||
return favorited;
|
||||
},
|
||||
condition: async (s) =>
|
||||
(s.mhsfData ?? { favoriteData: { favoritedByAccount: false } })
|
||||
.favoriteData.favoritedByAccount ?? false,
|
||||
tooltipDesc: "This tag represents that you favorited this server.",
|
||||
docsName: "Favorited",
|
||||
htmlDocs:
|
||||
|
||||
@ -168,7 +168,7 @@ export async function isFavorited(server: string): Promise<boolean> {
|
||||
export async function getAccountFavorites(): Promise<Array<string>> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
connector(`/favorites/account-favorites`, { version: 0 }),
|
||||
connector(`/user/favorites`, { version: 1 }),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTheme } from "@/lib/hooks/use-theme";
|
||||
|
||||
export function useDepTheme() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
29
apps/www/src/lib/history-util.tsx
Normal file
29
apps/www/src/lib/history-util.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
export async function getServerQuery(selector: string) {
|
||||
if (selector.length === 24) {
|
||||
// Server is id;
|
||||
|
||||
const res = await fetch(`https://api.minehut.com/server/${selector}`);
|
||||
const json = await res.json();
|
||||
|
||||
if (json.server === null) return null;
|
||||
|
||||
return { $or: [{ serverId: selector }, { server: json.server.name }] };
|
||||
}
|
||||
// Legacy behavior
|
||||
return { server: selector };
|
||||
}
|
||||
|
||||
export async function getServerName(selector: string) {
|
||||
if (selector.length === 24) {
|
||||
// Server is id
|
||||
|
||||
const res = await fetch(`https://api.minehut.com/server/${selector}`);
|
||||
const json = await res.json();
|
||||
|
||||
if (json.server === null) return null;
|
||||
|
||||
return json.server.name;
|
||||
}
|
||||
|
||||
return selector;
|
||||
}
|
||||
@ -1,13 +1,9 @@
|
||||
import { useClerk } from "@clerk/nextjs";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
favoriteServer,
|
||||
getAccountFavorites,
|
||||
getCommunityServerFavorites,
|
||||
isFavorited,
|
||||
} from "../api";
|
||||
import { getAccountFavorites } from "../api";
|
||||
import { useMHSFServer } from "./use-mhsf-server";
|
||||
|
||||
export function useFavoriteStore(server?: string) {
|
||||
export function useFavoriteStore(server?: ReturnType<typeof useMHSFServer>) {
|
||||
const [favorites, setFavorites] = useState<string[] | null>(null);
|
||||
const [isFavorite, setIsFavorite] = useState<boolean | null>(null);
|
||||
const [favoriteNumber, setFavoriteNumber] = useState<number | null>(null);
|
||||
@ -17,12 +13,20 @@ export function useFavoriteStore(server?: string) {
|
||||
if (isSignedIn) {
|
||||
getAccountFavorites().then((favorites) => setFavorites(favorites));
|
||||
}
|
||||
if (server) {
|
||||
getCommunityServerFavorites(server).then((number) =>
|
||||
setFavoriteNumber(number)
|
||||
);
|
||||
if (
|
||||
server !== null &&
|
||||
server?.loading === false &&
|
||||
server?.server !== null
|
||||
) {
|
||||
setFavoriteNumber(server.server.favoriteData.favoriteNumber);
|
||||
if (isFavorite === null) {
|
||||
isFavorited(server).then((isFavorite) => setIsFavorite(isFavorite));
|
||||
server
|
||||
.reloadServerData()
|
||||
.then(() =>
|
||||
setIsFavorite(
|
||||
server.server?.favoriteData.favoritedByAccount ?? false
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [isSignedIn, server, isFavorite]);
|
||||
@ -38,12 +42,24 @@ export function useFavoriteStore(server?: string) {
|
||||
loadingNumber: favoriteNumber === null,
|
||||
favoriteNumber,
|
||||
isFavorite,
|
||||
toggleFavorite: async (server: string) => {
|
||||
toggleFavorite: async () => {
|
||||
if (isFavorite === null) throw new Error("Hold up lemme load rq");
|
||||
if (favoriteNumber === null) throw new Error("Nah");
|
||||
await favoriteServer(server);
|
||||
const favoriteSync = await server?.favoriteServer();
|
||||
|
||||
// Resolve remote differences
|
||||
await server?.reloadServerData();
|
||||
|
||||
if (
|
||||
favoriteSync?.favorited !==
|
||||
(server?.server?.favoriteData.favoritedByAccount ?? false)
|
||||
)
|
||||
throw new Error(
|
||||
"Server is not synced between server data & server favorite response."
|
||||
);
|
||||
|
||||
setIsFavorite(server?.server?.favoriteData.favoritedByAccount ?? false);
|
||||
|
||||
if (isFavorite === true) {
|
||||
setIsFavorite(false);
|
||||
setFavoriteNumber(favoriteNumber - 1);
|
||||
@ -53,7 +69,5 @@ export function useFavoriteStore(server?: string) {
|
||||
setFavoriteNumber(favoriteNumber + 1);
|
||||
}
|
||||
},
|
||||
getServerFavoritesNumber: async (server: string) =>
|
||||
await getCommunityServerFavorites(server),
|
||||
};
|
||||
}
|
||||
|
||||
141
apps/www/src/lib/hooks/use-mhsf-multiple.tsx
Normal file
141
apps/www/src/lib/hooks/use-mhsf-multiple.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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 type { MHSFData } from "../types/data";
|
||||
import type { RouteParams } from "@/pages/api/v1/server/get/[server]";
|
||||
|
||||
export function useMHSFServer(id: string[], fast?: boolean) {
|
||||
const [data, setData] = useState<{
|
||||
[key: string]: MHSFData & RouteParams;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (id.length === 0) return;
|
||||
(async () => {
|
||||
const response = await fetch(
|
||||
`/api/v1/server/bulk${fast === true ? "?noStatistics=true" : ""}`,
|
||||
{
|
||||
body: JSON.stringify({ servers: id }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
const json = await response.json();
|
||||
|
||||
setData(json.servers);
|
||||
})();
|
||||
}, [id.length]);
|
||||
|
||||
return {
|
||||
loading: data === null,
|
||||
getServer: (id: string) => {
|
||||
const server = data?.[id];
|
||||
|
||||
return {
|
||||
server,
|
||||
loading: false,
|
||||
reloadServerData: async () => {
|
||||
const response = await fetch("/api/v1/server/get/" + id);
|
||||
const json = await response.json();
|
||||
|
||||
if (data !== null) {
|
||||
const dataCopy = data;
|
||||
dataCopy[id] = json.server;
|
||||
|
||||
setData(dataCopy);
|
||||
}
|
||||
},
|
||||
favoriteServer: async () => {
|
||||
if (!server)
|
||||
throw new Error("Server hasn't initialized, cannot continue.");
|
||||
|
||||
const response = await fetch(server?.actions.favorite);
|
||||
const json: { favorited: boolean } = await response.json();
|
||||
|
||||
return json;
|
||||
},
|
||||
ownServer: async () => {
|
||||
if (!server)
|
||||
throw new Error("Server hasn't initialized, cannot continue.");
|
||||
|
||||
const response = await fetch(server?.actions.own);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
"Player doesn't own server or a server error occurred."
|
||||
);
|
||||
},
|
||||
customizeServer: async (customization: {
|
||||
colorScheme?:
|
||||
| "zinc"
|
||||
| "slate"
|
||||
| "stone"
|
||||
| "gray"
|
||||
| "neutral"
|
||||
| "red"
|
||||
| "rose"
|
||||
| "orange"
|
||||
| "green"
|
||||
| "blue"
|
||||
| "yellow"
|
||||
| "violet";
|
||||
description?: string;
|
||||
discord?: string;
|
||||
banner?: string;
|
||||
}) => {
|
||||
if (!server)
|
||||
throw new Error("Server hasn't initialized, cannot continue.");
|
||||
|
||||
const response = await fetch(server?.actions.customize, {
|
||||
body: JSON.stringify({ customization }),
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Error while customizing server.");
|
||||
},
|
||||
reportServer: async (reason: string) => {
|
||||
if (!server)
|
||||
throw new Error("Server hasn't initialized, cannot continue.");
|
||||
|
||||
const response = await fetch(server.actions.report, {
|
||||
body: JSON.stringify({ reason }),
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
"Error while reporting server. Please email support@mhsf.app if reporting again breaks."
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
121
apps/www/src/lib/hooks/use-mhsf-server.tsx
Normal file
121
apps/www/src/lib/hooks/use-mhsf-server.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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 type { MHSFData } from "../types/data";
|
||||
import type { RouteParams } from "@/pages/api/v1/server/get/[server]";
|
||||
|
||||
export function useMHSFServer(id: string) {
|
||||
const [server, setServer] = useState<(MHSFData & RouteParams) | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await fetch("/api/v1/server/get/" + id);
|
||||
const json = await response.json();
|
||||
|
||||
console.log(json.server);
|
||||
|
||||
setServer(json.server);
|
||||
})();
|
||||
}, [id]);
|
||||
|
||||
return {
|
||||
server,
|
||||
loading: server === null,
|
||||
reloadServerData: async () => {
|
||||
const response = await fetch("/api/v1/server/get/" + id);
|
||||
const json = await response.json();
|
||||
|
||||
setServer(json.server);
|
||||
},
|
||||
favoriteServer: async () => {
|
||||
if (!server)
|
||||
throw new Error("Server hasn't initialized, cannot continue.");
|
||||
|
||||
const response = await fetch(server?.actions.favorite);
|
||||
const json: { favorited: boolean } = await response.json();
|
||||
|
||||
return json;
|
||||
},
|
||||
ownServer: async () => {
|
||||
if (!server)
|
||||
throw new Error("Server hasn't initialized, cannot continue.");
|
||||
|
||||
const response = await fetch(server?.actions.own);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
"Player doesn't own server or a server error occurred."
|
||||
);
|
||||
},
|
||||
customizeServer: async (customization: {
|
||||
colorScheme?:
|
||||
| "zinc"
|
||||
| "slate"
|
||||
| "stone"
|
||||
| "gray"
|
||||
| "neutral"
|
||||
| "red"
|
||||
| "rose"
|
||||
| "orange"
|
||||
| "green"
|
||||
| "blue"
|
||||
| "yellow"
|
||||
| "violet";
|
||||
description?: string;
|
||||
discord?: string;
|
||||
banner?: string;
|
||||
}) => {
|
||||
if (!server)
|
||||
throw new Error("Server hasn't initialized, cannot continue.");
|
||||
|
||||
const response = await fetch(server?.actions.customize, {
|
||||
body: JSON.stringify({ customization }),
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Error while customizing server.");
|
||||
},
|
||||
reportServer: async (reason: string) => {
|
||||
if (!server)
|
||||
throw new Error("Server hasn't initialized, cannot continue.");
|
||||
|
||||
const response = await fetch(server.actions.report, {
|
||||
body: JSON.stringify({ reason }),
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
"Error while reporting server. Please email support@mhsf.app if reporting again breaks."
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import type { OnlineServer } from "../types/mh-server";
|
||||
import type { OnlineServer, ServerResponse } from "../types/mh-server";
|
||||
import { useEffectOnce } from "../useEffectOnce";
|
||||
|
||||
export function useServer(serverSpecifier: { id?: string; name?: string }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [server, setServer] = useState<OnlineServer | null>(null);
|
||||
const [server, setServer] = useState<ServerResponse | null>(null);
|
||||
|
||||
useEffectOnce(() => {
|
||||
try {
|
||||
|
||||
10
apps/www/src/lib/hooks/use-theme.tsx
Normal file
10
apps/www/src/lib/hooks/use-theme.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { useTheme as useLibTheme } from "next-themes";
|
||||
|
||||
export function useTheme() {
|
||||
const theme = useLibTheme();
|
||||
|
||||
return {
|
||||
...theme,
|
||||
resolvedTheme: theme.forcedTheme ?? theme.resolvedTheme,
|
||||
};
|
||||
}
|
||||
@ -1,127 +0,0 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
* All external content is rather licensed under the ECA Agreement
|
||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||
*
|
||||
* All code under MHSF is licensed under the MIT License
|
||||
* by open source contributors
|
||||
*
|
||||
* Copyright (c) 2025 dvelo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { 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,
|
||||
) {
|
||||
const { userId } = getAuth(req);
|
||||
const { server } = req.body;
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
|
||||
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 });
|
||||
|
||||
// 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." });
|
||||
}
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
* All external content is rather licensed under the ECA Agreement
|
||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||
*
|
||||
* All code under MHSF is licensed under the MIT License
|
||||
* by open source contributors
|
||||
*
|
||||
* Copyright (c) 2025 dvelo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { 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,
|
||||
) {
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
await 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);
|
||||
await 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,77 +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 { MongoClient } from "mongodb";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
|
||||
export default async function handler(
|
||||
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 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;
|
||||
|
||||
return { day: daysOfWeek[c - 1], result: Math.floor(average) };
|
||||
}
|
||||
return 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) });
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
* All external content is rather licensed under the ECA Agreement
|
||||
* located here: https://mhsf.app/docs/legal/external-content-agreement
|
||||
*
|
||||
* All code under MHSF is licensed under the MIT License
|
||||
* by open source contributors
|
||||
*
|
||||
* Copyright (c) 2025 dvelo
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { MongoClient } from "mongodb";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
|
||||
export default async function handler(
|
||||
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 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;
|
||||
|
||||
return { month: months[c - 1], result: Math.floor(average) };
|
||||
}
|
||||
return 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) });
|
||||
}
|
||||
@ -31,6 +31,7 @@
|
||||
import type { MHSFData } from "@/lib/types/data";
|
||||
import { MongoClient } from "mongodb";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { RouteParams } from "./get/[server]";
|
||||
|
||||
// Type definitions for query parameters
|
||||
type QueryParams = {
|
||||
@ -43,6 +44,7 @@ type QueryParams = {
|
||||
maxAchievementEntries?: string | string[];
|
||||
achievementTimespanStart?: string | string[];
|
||||
achievementTimespanEnd?: string | string[];
|
||||
noStatistics?: string | string[];
|
||||
};
|
||||
|
||||
// Type for customization data
|
||||
@ -91,8 +93,10 @@ export default async function handler(
|
||||
return res.status(400).json({ servers: {} });
|
||||
}
|
||||
|
||||
let serverList = servers;
|
||||
|
||||
// Limit the number of servers to prevent abuse (max 25 servers per request)
|
||||
const serverList = servers.slice(0, 25);
|
||||
if (req.query.noStatistics !== "true") serverList = servers.slice(0, 25);
|
||||
|
||||
// Extract query parameters
|
||||
const queryOptions: QueryParams = {
|
||||
@ -105,6 +109,7 @@ export default async function handler(
|
||||
maxAchievementEntries: req.query.maxAchievementEntries,
|
||||
achievementTimespanStart: req.query.achievementTimespanStart,
|
||||
achievementTimespanEnd: req.query.achievementTimespanEnd,
|
||||
noStatistics: req.query.noStatistics,
|
||||
};
|
||||
|
||||
// Determine which data to fetch based on options
|
||||
@ -158,7 +163,7 @@ export default async function handler(
|
||||
);
|
||||
}
|
||||
|
||||
if (fetchOptions.players) {
|
||||
if (fetchOptions.players && queryOptions.noStatistics !== "true") {
|
||||
promises.push(
|
||||
findPlayerData(serverData.name, db, queryOptions).then(
|
||||
(data: PlayerData) => {
|
||||
@ -168,7 +173,10 @@ export default async function handler(
|
||||
);
|
||||
}
|
||||
|
||||
if (fetchOptions.achievements) {
|
||||
if (
|
||||
fetchOptions.achievements &&
|
||||
queryOptions.noStatistics !== "true"
|
||||
) {
|
||||
promises.push(
|
||||
findAchievements(serverData.name, db, queryOptions).then(
|
||||
(data: AchievementsData) => {
|
||||
@ -182,7 +190,7 @@ export default async function handler(
|
||||
await Promise.all(promises);
|
||||
|
||||
// Create default values for any missing data
|
||||
const serverResult: MHSFData = {
|
||||
const serverResult: MHSFData & RouteParams = {
|
||||
favoriteData: promiseResults.favoriteData || {
|
||||
favoritedByAccount: null,
|
||||
favoriteNumber: 0,
|
||||
@ -205,6 +213,18 @@ export default async function handler(
|
||||
historically: [],
|
||||
currently: [],
|
||||
},
|
||||
actions: {
|
||||
history: {
|
||||
dailyData: `/api/v1/server/get/${server}/history/daily-data`,
|
||||
monthlyData: `/api/v1/server/get/${server}/history/monthly-data`,
|
||||
relativeData: `/api/v1/server/get/${server}/history/relative-data`,
|
||||
historicalData: `/api/v1/server/get/${server}/history/historical-data`,
|
||||
},
|
||||
favorite: `/api/v1/server/get/${server}/favorite-server`,
|
||||
customize: `/api/v1/server/get/${server}/customize`,
|
||||
own: `/api/v1/server/get/${server}/own-server`,
|
||||
report: `/api/v1/server/get/${server}/report-server`,
|
||||
},
|
||||
};
|
||||
|
||||
result[server] = serverResult;
|
||||
@ -287,7 +307,9 @@ async function findFavoriteData(
|
||||
const [userFavorites, metaData, historyData] = await Promise.all([
|
||||
userId ? db.collection("favorites").findOne({ user: userId }) : null,
|
||||
db.collection("meta").findOne({ server: serverName }),
|
||||
fetchHistoryData(db, serverName, query),
|
||||
query.noStatistics !== "true"
|
||||
? fetchHistoryData(db, serverName, query)
|
||||
: [],
|
||||
]);
|
||||
|
||||
// Process user favorites
|
||||
|
||||
121
apps/www/src/pages/api/v1/server/get/[server]/favorite-server.ts
Normal file
121
apps/www/src/pages/api/v1/server/get/[server]/favorite-server.ts
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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 { NextApiResponse, NextApiRequest } from "next";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { getAuth } from "@clerk/nextjs/server";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
import { getServerName } from "@/lib/history-util";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { userId } = getAuth(req);
|
||||
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
const server = await getServerName(req.query.server as string);
|
||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
||||
|
||||
if (!server) {
|
||||
return res.status(400).json({ error: "Server not provided" });
|
||||
}
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||
const favoritesCollection = db.collection("favorites");
|
||||
|
||||
// Use findOne instead of find().toArray() since we only need one document
|
||||
const userFavorites = await favoritesCollection.findOne({ user: userId });
|
||||
|
||||
if (!userFavorites) {
|
||||
// Use insertOne with { w: 1 } for write acknowledgment
|
||||
await favoritesCollection.insertOne(
|
||||
{ user: userId, favorites: [server] },
|
||||
{ w: 1 } as any
|
||||
);
|
||||
await increaseNum(client, server as string);
|
||||
return res.send({ favorited: true });
|
||||
}
|
||||
|
||||
const existingFavorites = userFavorites.favorites;
|
||||
const isFavorite = existingFavorites.includes(server);
|
||||
|
||||
// Update favorites array
|
||||
const updatedFavorites = isFavorite
|
||||
? existingFavorites.filter((fav: any) => fav !== server)
|
||||
: [...existingFavorites, server];
|
||||
|
||||
// Use updateOne instead of replaceOne for better performance
|
||||
await favoritesCollection.updateOne(
|
||||
{ _id: userFavorites._id },
|
||||
{ $set: { favorites: updatedFavorites } }
|
||||
);
|
||||
|
||||
// Update favorite count
|
||||
isFavorite
|
||||
? await decreaseNum(client, server as string)
|
||||
: await increaseNum(client, server as string);
|
||||
|
||||
res.send({ favorited: !isFavorite });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: "Internal Server Error" });
|
||||
} finally {
|
||||
// Ensure client is always closed
|
||||
waitUntil(client.close());
|
||||
}
|
||||
}
|
||||
|
||||
// Optimized helper functions
|
||||
export async function increaseNum(client: MongoClient, server: string) {
|
||||
const db = client.db("mhsf");
|
||||
const collection = db.collection("meta");
|
||||
|
||||
// Use $inc operator for atomic increment
|
||||
await collection.updateOne(
|
||||
{ server },
|
||||
{ $inc: { favorites: 1 }, $set: { date: new Date() } },
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
|
||||
export async function decreaseNum(client: MongoClient, server: string) {
|
||||
const db = client.db("mhsf");
|
||||
const collection = db.collection("meta");
|
||||
|
||||
// Use $inc operator for atomic decrement
|
||||
await collection.updateOne(
|
||||
{ server },
|
||||
{ $inc: { favorites: -1 } },
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
*
|
||||
* 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 { NextApiRequest, NextApiResponse } from "next";
|
||||
import { MongoClient as MongoClientImpl } from "mongodb";
|
||||
import { getServerQuery } from "@/lib/history-util";
|
||||
|
||||
interface DailyAverage {
|
||||
day: string;
|
||||
result: number;
|
||||
}
|
||||
|
||||
interface ResponseData {
|
||||
result: DailyAverage[];
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<ResponseData | { message: string }>
|
||||
) {
|
||||
const client = new MongoClientImpl(process.env.MONGO_DB as string);
|
||||
|
||||
try {
|
||||
const db = client.db("mhsf").collection("history");
|
||||
const server = await getServerQuery(req.query.server as string);
|
||||
const daysOfWeek = [
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
];
|
||||
|
||||
if (server === null)
|
||||
return res.status(400).json({ message: "Invalid server query" });
|
||||
|
||||
// Convert $or query to separate find operations if needed
|
||||
const matchStage = server.$or
|
||||
? {
|
||||
$match: {
|
||||
$or: server.$or,
|
||||
},
|
||||
}
|
||||
: { $match: server };
|
||||
|
||||
// Use MongoDB aggregation pipeline for better performance
|
||||
const dailyAverages = (await db
|
||||
.aggregate([
|
||||
matchStage,
|
||||
{
|
||||
$group: {
|
||||
_id: { $dayOfWeek: "$date" },
|
||||
averagePlayerCount: { $avg: "$player_count" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
day: { $arrayElemAt: [daysOfWeek, { $subtract: ["$_id", 1] }] },
|
||||
result: { $floor: "$averagePlayerCount" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { _id: 1 },
|
||||
},
|
||||
])
|
||||
.toArray()) as DailyAverage[];
|
||||
|
||||
res.send({ result: dailyAverages });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ message: "An error occurred while fetching data" });
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
*
|
||||
* 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 { NextApiRequest, NextApiResponse } from "next";
|
||||
import { MongoClient as MongoClientImpl } from "mongodb";
|
||||
import { getServerQuery } from "@/lib/history-util";
|
||||
|
||||
// Define types for our data
|
||||
interface ServerHistoricalRecord {
|
||||
server: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ResponseData {
|
||||
data: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<ResponseData | { message: string }>
|
||||
) {
|
||||
const client = new MongoClientImpl(process.env.MONGO_DB as string);
|
||||
|
||||
try {
|
||||
const db = client.db("mhsf").collection("historical");
|
||||
const server = await getServerQuery(req.query.server as string);
|
||||
const scopes: string[] = checkForInfoOrLeave(res, req.body.scopes);
|
||||
|
||||
if (server === null)
|
||||
return res.status(400).json({ message: "Invalid server query" });
|
||||
|
||||
// Only fetch the fields we need using projection
|
||||
const projection: Record<string, 1> = { server: 1 };
|
||||
for (const scope of scopes) {
|
||||
projection[scope] = 1;
|
||||
}
|
||||
|
||||
const allData = await db
|
||||
.find<ServerHistoricalRecord>({ server }, { projection })
|
||||
.toArray();
|
||||
|
||||
// Use map instead of forEach for better performance
|
||||
const data = allData.map((d) => {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const scope of scopes) {
|
||||
result[scope] = d[scope];
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
res.send({ data });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "An error occurred while fetching data" });
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
function checkForInfoOrLeave(
|
||||
res: NextApiResponse,
|
||||
info: string[] | undefined
|
||||
): string[] {
|
||||
if (info === undefined) {
|
||||
res.status(400).json({ message: "Information wasn't supplied" });
|
||||
return [];
|
||||
}
|
||||
return info;
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
*
|
||||
* 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 { NextApiRequest, NextApiResponse } from "next";
|
||||
import { MongoClient as MongoClientImpl } from "mongodb";
|
||||
import { getServerQuery } from "@/lib/history-util";
|
||||
|
||||
interface MonthlyAverage {
|
||||
month: string;
|
||||
result: number;
|
||||
}
|
||||
|
||||
interface ResponseData {
|
||||
result: MonthlyAverage[];
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<ResponseData | { message: string }>
|
||||
) {
|
||||
const client = new MongoClientImpl(process.env.MONGO_DB as string);
|
||||
|
||||
try {
|
||||
const db = client.db("mhsf").collection("history");
|
||||
const server = await getServerQuery(req.query.server as string);
|
||||
const months = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
if (server === null)
|
||||
return res.status(400).json({ message: "Invalid server query" });
|
||||
|
||||
// Convert $or query to separate find operations if needed
|
||||
const matchStage = server.$or
|
||||
? {
|
||||
$match: {
|
||||
$or: server.$or,
|
||||
|
||||
date: {
|
||||
$gte: new Date(currentYear, 0, 1),
|
||||
$lt: new Date(currentYear + 1, 0, 1),
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
$match: server,
|
||||
date: {
|
||||
$gte: new Date(currentYear, 0, 1),
|
||||
$lt: new Date(currentYear + 1, 0, 1),
|
||||
},
|
||||
};
|
||||
|
||||
// Use MongoDB aggregation pipeline for better performance
|
||||
const monthlyAverages = (await db
|
||||
.aggregate([
|
||||
matchStage,
|
||||
{
|
||||
$group: {
|
||||
_id: { $month: "$date" },
|
||||
averagePlayerCount: { $avg: "$player_count" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
month: { $arrayElemAt: [months, { $subtract: ["$_id", 1] }] },
|
||||
result: { $floor: "$averagePlayerCount" },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: { _id: 1 },
|
||||
},
|
||||
])
|
||||
.toArray()) as MonthlyAverage[];
|
||||
|
||||
res.send({ result: monthlyAverages });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "An error occurred while fetching data" });
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* MHSF, Minehut Server List
|
||||
*
|
||||
* 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 { NextApiRequest, NextApiResponse } from "next";
|
||||
import { MongoClient as MongoClientImpl } from "mongodb";
|
||||
import { getServerQuery } from "@/lib/history-util";
|
||||
|
||||
interface ServerHistoryRecord {
|
||||
server: string;
|
||||
player_count: number;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
interface MHRecord {
|
||||
total_players: number;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
interface RelativeData {
|
||||
relativePrecentage: number;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
interface ResponseData {
|
||||
data: RelativeData[];
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<ResponseData | { message: string }>
|
||||
) {
|
||||
const client = new MongoClientImpl(process.env.MONGO_DB as string);
|
||||
|
||||
try {
|
||||
const db = client.db("mhsf").collection("history");
|
||||
const mh = client.db("mhsf").collection("mh");
|
||||
const server = await getServerQuery(req.query.server as string);
|
||||
|
||||
if (server === undefined)
|
||||
return res.status(400).json({ message: "Invalid server query" });
|
||||
|
||||
// Handle both $or and simple server queries
|
||||
const findQuery = server?.$or ? { $or: server.$or } : server;
|
||||
|
||||
// Get only the last 30 records with needed fields
|
||||
const recentData = await db
|
||||
.find<ServerHistoryRecord>(findQuery ?? {}, {
|
||||
projection: { player_count: 1, date: 1 },
|
||||
sort: { date: -1 },
|
||||
limit: 30,
|
||||
})
|
||||
.toArray();
|
||||
|
||||
const data: RelativeData[] = [];
|
||||
|
||||
// Process in batches to reduce the number of database queries
|
||||
const batchSize = 5;
|
||||
for (let i = 0; i < recentData.length; i += batchSize) {
|
||||
const batch = recentData.slice(i, i + batchSize);
|
||||
const batchQueries = batch.map(async (d) => {
|
||||
const dateOfEntry = new Date(d.date);
|
||||
const hourBefore = new Date(dateOfEntry.getTime() - 1000 * 60 * 60);
|
||||
const hourAfter = new Date(dateOfEntry.getTime() + 1000 * 60 * 60);
|
||||
|
||||
const result = await mh.findOne<MHRecord>(
|
||||
{
|
||||
date: {
|
||||
$gte: hourBefore,
|
||||
$lt: hourAfter,
|
||||
},
|
||||
},
|
||||
{ projection: { total_players: 1, date: 1 } }
|
||||
);
|
||||
|
||||
if (result) {
|
||||
return {
|
||||
relativePrecentage: d.player_count / result.total_players,
|
||||
date: dateOfEntry,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const batchResults = await Promise.all(batchQueries);
|
||||
data.push(
|
||||
...batchResults.filter((item): item is RelativeData => item !== null)
|
||||
);
|
||||
}
|
||||
|
||||
res.send({ data });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).json({ message: "An error occurred while fetching data" });
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
@ -32,9 +32,24 @@ import type { MHSFData } from "@/lib/types/data";
|
||||
import { MongoClient } from "mongodb";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export type RouteParams = {
|
||||
actions: {
|
||||
favorite: string;
|
||||
customize: string;
|
||||
own: string;
|
||||
report: string;
|
||||
history: {
|
||||
dailyData: string;
|
||||
monthlyData: string;
|
||||
relativeData: string;
|
||||
historicalData: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<{ server: MHSFData | null }>
|
||||
res: NextApiResponse<{ server: (MHSFData & RouteParams) | null }>
|
||||
) {
|
||||
const {
|
||||
server,
|
||||
@ -81,13 +96,24 @@ export default async function handler(
|
||||
}),
|
||||
]);
|
||||
|
||||
// Ignore the linter error as requested
|
||||
res.send({
|
||||
server: {
|
||||
favoriteData,
|
||||
customizationData,
|
||||
playerData,
|
||||
achievements,
|
||||
actions: {
|
||||
history: {
|
||||
dailyData: `/api/v1/server/get/${server}/history/daily-data`,
|
||||
monthlyData: `/api/v1/server/get/${server}/history/monthly-data`,
|
||||
relativeData: `/api/v1/server/get/${server}/history/relative-data`,
|
||||
historicalData: `/api/v1/server/get/${server}/history/historical-data`,
|
||||
},
|
||||
favorite: `/api/v1/server/get/${server}/favorite-server`,
|
||||
customize: `/api/v1/server/get/${server}/customize`,
|
||||
own: `/api/v1/server/get/${server}/own-server`,
|
||||
report: `/api/v1/server/get/${server}/report-server`,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
127
apps/www/src/pages/api/v1/server/get/[server]/own-server.ts
Normal file
127
apps/www/src/pages/api/v1/server/get/[server]/own-server.ts
Normal file
@ -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 { 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
|
||||
) {
|
||||
const { userId } = getAuth(req);
|
||||
const { server } = req.query;
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
|
||||
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 });
|
||||
|
||||
// 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,15 +31,15 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getAuth } from "@clerk/nextjs/server";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { inngest } from "../inngest";
|
||||
import { inngest } from "@/pages/api/inngest";
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { userId } = getAuth(req);
|
||||
const { server } = req.body;
|
||||
const { server } = req.query;
|
||||
|
||||
if (server == null) {
|
||||
res.status(400).send({ message: "Couldn't find data" });
|
||||
@ -35,7 +35,7 @@ import { waitUntil } from "@vercel/functions";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const { userId } = getAuth(req);
|
||||
|
||||
@ -54,8 +54,8 @@ export default async function handler(
|
||||
waitUntil(client.close());
|
||||
|
||||
if (find.length == 0) {
|
||||
res.send({ result: [] });
|
||||
res.send({ favorites: [] });
|
||||
} else {
|
||||
res.send({ result: find[0].favorites });
|
||||
res.send({ favorites: find[0].favorites });
|
||||
}
|
||||
}
|
||||
41
yarn.lock
41
yarn.lock
@ -1741,6 +1741,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e"
|
||||
integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==
|
||||
|
||||
"@number-flow/react@^0.5.7":
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@number-flow/react/-/react-0.5.7.tgz#4e4bd997e5cf434e749ec1763d1723f4cd7fda5b"
|
||||
integrity sha512-Fm1ZTUx5SYFZcJ3NjNKzC513Sq0XbU//X1yIEJ33MLBNyff+S16akGvez1zD1EjBHbgRGUyyNKQP6U8u0cszvA==
|
||||
dependencies:
|
||||
esm-env "^1.1.4"
|
||||
number-flow "0.5.5"
|
||||
|
||||
"@opentelemetry/api-logs@0.39.1":
|
||||
version "0.39.1"
|
||||
resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.39.1.tgz#3ea1e9dda11c35f993cb60dc5e52780b8175e702"
|
||||
@ -2181,7 +2189,7 @@
|
||||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.5.5"
|
||||
|
||||
"@radix-ui/react-dialog@^1.1.1", "@radix-ui/react-dialog@^1.1.2":
|
||||
"@radix-ui/react-dialog@^1.1.1", "@radix-ui/react-dialog@^1.1.2", "@radix-ui/react-dialog@^1.1.6":
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz#65b4465e99ad900f28a98eed9a94bb21ec644bf7"
|
||||
integrity sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==
|
||||
@ -5919,6 +5927,11 @@ eslint@^9:
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.3"
|
||||
|
||||
esm-env@^1.1.4:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/esm-env/-/esm-env-1.2.2.tgz#263c9455c55861f41618df31b20cb571fc20b75e"
|
||||
integrity sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==
|
||||
|
||||
espree@^10.0.1, espree@^10.3.0:
|
||||
version "10.3.0"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a"
|
||||
@ -8095,16 +8108,16 @@ lru-cache@^7.14.1:
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
|
||||
integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
|
||||
|
||||
lucide-react@^0.454.0:
|
||||
version "0.454.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.454.0.tgz#a81b9c482018720f07ead0503ae502d94d528444"
|
||||
integrity sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==
|
||||
|
||||
lucide-react@^0.474.0:
|
||||
version "0.474.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.474.0.tgz#9fcaa96250fa2de0b3e2803d4ad744eaea572247"
|
||||
integrity sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==
|
||||
|
||||
lucide-react@^0.479.0:
|
||||
version "0.479.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.479.0.tgz#7321f979a389ec5dd86747b2deb6444cf0922f8d"
|
||||
integrity sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ==
|
||||
|
||||
luxon@~3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20"
|
||||
@ -9611,6 +9624,13 @@ nprogress@^0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
|
||||
integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==
|
||||
|
||||
number-flow@0.5.5:
|
||||
version "0.5.5"
|
||||
resolved "https://registry.yarnpkg.com/number-flow/-/number-flow-0.5.5.tgz#955dc8b98d0e5a3a6367c019c347f577e462a612"
|
||||
integrity sha512-oE+gyA3S0ar8un2dg80TlEi3hjvi/UnTewHl2bu9dGKMxU7nT8VTUdIf1X7NbRLslqyyTyxdSmIAv/QJhaq1pw==
|
||||
dependencies:
|
||||
esm-env "^1.1.4"
|
||||
|
||||
nuqs@^2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/nuqs/-/nuqs-2.4.1.tgz#dfc7ac4eb0f2d3fa55e5b922d02e08612c59cf2f"
|
||||
@ -10512,7 +10532,7 @@ recharts-scale@^0.4.4:
|
||||
dependencies:
|
||||
decimal.js-light "^2.4.1"
|
||||
|
||||
recharts@^2.12.7, recharts@^2.15.1:
|
||||
recharts@^2.15.1:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.1.tgz#0941adf0402528d54f6d81997eb15840c893aa3c"
|
||||
integrity sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==
|
||||
@ -12495,13 +12515,6 @@ vary@^1, vary@~1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||
|
||||
vaul@^0.9.1:
|
||||
version "0.9.9"
|
||||
resolved "https://registry.yarnpkg.com/vaul/-/vaul-0.9.9.tgz#ff075c3cba6193d4859bb6f1b09efcce049cf812"
|
||||
integrity sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==
|
||||
dependencies:
|
||||
"@radix-ui/react-dialog" "^1.1.1"
|
||||
|
||||
vaul@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.2.tgz#c959f8b9dc2ed4f7d99366caee433fbef91f5ba9"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user