mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-15 08:28:03 -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.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "@/lib/hooks/use-theme";
|
||||||
import type { SVGProps } from "react";
|
import type { SVGProps } from "react";
|
||||||
const Github = (props: SVGProps<SVGSVGElement>) => {
|
const Github = (props: SVGProps<SVGSVGElement>) => {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "@/lib/hooks/use-theme";
|
||||||
import type { SVGProps } from "react";
|
import type { SVGProps } from "react";
|
||||||
const Github = (props: SVGProps<SVGSVGElement>) => {
|
const Github = (props: SVGProps<SVGSVGElement>) => {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|||||||
@ -21,10 +21,12 @@
|
|||||||
"@emotion/is-prop-valid": "^1.3.0",
|
"@emotion/is-prop-valid": "^1.3.0",
|
||||||
"@linear/sdk": "^31.0.0",
|
"@linear/sdk": "^31.0.0",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@number-flow/react": "^0.5.7",
|
||||||
"@radix-ui/react-aspect-ratio": "1.1.1",
|
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||||
"@radix-ui/react-avatar": "1.1.1",
|
"@radix-ui/react-avatar": "1.1.1",
|
||||||
"@radix-ui/react-collapsible": "1.1.1",
|
"@radix-ui/react-collapsible": "1.1.1",
|
||||||
"@radix-ui/react-context-menu": "^2.2.6",
|
"@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-hover-card": "1.1.1",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-menubar": "1.1.1",
|
"@radix-ui/react-menubar": "1.1.1",
|
||||||
@ -49,7 +51,7 @@
|
|||||||
"inngest": "^3.21.2",
|
"inngest": "^3.21.2",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"json-beautify": "^1.1.1",
|
"json-beautify": "^1.1.1",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.479.0",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
"minimessage-2-html": "1.6.0",
|
"minimessage-2-html": "1.6.0",
|
||||||
"minimessage-js": "^1.1.3",
|
"minimessage-js": "^1.1.3",
|
||||||
@ -71,6 +73,7 @@
|
|||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-qr-code": "^2.0.15",
|
"react-qr-code": "^2.0.15",
|
||||||
"react-snowfall": "^2.2.0",
|
"react-snowfall": "^2.2.0",
|
||||||
|
"recharts": "^2.15.1",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"sonner": "^1.7.0",
|
"sonner": "^1.7.0",
|
||||||
@ -81,7 +84,8 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tailwindcss-patch": "^4.0.0",
|
"tailwindcss-patch": "^4.0.0",
|
||||||
"turbo": "^2.4.0",
|
"turbo": "^2.4.0",
|
||||||
"unplugin-tailwindcss-mangle": "^3.0.1"
|
"unplugin-tailwindcss-mangle": "^3.0.1",
|
||||||
|
"vaul": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@clerk/themes": "^2.1.19",
|
"@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 {
|
.system-ui-font--font-boundary {
|
||||||
font-family:
|
font-family:
|
||||||
system-ui,
|
system-ui,
|
||||||
|
|||||||
@ -47,7 +47,6 @@ export default function RootLayout({
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<noscript>{children}</noscript>
|
<noscript>{children}</noscript>
|
||||||
<script src="https://unpkg.com/react-scan/dist/auto.global.js" />
|
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export function Footer() {
|
|||||||
Rules
|
Rules
|
||||||
</Link>
|
</Link>
|
||||||
, <Link href="https://minehut.com/terms-of-service">ToS</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>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { useClerk, useUser } from "@clerk/nextjs";
|
import { useClerk, useUser } from "@clerk/nextjs";
|
||||||
import { ArrowDown, GalleryVertical } from "lucide-react";
|
import { ArrowDown, GalleryVertical } from "lucide-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "@/lib/hooks/use-theme";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Gradient } from "stripe-gradient";
|
import { Gradient } from "stripe-gradient";
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "@/lib/hooks/use-theme";
|
||||||
import type { SVGProps } from "react";
|
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">
|
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,
|
DropdownMenuSeparator,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import Github from "@/components/ui/github";
|
import Github from "@/components/ui/github";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { Link } from "@/components/util/link";
|
import { Link } from "@/components/util/link";
|
||||||
import { version } from "@/config/version";
|
import { version } from "@/config/version";
|
||||||
|
import { useSettingsStore } from "@/lib/hooks/use-settings-store";
|
||||||
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
||||||
import { LogIn, LogOut, Settings, Ship, User, UserCog } from "lucide-react";
|
import { LogIn, LogOut, Settings, Ship, User, UserCog } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@ -44,6 +46,7 @@ import { useRouter } from "next/navigation";
|
|||||||
export function MenuDropdown() {
|
export function MenuDropdown() {
|
||||||
const clerk = useClerk();
|
const clerk = useClerk();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const settings = useSettingsStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -105,14 +108,33 @@ export function MenuDropdown() {
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<li className="flex flex-col px-2 py-1 mx-auto my-1 text-xs w-full">
|
<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">
|
<div className="flex flex-row gap-2 w-full items-center">
|
||||||
<div className="flex-1">
|
{settings.get("debug-mode") === "true" ? (
|
||||||
<button
|
<Tooltip>
|
||||||
className="hover:brightness-110 transition-all"
|
<TooltipTrigger>
|
||||||
type="button"
|
<div className="flex-1">
|
||||||
>
|
<button
|
||||||
<Badge variant="blue-subtle">v{version}</Badge>
|
className="hover:brightness-110 transition-all"
|
||||||
</button>
|
type="button"
|
||||||
</div>
|
>
|
||||||
|
<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"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Badge variant="blue-subtle">v{version}</Badge>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Link href="Special:GitHub">
|
<Link href="Special:GitHub">
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
|
|||||||
@ -71,9 +71,9 @@ export function NavBar() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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",
|
"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)
|
pathname !== null && animatedTopbarPages.includes(pathname)
|
||||||
? "[--animation-delay:1000ms] opacity-0 animate-fade-in"
|
? "[--animation-delay:1000ms] opacity-0 animate-fade-in"
|
||||||
: ""
|
: ""
|
||||||
|
|||||||
@ -36,11 +36,16 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { Statistics } from "./statistics";
|
import { Statistics } from "./statistics";
|
||||||
import InfiniteScroll from "react-infinite-scroll-component";
|
import InfiniteScroll from "react-infinite-scroll-component";
|
||||||
import { useInfiniteScrolling } from "@/lib/hooks/use-infinite-scrolling";
|
import { useInfiniteScrolling } from "@/lib/hooks/use-infinite-scrolling";
|
||||||
|
import { useMHSFServer } from "@/lib/hooks/use-mhsf-multiple";
|
||||||
|
|
||||||
export function ServerList() {
|
export function ServerList() {
|
||||||
const { servers, loading, serverCount, playerCount } = useServers();
|
const { servers, loading, serverCount, playerCount } = useServers();
|
||||||
const { itemsLength, fetchMoreData, hasMoreData, data } =
|
const { itemsLength, fetchMoreData, hasMoreData, data } =
|
||||||
useInfiniteScrolling(servers);
|
useInfiniteScrolling(servers);
|
||||||
|
const mhsfServers = useMHSFServer(
|
||||||
|
servers.map((s) => s.staticInfo._id),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
if (loading)
|
if (loading)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export function Statistics({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
(async () => {
|
(async () => {
|
||||||
const fetchRes = await fetch("/api/v0/history/meta-daily-avg");
|
const fetchRes = await fetch("/api/v1/server/minehut/daily-avg");
|
||||||
const fetchJson: {
|
const fetchJson: {
|
||||||
totalServerAverage: number;
|
totalServerAverage: number;
|
||||||
totalPlayerAverage: 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 { miniMessage } from "minimessage-js";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Material } from "@/components/ui/material";
|
||||||
|
|
||||||
export function MOTDRow({ server }: { server: ServerResponse }) {
|
export function MOTDRow({ server }: { server: ServerResponse }) {
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
return (
|
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>
|
<strong className="text-lg">MOTD</strong>
|
||||||
<br />
|
<br />
|
||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
@ -66,6 +67,6 @@ export function MOTDRow({ server }: { server: ServerResponse }) {
|
|||||||
click to copy HTML
|
click to copy HTML
|
||||||
</button>
|
</button>
|
||||||
</small>
|
</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.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MongoClient } from "mongodb";
|
import { type ReactNode, useEffect, useState } from "react";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { ReportingDialog } from "./reporting-dialog";
|
||||||
import { waitUntil } from "@vercel/functions";
|
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||||
|
|
||||||
export default async function handler(
|
export function ReportingProvider({
|
||||||
req: NextApiRequest,
|
children,
|
||||||
res: NextApiResponse,
|
server,
|
||||||
) {
|
}: {
|
||||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
children: ReactNode | ReactNode[];
|
||||||
const db = client.db("mhsf").collection("historical");
|
server: ReturnType<typeof useMHSFServer>;
|
||||||
const server = req.query.server as string;
|
}) {
|
||||||
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const allData = await db.find({ server }).toArray();
|
useEffect(() => {
|
||||||
const data: any[] = [];
|
window.addEventListener("open-report-menu", () => {
|
||||||
|
setOpen(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
allData.forEach((d) => {
|
return (
|
||||||
const result: any = {};
|
<>
|
||||||
scopes.forEach((b) => {
|
<ReportingDialog server={server} open={open} setOpen={setOpen} />{" "}
|
||||||
result[b] = d[b];
|
{children}
|
||||||
});
|
</>
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
@ -31,25 +31,38 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ServerResponse } from "@/lib/types/mh-server";
|
import { ServerResponse } from "@/lib/types/mh-server";
|
||||||
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
||||||
import { Heart, Star } from "lucide-react";
|
import { EllipsisVertical, Flag, Heart, Star } from "lucide-react";
|
||||||
import { useFavoriteStore } from "@/lib/hooks/use-favorite-store";
|
import { useFavoriteStore } from "@/lib/hooks/use-favorite-store";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { 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 clerk = useClerk();
|
||||||
const favoritesStore = useFavoriteStore(server.name);
|
const favoritesStore = useFavoriteStore(mhsfData);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<span className="flex items-center gap-2">
|
||||||
<SignedIn>
|
<SignedIn>
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-2 text-sm"
|
className="flex items-center gap-2 text-sm"
|
||||||
variant={favoritesStore.isFavorite ? "secondary" : "default"}
|
variant={favoritesStore.isFavorite ? "secondary" : "default"}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await favoritesStore.toggleFavorite(server.name);
|
await favoritesStore.toggleFavorite();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}}
|
}}
|
||||||
disabled={loading || favoritesStore.isFavorite === null}
|
disabled={loading || favoritesStore.isFavorite === null}
|
||||||
@ -61,9 +74,10 @@ export function ServerPageButtons({ server }: { server: ServerResponse }) {
|
|||||||
/>
|
/>
|
||||||
Favorite
|
Favorite
|
||||||
{favoritesStore.favoriteNumber !== null && (
|
{favoritesStore.favoriteNumber !== null && (
|
||||||
<code>{favoritesStore.favoriteNumber}</code>
|
<code>
|
||||||
|
<NumberFlow value={favoritesStore.favoriteNumber} />{" "}
|
||||||
|
</code>
|
||||||
)}
|
)}
|
||||||
{loading && <Spinner />}
|
|
||||||
</Button>
|
</Button>
|
||||||
</SignedIn>
|
</SignedIn>
|
||||||
<SignedOut>
|
<SignedOut>
|
||||||
@ -78,6 +92,28 @@ export function ServerPageButtons({ server }: { server: ServerResponse }) {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</SignedOut>
|
</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 { Separator } from "@/components/ui/separator";
|
||||||
import { ServerRows } from "./server-rows";
|
import { ServerRows } from "./server-rows";
|
||||||
import { ServerPageButtons } from "./server-page-buttons";
|
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 (
|
return (
|
||||||
<div className="pt-[150px] xl:px-[100px]">
|
<div
|
||||||
<span className="flex items-center gap-2 w-full">
|
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">
|
<div className="bg-secondary p-4 rounded-lg ml-4">
|
||||||
<IconDisplay server={server} />
|
<IconDisplay server={server} />
|
||||||
</div>
|
</div>
|
||||||
@ -17,7 +49,7 @@ export function ServerMainPage({ server }: { server: ServerResponse }) {
|
|||||||
<div className="flex justify-between w-full">
|
<div className="flex justify-between w-full">
|
||||||
<h1 className="text-2xl font-bold">{server.name}</h1>
|
<h1 className="text-2xl font-bold">{server.name}</h1>
|
||||||
<span>
|
<span>
|
||||||
<ServerPageButtons server={server} />
|
<ServerPageButtons server={server} mhsfData={mhsfData} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="flex items-center gap-2 flex-wrap">
|
<span className="flex items-center gap-2 flex-wrap">
|
||||||
@ -26,7 +58,7 @@ export function ServerMainPage({ server }: { server: ServerResponse }) {
|
|||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
<Separator className="my-6" />
|
<Separator className="my-6" />
|
||||||
<ServerRows server={server} />
|
<ServerRows server={server} mhsfData={mhsfData} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,19 +2,20 @@
|
|||||||
import { Placeholder } from "@/components/ui/placeholder";
|
import { Placeholder } from "@/components/ui/placeholder";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { useServer } from "@/lib/hooks/use-server";
|
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 { X } from "lucide-react";
|
||||||
import { ServerMainPage } from "./server-page";
|
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 }) {
|
export function ServerProvider({ serverId }: { serverId: string }) {
|
||||||
const { server, error, loading } = useServer({ id: serverId });
|
const { server, error, loading } = useServer({ id: serverId });
|
||||||
|
const settings = useSettingsStore();
|
||||||
if (loading)
|
const mhsf = useMHSFServer(serverId);
|
||||||
return (
|
|
||||||
<div className="absolute top-[50%] left-[50%]">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error !== null)
|
if (error !== null)
|
||||||
return (
|
return (
|
||||||
@ -33,8 +34,48 @@ export function ServerProvider({ serverId }: { serverId: string }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-10">
|
<DebugProvider
|
||||||
<ServerMainPage server={server as OnlineServer} />
|
debugOptions={{
|
||||||
</div>
|
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 useClipboard from "@/lib/useClipboard";
|
||||||
import { MOTDRow } from "./motd/motd-row";
|
import { MOTDRow } from "./motd/motd-row";
|
||||||
import { StatisticsMainRow } from "./stats/stats-main-row";
|
import { StatisticsMainRow } from "./stats/stats-main-row";
|
||||||
|
import type { useMHSFServer } from "@/lib/hooks/use-mhsf-server";
|
||||||
|
|
||||||
export function ServerRows({ server }: { server: ServerResponse }) {
|
export function ServerRows({ server, mhsfData }: { server: ServerResponse, mhsfData: ReturnType<typeof useMHSFServer> }) {
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="lg:grid lg:grid-cols-3 w-full gap-3">
|
<span className="lg:grid lg:grid-cols-3 w-full gap-3">
|
||||||
<MOTDRow server={server} />
|
<MOTDRow server={server} />
|
||||||
<StatisticsMainRow server={server} />
|
<StatisticsMainRow server={server} mhsfData={mhsfData} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,170 @@
|
|||||||
|
"use client";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import type { ServerResponse } from "@/lib/types/mh-server";
|
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 (
|
return (
|
||||||
<span className="border rounded-xl p-4 relative col-span-2 min-h-[250px] max-h-[250px]">
|
<Material
|
||||||
<strong className="text-lg">Statistics</strong>
|
className="relative col-span-2 h-[250px] max-lg:mt-3"
|
||||||
<br />
|
padding="none"
|
||||||
<Separator className="my-2" />
|
>
|
||||||
</span>
|
<div className="p-4">
|
||||||
|
<span className="flex gap-4 mb-2">
|
||||||
|
<strong className="text-lg">Statistics</strong>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"text-sm cursor-pointer hover:bg-slate-100 dark:hover:bg-zinc-700/30 transition-all duration-75 disabled:opacity-50 disabled:pointer-events-none",
|
||||||
|
"rounded-xl px-2 flex items-center gap-2",
|
||||||
|
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 settingsStore = useSettingsStore();
|
||||||
const [fontFamily, setFontFamily] = useState("inter");
|
const [fontFamily, setFontFamily] = useState("inter");
|
||||||
const [mcFont, setMcFont] = useState(true);
|
const [mcFont, setMcFont] = useState(true);
|
||||||
|
const [debugMode, setDebugMode] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
setFontFamily((settingsStore.get("font-family") ?? "inter") as string);
|
||||||
setMcFont((settingsStore.get("mc-font") === "true") as boolean);
|
setMcFont((settingsStore.get("mc-font") === "true") as boolean);
|
||||||
|
setDebugMode((settingsStore.get("debug-mode") === "true") as boolean);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -119,6 +121,22 @@ export function BrowserSettings() {
|
|||||||
</Select>
|
</Select>
|
||||||
</SettingContent>
|
</SettingContent>
|
||||||
</Setting>
|
</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>
|
</Material>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,45 +28,39 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MongoClient } from "mongodb";
|
import { Material } from "@/components/ui/material";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { Setting, SettingContent, SettingDescription, SettingMeta, SettingTitle } from "./setting";
|
||||||
import { waitUntil } from "@vercel/functions";
|
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(
|
export function DebugSettings() {
|
||||||
req: NextApiRequest,
|
const [randomText, setRandomText] = useState("")
|
||||||
res: NextApiResponse,
|
|
||||||
) {
|
|
||||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
|
||||||
const db = client.db("mhsf").collection("history");
|
|
||||||
const mh = client.db("mhsf").collection("mh");
|
|
||||||
const server = req.query.server as string;
|
|
||||||
|
|
||||||
const allData = await db.find({ server }).toArray();
|
return (
|
||||||
const data: any[] = [];
|
<Material className="mt-6 grid gap-4">
|
||||||
if (server === "peww") console.log(allData.slice(-30));
|
<h2 className="text-xl font-semibold text-inherit">Debug Settings</h2>
|
||||||
|
<Setting>
|
||||||
for (const d of allData.slice(-30)) {
|
<SettingContent>
|
||||||
const dateOfEntry = new Date(d.date);
|
<SettingMeta>
|
||||||
const result = await mh
|
<SettingTitle>
|
||||||
.find({
|
Generate loading text
|
||||||
date: {
|
</SettingTitle>
|
||||||
$gte: new Date(dateOfEntry.getTime() - 1000 * 60 * 60),
|
<SettingDescription>
|
||||||
$lt: new Date(dateOfEntry.getTime() + 1000 * 60 * 60),
|
Generate a random loading text
|
||||||
},
|
</SettingDescription>
|
||||||
})
|
</SettingMeta>
|
||||||
.toArray();
|
<div className="block pb-6">
|
||||||
|
<Button onClick={() => {
|
||||||
if (result.length > 0) {
|
setRandomText(loadingList[Math.floor(Math.random() * loadingList.length)])
|
||||||
const resultedData = result[0];
|
}}>
|
||||||
data.push({
|
Generate
|
||||||
relativePrecentage: d.player_count / resultedData.total_players,
|
</Button>
|
||||||
date: dateOfEntry,
|
<AnimatedText className="font-bold" text={randomText + "..."}/>
|
||||||
});
|
</div>
|
||||||
}
|
</SettingContent>
|
||||||
}
|
</Setting>
|
||||||
|
</Material>
|
||||||
// Close the database, but don't close this
|
);
|
||||||
// serverless instance until it happens
|
|
||||||
waitUntil(client.close());
|
|
||||||
res.send({ data });
|
|
||||||
}
|
}
|
||||||
@ -37,10 +37,22 @@ import { cn } from "@/lib/utils";
|
|||||||
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
import { SignedIn, SignedOut, useClerk } from "@clerk/nextjs";
|
||||||
import { ExternalLink, Globe, TabletSmartphone } from "lucide-react";
|
import { ExternalLink, Globe, TabletSmartphone } from "lucide-react";
|
||||||
import { BrowserSettings } from "./browser-settings";
|
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() {
|
export function Settings() {
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const [debugEnabled, setDebugEnabled] = useState(false);
|
||||||
const clerk = useClerk();
|
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 (
|
return (
|
||||||
<main className="lg:px-[10rem] px-4">
|
<main className="lg:px-[10rem] px-4">
|
||||||
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
|
||||||
@ -62,6 +74,15 @@ export function Settings() {
|
|||||||
<TabletSmartphone size={16} />
|
<TabletSmartphone size={16} />
|
||||||
User stored settings
|
User stored settings
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
{debugEnabled && (
|
||||||
|
<TabsTrigger
|
||||||
|
value="debug-settings"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
Debug settings
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
|
||||||
<SignedIn>
|
<SignedIn>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="account-settings"
|
value="account-settings"
|
||||||
@ -75,6 +96,10 @@ export function Settings() {
|
|||||||
<TabsContent value="browser-settings">
|
<TabsContent value="browser-settings">
|
||||||
<BrowserSettings />
|
<BrowserSettings />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="debug-settings">
|
||||||
|
<DebugSettings />
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="user-settings">
|
<TabsContent value="user-settings">
|
||||||
<SignedOut>
|
<SignedOut>
|
||||||
<Material className="mt-6 grid gap-4 py-6">
|
<Material className="mt-6 grid gap-4 py-6">
|
||||||
|
|||||||
@ -48,34 +48,36 @@ function Alert({
|
|||||||
<Material
|
<Material
|
||||||
padding="sm"
|
padding="sm"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-row space-x-2 items-center",
|
"flex flex-row items-center",
|
||||||
variant === "error"
|
variant === "error"
|
||||||
? "bg-[#fdeded_!important] dark:bg-[#160b0b_!important]"
|
? "bg-[#fdeded_!important] dark:bg-[#160b0b_!important]"
|
||||||
: variant === "warning"
|
: variant === "warning"
|
||||||
? "bg-[#fef4e5_!important] dark:bg-[#191209_!important]"
|
? "bg-[#fef4e5_!important] dark:bg-[#191209_!important]"
|
||||||
: variant === "info"
|
: variant === "info"
|
||||||
? "bg-[#e5f6fd_!important] dark:bg-[#091418_!important]"
|
? "bg-[#e5f6fd_!important] dark:bg-[#091418_!important]"
|
||||||
: "",
|
: "",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{icon ? (
|
{icon ? (
|
||||||
icon
|
icon
|
||||||
) : (
|
) : (
|
||||||
<CircleAlert
|
<div className="flex items-center justify-center h-full">
|
||||||
size={18}
|
<CircleAlert
|
||||||
className={
|
size={16}
|
||||||
variant === "error"
|
className={
|
||||||
? "text-[#d76463] dark:text-[#df2317]"
|
variant === "error"
|
||||||
: variant === "warning"
|
? "text-[#d76463] dark:text-[#df2317]"
|
||||||
? "text-[#eea065] dark:text-[#e3920a]"
|
: variant === "warning"
|
||||||
: variant === "info"
|
? "text-[#eea065] dark:text-[#e3920a]"
|
||||||
? "text-[#67b1d5] dark:text-[#1a97e3]"
|
: variant === "info"
|
||||||
: ""
|
? "text-[#67b1d5] dark:text-[#1a97e3]"
|
||||||
}
|
: ""
|
||||||
/>
|
}
|
||||||
)}{" "}
|
/>
|
||||||
<p>{children}</p>
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="flex-1">{children}</p>
|
||||||
</Material>
|
</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}>
|
<DialogPrimitive.Overlay ref={ref} asChild {...props}>
|
||||||
<motion.div
|
<motion.div
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
@ -95,12 +95,12 @@ const DialogContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
className={cn(
|
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",
|
"w-full border border-slate-200 border-b-slate-300",
|
||||||
"dark:border-zinc-900 dark:border-t-zinc-800 dark:border-b-zinc-900",
|
"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",
|
"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",
|
"p-5 flex flex-col gap-2 dark:bg-zinc-950 rounded-xl",
|
||||||
"bg-white fixed",
|
"bg-white fixed z-9",
|
||||||
className
|
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.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "@/lib/hooks/use-theme";
|
||||||
import type { SVGProps } from "react";
|
import type { SVGProps } from "react";
|
||||||
const Github = (props: SVGProps<SVGSVGElement>) => {
|
const Github = (props: SVGProps<SVGSVGElement>) => {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const TabsList = React.forwardRef<
|
|||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const TooltipContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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",
|
"border dark:border-slate-200 border-zinc-800 dark:border-b-slate-300 border-t-zinc-700",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
import { ClerkProvider as ImportedClerkProvider } from "@clerk/nextjs";
|
import { ClerkProvider as ImportedClerkProvider } from "@clerk/nextjs";
|
||||||
import { dark } from "@clerk/themes";
|
import { dark } from "@clerk/themes";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "@/lib/hooks/use-theme";
|
||||||
import { MultisessionAppSupport } from "@clerk/nextjs/internal";
|
import { MultisessionAppSupport } from "@clerk/nextjs/internal";
|
||||||
|
|
||||||
export const ClerkProvider = ({ children }: { children: React.ReactNode }) => {
|
export const ClerkProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
import { Moon, Sun } from "lucide-react";
|
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 { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -33,15 +33,30 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ThemeProvider as NextThemeProvider } from "next-themes";
|
import { ThemeProvider as NextThemeProvider } from "next-themes";
|
||||||
import type { ThemeProviderProps } from "next-themes";
|
import type { ThemeProviderProps } from "next-themes";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
const [mounted, setMounted] = React.useState(false);
|
const [mounted, setMounted] = React.useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [forcedDark, setForcedDark] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
|
||||||
|
window.addEventListener("force-dark-mode", () => {
|
||||||
|
setForcedDark(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setForcedDark(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
if (!mounted) return null;
|
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 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 type { OnlineServer, ServerResponse } from "@/lib/types/mh-server";
|
||||||
import { Cake, ServerCog } from "lucide-react";
|
import { Cake, ServerCog } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
@ -56,6 +56,7 @@ export const allTags: Array<{
|
|||||||
condition?: (server: {
|
condition?: (server: {
|
||||||
online?: OnlineServer;
|
online?: OnlineServer;
|
||||||
server?: ServerResponse;
|
server?: ServerResponse;
|
||||||
|
mhsfData?: MHSFData;
|
||||||
}) => Promise<boolean>;
|
}) => Promise<boolean>;
|
||||||
tooltipDesc: string;
|
tooltipDesc: string;
|
||||||
htmlDocs: string;
|
htmlDocs: string;
|
||||||
@ -200,12 +201,9 @@ export const allTags: Array<{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: async (s) => "Favorited",
|
name: async (s) => "Favorited",
|
||||||
condition: async (s) => {
|
condition: async (s) =>
|
||||||
const favorited = await isFavorited(
|
(s.mhsfData ?? { favoriteData: { favoritedByAccount: false } })
|
||||||
(s.online ?? s.server ?? { name: "" }).name
|
.favoriteData.favoritedByAccount ?? false,
|
||||||
);
|
|
||||||
return favorited;
|
|
||||||
},
|
|
||||||
tooltipDesc: "This tag represents that you favorited this server.",
|
tooltipDesc: "This tag represents that you favorited this server.",
|
||||||
docsName: "Favorited",
|
docsName: "Favorited",
|
||||||
htmlDocs:
|
htmlDocs:
|
||||||
|
|||||||
@ -168,7 +168,7 @@ export async function isFavorited(server: string): Promise<boolean> {
|
|||||||
export async function getAccountFavorites(): Promise<Array<string>> {
|
export async function getAccountFavorites(): Promise<Array<string>> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
connector(`/favorites/account-favorites`, { version: 0 }),
|
connector(`/user/favorites`, { version: 1 }),
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -28,10 +28,10 @@
|
|||||||
* OTHER DEALINGS IN THE SOFTWARE.
|
* OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "@/lib/hooks/use-theme";
|
||||||
|
|
||||||
export function useDepTheme() {
|
export function useDepTheme() {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
return resolvedTheme === "dark" ? "black" : "white";
|
return resolvedTheme === "dark" ? "black" : "white";
|
||||||
}
|
}
|
||||||
|
|||||||
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 { useClerk } from "@clerk/nextjs";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import { getAccountFavorites } from "../api";
|
||||||
favoriteServer,
|
import { useMHSFServer } from "./use-mhsf-server";
|
||||||
getAccountFavorites,
|
|
||||||
getCommunityServerFavorites,
|
|
||||||
isFavorited,
|
|
||||||
} from "../api";
|
|
||||||
|
|
||||||
export function useFavoriteStore(server?: string) {
|
export function useFavoriteStore(server?: ReturnType<typeof useMHSFServer>) {
|
||||||
const [favorites, setFavorites] = useState<string[] | null>(null);
|
const [favorites, setFavorites] = useState<string[] | null>(null);
|
||||||
const [isFavorite, setIsFavorite] = useState<boolean | null>(null);
|
const [isFavorite, setIsFavorite] = useState<boolean | null>(null);
|
||||||
const [favoriteNumber, setFavoriteNumber] = useState<number | null>(null);
|
const [favoriteNumber, setFavoriteNumber] = useState<number | null>(null);
|
||||||
@ -17,12 +13,20 @@ export function useFavoriteStore(server?: string) {
|
|||||||
if (isSignedIn) {
|
if (isSignedIn) {
|
||||||
getAccountFavorites().then((favorites) => setFavorites(favorites));
|
getAccountFavorites().then((favorites) => setFavorites(favorites));
|
||||||
}
|
}
|
||||||
if (server) {
|
if (
|
||||||
getCommunityServerFavorites(server).then((number) =>
|
server !== null &&
|
||||||
setFavoriteNumber(number)
|
server?.loading === false &&
|
||||||
);
|
server?.server !== null
|
||||||
|
) {
|
||||||
|
setFavoriteNumber(server.server.favoriteData.favoriteNumber);
|
||||||
if (isFavorite === null) {
|
if (isFavorite === null) {
|
||||||
isFavorited(server).then((isFavorite) => setIsFavorite(isFavorite));
|
server
|
||||||
|
.reloadServerData()
|
||||||
|
.then(() =>
|
||||||
|
setIsFavorite(
|
||||||
|
server.server?.favoriteData.favoritedByAccount ?? false
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isSignedIn, server, isFavorite]);
|
}, [isSignedIn, server, isFavorite]);
|
||||||
@ -38,12 +42,24 @@ export function useFavoriteStore(server?: string) {
|
|||||||
loadingNumber: favoriteNumber === null,
|
loadingNumber: favoriteNumber === null,
|
||||||
favoriteNumber,
|
favoriteNumber,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
toggleFavorite: async (server: string) => {
|
toggleFavorite: async () => {
|
||||||
if (isFavorite === null) throw new Error("Hold up lemme load rq");
|
if (isFavorite === null) throw new Error("Hold up lemme load rq");
|
||||||
if (favoriteNumber === null) throw new Error("Nah");
|
if (favoriteNumber === null) throw new Error("Nah");
|
||||||
await favoriteServer(server);
|
const favoriteSync = await server?.favoriteServer();
|
||||||
|
|
||||||
// Resolve remote differences
|
// 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) {
|
if (isFavorite === true) {
|
||||||
setIsFavorite(false);
|
setIsFavorite(false);
|
||||||
setFavoriteNumber(favoriteNumber - 1);
|
setFavoriteNumber(favoriteNumber - 1);
|
||||||
@ -53,7 +69,5 @@ export function useFavoriteStore(server?: string) {
|
|||||||
setFavoriteNumber(favoriteNumber + 1);
|
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 { useState } from "react";
|
||||||
import type { OnlineServer } from "../types/mh-server";
|
import type { OnlineServer, ServerResponse } from "../types/mh-server";
|
||||||
import { useEffectOnce } from "../useEffectOnce";
|
import { useEffectOnce } from "../useEffectOnce";
|
||||||
|
|
||||||
export function useServer(serverSpecifier: { id?: string; name?: string }) {
|
export function useServer(serverSpecifier: { id?: string; name?: string }) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [server, setServer] = useState<OnlineServer | null>(null);
|
const [server, setServer] = useState<ServerResponse | null>(null);
|
||||||
|
|
||||||
useEffectOnce(() => {
|
useEffectOnce(() => {
|
||||||
try {
|
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 type { MHSFData } from "@/lib/types/data";
|
||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from "mongodb";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { RouteParams } from "./get/[server]";
|
||||||
|
|
||||||
// Type definitions for query parameters
|
// Type definitions for query parameters
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
@ -43,6 +44,7 @@ type QueryParams = {
|
|||||||
maxAchievementEntries?: string | string[];
|
maxAchievementEntries?: string | string[];
|
||||||
achievementTimespanStart?: string | string[];
|
achievementTimespanStart?: string | string[];
|
||||||
achievementTimespanEnd?: string | string[];
|
achievementTimespanEnd?: string | string[];
|
||||||
|
noStatistics?: string | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Type for customization data
|
// Type for customization data
|
||||||
@ -91,8 +93,10 @@ export default async function handler(
|
|||||||
return res.status(400).json({ servers: {} });
|
return res.status(400).json({ servers: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let serverList = servers;
|
||||||
|
|
||||||
// Limit the number of servers to prevent abuse (max 25 servers per request)
|
// 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
|
// Extract query parameters
|
||||||
const queryOptions: QueryParams = {
|
const queryOptions: QueryParams = {
|
||||||
@ -105,6 +109,7 @@ export default async function handler(
|
|||||||
maxAchievementEntries: req.query.maxAchievementEntries,
|
maxAchievementEntries: req.query.maxAchievementEntries,
|
||||||
achievementTimespanStart: req.query.achievementTimespanStart,
|
achievementTimespanStart: req.query.achievementTimespanStart,
|
||||||
achievementTimespanEnd: req.query.achievementTimespanEnd,
|
achievementTimespanEnd: req.query.achievementTimespanEnd,
|
||||||
|
noStatistics: req.query.noStatistics,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine which data to fetch based on options
|
// 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(
|
promises.push(
|
||||||
findPlayerData(serverData.name, db, queryOptions).then(
|
findPlayerData(serverData.name, db, queryOptions).then(
|
||||||
(data: PlayerData) => {
|
(data: PlayerData) => {
|
||||||
@ -168,7 +173,10 @@ export default async function handler(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fetchOptions.achievements) {
|
if (
|
||||||
|
fetchOptions.achievements &&
|
||||||
|
queryOptions.noStatistics !== "true"
|
||||||
|
) {
|
||||||
promises.push(
|
promises.push(
|
||||||
findAchievements(serverData.name, db, queryOptions).then(
|
findAchievements(serverData.name, db, queryOptions).then(
|
||||||
(data: AchievementsData) => {
|
(data: AchievementsData) => {
|
||||||
@ -182,7 +190,7 @@ export default async function handler(
|
|||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
// Create default values for any missing data
|
// Create default values for any missing data
|
||||||
const serverResult: MHSFData = {
|
const serverResult: MHSFData & RouteParams = {
|
||||||
favoriteData: promiseResults.favoriteData || {
|
favoriteData: promiseResults.favoriteData || {
|
||||||
favoritedByAccount: null,
|
favoritedByAccount: null,
|
||||||
favoriteNumber: 0,
|
favoriteNumber: 0,
|
||||||
@ -205,6 +213,18 @@ export default async function handler(
|
|||||||
historically: [],
|
historically: [],
|
||||||
currently: [],
|
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;
|
result[server] = serverResult;
|
||||||
@ -287,7 +307,9 @@ async function findFavoriteData(
|
|||||||
const [userFavorites, metaData, historyData] = await Promise.all([
|
const [userFavorites, metaData, historyData] = await Promise.all([
|
||||||
userId ? db.collection("favorites").findOne({ user: userId }) : null,
|
userId ? db.collection("favorites").findOne({ user: userId }) : null,
|
||||||
db.collection("meta").findOne({ server: serverName }),
|
db.collection("meta").findOne({ server: serverName }),
|
||||||
fetchHistoryData(db, serverName, query),
|
query.noStatistics !== "true"
|
||||||
|
? fetchHistoryData(db, serverName, query)
|
||||||
|
: [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Process user favorites
|
// 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 { MongoClient } from "mongodb";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
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(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse<{ server: MHSFData | null }>
|
res: NextApiResponse<{ server: (MHSFData & RouteParams) | null }>
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
server,
|
server,
|
||||||
@ -81,13 +96,24 @@ export default async function handler(
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Ignore the linter error as requested
|
|
||||||
res.send({
|
res.send({
|
||||||
server: {
|
server: {
|
||||||
favoriteData,
|
favoriteData,
|
||||||
customizationData,
|
customizationData,
|
||||||
playerData,
|
playerData,
|
||||||
achievements,
|
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) {
|
} 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,53 +31,53 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getAuth } from "@clerk/nextjs/server";
|
import { getAuth } from "@clerk/nextjs/server";
|
||||||
import { MongoClient } from "mongodb";
|
import { MongoClient } from "mongodb";
|
||||||
import { inngest } from "../inngest";
|
import { inngest } from "@/pages/api/inngest";
|
||||||
import { waitUntil } from "@vercel/functions";
|
import { waitUntil } from "@vercel/functions";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
const { userId } = getAuth(req);
|
const { userId } = getAuth(req);
|
||||||
const { server } = req.body;
|
const { server } = req.query;
|
||||||
|
|
||||||
if (server == null) {
|
if (server == null) {
|
||||||
res.status(400).send({ message: "Couldn't find data" });
|
res.status(400).send({ message: "Couldn't find data" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { reason } = req.body;
|
const { reason } = req.body;
|
||||||
|
|
||||||
if (reason == null) {
|
if (reason == null) {
|
||||||
res.status(400).send({ message: "Couldn't find data" });
|
res.status(400).send({ message: "Couldn't find data" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
const client = new MongoClient(process.env.MONGO_DB as string);
|
||||||
await client.connect();
|
await client.connect();
|
||||||
|
|
||||||
const db = client.db("mhsf");
|
const db = client.db("mhsf");
|
||||||
const collection = db.collection("reports");
|
const collection = db.collection("reports");
|
||||||
const entry = await collection.insertOne({
|
const entry = await collection.insertOne({
|
||||||
server: server,
|
server: server,
|
||||||
reason: reason,
|
reason: reason,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
});
|
});
|
||||||
// Don't wait for this to finish, just continue anyway
|
// Don't wait for this to finish, just continue anyway
|
||||||
inngest.send({
|
inngest.send({
|
||||||
name: "report-server",
|
name: "report-server",
|
||||||
data: {
|
data: {
|
||||||
_id: entry.insertedId.toString(),
|
_id: entry.insertedId.toString(),
|
||||||
server,
|
server,
|
||||||
reason,
|
reason,
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close the database, but don't close this
|
// Close the database, but don't close this
|
||||||
// serverless instance until it happens
|
// serverless instance until it happens
|
||||||
waitUntil(client.close());
|
waitUntil(client.close());
|
||||||
res.send({ msg: "Successfully reported server!" });
|
res.send({ msg: "Successfully reported server!" });
|
||||||
}
|
}
|
||||||
@ -34,28 +34,28 @@ import { getAuth } from "@clerk/nextjs/server";
|
|||||||
import { waitUntil } from "@vercel/functions";
|
import { waitUntil } from "@vercel/functions";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
const { userId } = getAuth(req);
|
const { userId } = getAuth(req);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
const client = new MongoClient(process.env.MONGO_DB as string);
|
const client = new MongoClient(process.env.MONGO_DB as string);
|
||||||
await client.connect();
|
await client.connect();
|
||||||
|
|
||||||
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
|
||||||
const collection = db.collection("favorites");
|
const collection = db.collection("favorites");
|
||||||
const find = await collection.find({ user: userId }).toArray();
|
const find = await collection.find({ user: userId }).toArray();
|
||||||
|
|
||||||
// Close the database, but don't close this
|
// Close the database, but don't close this
|
||||||
// serverless instance until it happens
|
// serverless instance until it happens
|
||||||
waitUntil(client.close());
|
waitUntil(client.close());
|
||||||
|
|
||||||
if (find.length == 0) {
|
if (find.length == 0) {
|
||||||
res.send({ result: [] });
|
res.send({ favorites: [] });
|
||||||
} else {
|
} 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"
|
resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e"
|
||||||
integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==
|
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":
|
"@opentelemetry/api-logs@0.39.1":
|
||||||
version "0.39.1"
|
version "0.39.1"
|
||||||
resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.39.1.tgz#3ea1e9dda11c35f993cb60dc5e52780b8175e702"
|
resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.39.1.tgz#3ea1e9dda11c35f993cb60dc5e52780b8175e702"
|
||||||
@ -2181,7 +2189,7 @@
|
|||||||
aria-hidden "^1.1.1"
|
aria-hidden "^1.1.1"
|
||||||
react-remove-scroll "2.5.5"
|
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"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz#65b4465e99ad900f28a98eed9a94bb21ec644bf7"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz#65b4465e99ad900f28a98eed9a94bb21ec644bf7"
|
||||||
integrity sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==
|
integrity sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==
|
||||||
@ -5919,6 +5927,11 @@ eslint@^9:
|
|||||||
natural-compare "^1.4.0"
|
natural-compare "^1.4.0"
|
||||||
optionator "^0.9.3"
|
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:
|
espree@^10.0.1, espree@^10.3.0:
|
||||||
version "10.3.0"
|
version "10.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a"
|
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"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
|
||||||
integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
|
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:
|
lucide-react@^0.474.0:
|
||||||
version "0.474.0"
|
version "0.474.0"
|
||||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.474.0.tgz#9fcaa96250fa2de0b3e2803d4ad744eaea572247"
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.474.0.tgz#9fcaa96250fa2de0b3e2803d4ad744eaea572247"
|
||||||
integrity sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==
|
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:
|
luxon@~3.5.0:
|
||||||
version "3.5.0"
|
version "3.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20"
|
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"
|
resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
|
||||||
integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==
|
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:
|
nuqs@^2.4.1:
|
||||||
version "2.4.1"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/nuqs/-/nuqs-2.4.1.tgz#dfc7ac4eb0f2d3fa55e5b922d02e08612c59cf2f"
|
resolved "https://registry.yarnpkg.com/nuqs/-/nuqs-2.4.1.tgz#dfc7ac4eb0f2d3fa55e5b922d02e08612c59cf2f"
|
||||||
@ -10512,7 +10532,7 @@ recharts-scale@^0.4.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
decimal.js-light "^2.4.1"
|
decimal.js-light "^2.4.1"
|
||||||
|
|
||||||
recharts@^2.12.7, recharts@^2.15.1:
|
recharts@^2.15.1:
|
||||||
version "2.15.1"
|
version "2.15.1"
|
||||||
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.1.tgz#0941adf0402528d54f6d81997eb15840c893aa3c"
|
resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.1.tgz#0941adf0402528d54f6d81997eb15840c893aa3c"
|
||||||
integrity sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==
|
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"
|
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
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:
|
vaul@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.2.tgz#c959f8b9dc2ed4f7d99366caee433fbef91f5ba9"
|
resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.2.tgz#c959f8b9dc2ed4f7d99366caee433fbef91f5ba9"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user