Compare commits

...

5 Commits

Author SHA1 Message Date
dependabot[bot]
320f0aff9e
Merge d08eb0f5e70776d1d1c89f7b5c6c7c983feef9b0 into 2a29c8935ac9bb19701aa6922517723633aae6e7 2025-01-03 20:15:54 +00:00
dependabot[bot]
d08eb0f5e7
build(deps-dev): bump @unocss/preset-uno from 0.61.5 to 0.65.3
Dependabot couldn't find the original pull request head commit, cef83f9f6d318b7c878274588b219fe1bd84c096.
2025-01-03 20:15:52 +00:00
dvelo
2a29c8935a
Merge pull request #60 from DeveloLongScript/dev
feat: new affiliate
2025-01-03 14:13:26 -06:00
dvelo
270f6efaa6 fix: issue 2025-01-03 13:40:13 -06:00
dvelo
fcb4221fca feat: new affiliate 2025-01-03 13:26:25 -06:00
45 changed files with 3532 additions and 1844 deletions

@ -42,6 +42,7 @@ We use [Atlas](https://www.mongodb.com/atlas) to host our MongoDB database, but
```dotenv
MONGO_DB="mongodb+srv://..."
```
You can also set `CUSTOM_MONGO_DB` to a database name that will apply to all operations except statistical operations.
## Smaller things (for production-ready servers)

@ -11,7 +11,7 @@ The tech stack of MHSF is relatively modern to ensure MHSF keeps up with standar
- **shadcn/ui** is the UI framework used to keep the whole website consistent.
- **Contentlayer** manages all the pages used for documentation
- **TailwindCSS** makes MHSF use (mostly) no CSS for better efficency
- **react-hot-toast** provides the Toast used for MHSF
- **Sonner** provides the Toast used for MHSF
## Back-end
- **Inngest** runs periodic tasks

@ -15,3 +15,7 @@ If they match, you should see a button named Click to own. Press that button, an
## I can't link my server, because my server doesn't have a author
Your server must have an author in-order to be automagically linked, and if it doesn't have an author, that means you will have to manually link your server. To do that, make an issue on GitHub, showing that your server has no author, but needs to be linked. Show proof that you own the server, along with your account username, and your account will own the server you need.
## There is an error while linking my server!
This most likely is because the Minehut API is blocking the server-side request to verify your the owner of that server, or your server [has no author](#i-cant-link-my-server-because-my-server-doesnt-have-a-author).
Try again in 30 minute intervals, or just make an issue on GitHub to link your server.

@ -33,6 +33,7 @@
"@unocss/postcss": "^0.61.5",
"@unocss/transformer-directives": "^0.61.5",
"@unocss/webpack": "^0.61.5",
"ag-grid-react": "^33.0.3",
"contentlayer": "^0.3.4",
"cron": "^3.1.7",
"discord.js": "^14.15.3",
@ -93,7 +94,7 @@
"@types/react-dom": "^18",
"@types/react-twemoji": "^0.4.3",
"@unocss/eslint-config": "^0.61.5",
"@unocss/preset-uno": "^0.61.5",
"@unocss/preset-uno": "^0.65.3",
"@unocss/transformer-compile-class": "^0.61.5",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",

@ -0,0 +1,51 @@
/*
* 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) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { useServerExists } from "@/lib/hooks/use-server-exists";
import { notFound } from "next/navigation";
export default function ServerLayout({
params,
children,
}: {
params: { server: string };
children: React.ReactNode;
}) {
const { serverExists, loading } = useServerExists(params.server);
if (loading) return <LoadingSpinner />;
if (!serverExists) notFound();
return children;
}

@ -30,12 +30,14 @@
import Banner from "@/components/Banner";
import ColorProvider from "@/components/ColorProvider";
import { NewChart } from "@/components/NewChart";
import { NewChart } from "@/components/charts/NewChart";
import ServerView from "@/components/ServerView";
import TabServer from "@/components/misc/TabServer";
import { Separator } from "@/components/ui/separator";
import type { Metadata, ResolvingMetadata } from "next";
import StickyTopbar from "@/components/misc/StickyTopbar";
import { RelativeChart } from "@/components/charts/RelativeChart";
import { MonthlyChart } from "@/components/charts/MonthlyChart";
import { DailyChart } from "@/components/charts/DailyChart";
type Props = {
params: { server: string };
@ -101,17 +103,23 @@ export default function ServerPage({ params }: { params: { server: string } }) {
<ColorProvider server={params.server}>
<div className={"pt-[300px] xl:px-[100px]"}>
<Banner server={params.server} />
<div className="pt-8 z-10 relative">
<div className="pt-8 z-1 relative">
<ServerView server={params.server} />
</div>
<StickyTopbar scrollElevation={100} className="pt-4">
<TabServer server={params.server} tabDef="statistics" />
</StickyTopbar>
<Separator />
<br />
<div className="p-4 gap-4">
<div className="p-4 gap-4 relative z-1">
<NewChart server={params.server} />
<br />
<RelativeChart server={params.server} />
<br />
<div className="grid grid-cols-2 gap-4">
<MonthlyChart server={params.server} />
<DailyChart server={params.server} />
</div>
</div>
</div>
</ColorProvider>

@ -87,6 +87,7 @@ export default function AfterServerView({ server }: { server: string }) {
getCommunityServerFavorites(server).then((c) => {
mhsf.setFavorites(c);
});
setView(description !== "" || discord !== "" ? "desc" : "extra");
}
fetch("https://api.minehut.com/server/" + server + "?byName=true").then(
(c) => c.json().then((n) => setServerObject(n.server))
@ -96,7 +97,7 @@ export default function AfterServerView({ server }: { server: string }) {
});
setLoading(false);
});
}, []);
}, [description, discord]);
if (loading) return <></>;
return (
@ -216,20 +217,13 @@ export default function AfterServerView({ server }: { server: string }) {
</Card>
)}
{discord != "" && view == "desc" && (
<Card>
<CardHeader>
<CardTitle>Discord Server</CardTitle>
<CardDescription className="p-4 prose dark:prose-invert">
<iframe
src={`https://discord.com/widget?id=${discord}&theme=${resolvedTheme}`}
height="500"
allowTransparency={true}
className="rounded-lg max-sm:w-[100px] max-md:w-[250px]"
className="rounded-lg max-md:w-full"
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"
/>
</CardDescription>
</CardHeader>
</Card>
)}{" "}
{view == "achievements" && (
<div className="col-span-4">
@ -452,7 +446,7 @@ export default function AfterServerView({ server }: { server: string }) {
</tr>
<tr>
<th className="border p-2">Server ID</th>
<td className="border p-2">
<td className="border p-2 break-all">
{serverObject?._id == undefined ? (
"? (unknown)"
) : (

@ -305,6 +305,41 @@ export function BrandingGenericIcon(props: SVGProps<SVGSVGElement>) {
}
}
export function BadgeOfAffiliation(props: SVGProps<SVGSVGElement>) {
return (
<svg
width="81"
height="81"
viewBox="0 0 81 81"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect width="81" height="81" rx="34" fill="url(#paint0_linear_1_12)" />
<path
d="M29.5 36.5H26C25.0717 36.5 24.1815 36.1313 23.5251 35.4749C22.8687 34.8185 22.5 33.9283 22.5 33V26C22.5 25.0717 22.8687 24.1815 23.5251 23.5251C24.1815 22.8687 25.0717 22.5 26 22.5H54C54.9283 22.5 55.8185 22.8687 56.4749 23.5251C57.1313 24.1815 57.5 25.0717 57.5 26V33C57.5 33.9283 57.1313 34.8185 56.4749 35.4749C55.8185 36.1313 54.9283 36.5 54 36.5H50.5M29.5 43.5H26C25.0717 43.5 24.1815 43.8687 23.5251 44.5251C22.8687 45.1815 22.5 46.0717 22.5 47V54C22.5 54.9283 22.8687 55.8185 23.5251 56.4749C24.1815 57.1313 25.0717 57.5 26 57.5H54C54.9283 57.5 55.8185 57.1313 56.4749 56.4749C57.1313 55.8185 57.5 54.9283 57.5 54V47C57.5 46.0717 57.1313 45.1815 56.4749 44.5251C55.8185 43.8687 54.9283 43.5 54 43.5H50.5M29.5 29.5H29.5175M29.5 50.5H29.5175M41.75 29.5L34.75 40H45.25L38.25 50.5"
stroke="white"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
/>
<defs>
<linearGradient
id="paint0_linear_1_12"
x1="40.5"
y1="0"
x2="40.5"
y2="81"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#37B14F" />
<stop offset="1" stop-color="#3D4B17" />
</linearGradient>
</defs>
</svg>
);
}
export const Discord = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 256 199"

@ -31,6 +31,7 @@
"use client";
import { BorderBeam } from "@/components/effects/border-beam";
import { Button } from "@/components/ui/button";
import { AgGridReact, CustomCellRendererProps } from "ag-grid-react";
import {
Dialog,
DialogContent,
@ -39,6 +40,15 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
AllCommunityModule,
ModuleRegistry,
colorSchemeDarkBlue,
colorSchemeDarkWarm,
colorSchemeLightCold,
colorSchemeLightWarm,
themeQuartz,
} from "ag-grid-community";
import {
Menubar,
MenubarCheckboxItem,
@ -74,8 +84,10 @@ import {
ArrowDownZA,
Check,
CircleUser,
Copy,
ImageIcon,
Info,
Layers,
LogIn,
Network,
Sun,
@ -103,6 +115,15 @@ import { Skeleton } from "./ui/skeleton";
import { affiliates } from "@/config/affiliates";
import { LoadingSpinner } from "./ui/loading-spinner";
import StickyTopbar from "./misc/StickyTopbar";
import { HoverCard } from "@radix-ui/react-hover-card";
import { HoverCardTrigger } from "./ui/hover-card";
import { ExampleChart } from "./charts/ExampleChart";
// ag-grid
ModuleRegistry.registerModules([AllCommunityModule]);
const themeLightWarm = themeQuartz.withPart(colorSchemeLightWarm);
const themeDarkWarm = themeQuartz.withPart(colorSchemeDarkBlue);
const features = [
{
@ -164,12 +185,15 @@ export default function ServerList() {
const { user, isSignedIn } = useUser();
const [pOS, setpOS] = useState(false);
const [ipr, setIPR] = useState<string>("4");
const [presentationMode, setPresentationMode] = useState<"table" | "grid">(
"grid",
);
const [am, setAM] = useState(false);
const [filters, setFilters] = useState<
Array<(server: OnlineServer) => Promise<boolean>>
>([]);
const [randomData, setRandomData] = useState<OnlineServer | undefined>(
undefined
undefined,
);
const { resolvedTheme } = useTheme();
const [color, setColor] = useState("#ffffff");
@ -344,14 +368,14 @@ export default function ServerList() {
<br className="hidden md:block" /> in less than 10 minutes.
</p>
<div className="relative flex h-[300px] w-full flex-col items-center justify-center overflow-hidden rounded-lg bg-background ">
<Marquee className="[--duration:30s]">
<Marquee className="[--duration:30s]" pauseOnHover>
{serverList.currentServers.slice(0, 20).map((server) => (
<div
key={server.name}
className={cn(
"relative w-64 cursor-pointer overflow-hidden rounded-xl border no-underline " +
"border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05] " +
"dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]"
"dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]",
)}
onClick={() =>
router.push(pageFind(`Server:${server.name}`))
@ -378,14 +402,14 @@ export default function ServerList() {
</div>
))}
</Marquee>
<Marquee reverse className="[--duration:30s]">
<Marquee reverse className="[--duration:30s]" pauseOnHover>
{serverList.currentServers.slice(0, 20).map((server) => (
<div
key={server.name}
className={cn(
"relative w-64 cursor-pointer overflow-hidden rounded-xl border no-underline " +
"border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05] " +
"dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]"
"dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]",
)}
onClick={() => router.push(`/server/${server.name}`)}
>
@ -428,13 +452,22 @@ export default function ServerList() {
and descriptions, making your server stand out with information
that can be shown to players.
</p>
<BentoGrid className="max-h-[100px]">
<BentoGrid className="max-h-[100px] mb-[300px]">
{features.map((feature, idx) => (
<BentoCard key={idx} {...feature} />
))}
</BentoGrid>
<Separator />
<br />
<br />
<h1 className="animate-fade-in -translate-y-4 text-balance bg-gradient-to-br from-black from-30% to-black/40 bg-clip-text pb-6 text-2xl font-semibold leading-none tracking-tighter text-transparent opacity-0 [--animation-delay:200ms] sm:text-2xl md:text-3xl lg:text-4xl dark:from-white dark:to-white/40">
Monitor your success
</h1>
<p className="animate-fade-in mb-12 -translate-y-4 text-balance text-lg tracking-tight text-gray-400 opacity-0 [--animation-delay:400ms] md:text-xl">
Ever wondered how a server was doing? MHSF constantly monitors servers
and shows you statistics about how a server is doing at any point of time.
</p>
<ExampleChart />
</div>
)}
<br />
@ -572,7 +605,7 @@ export default function ServerList() {
c.forEach(
(b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
}
},
);
setMotdList(updatedSL);
setServers(serverList.currentServers);
@ -588,7 +621,7 @@ export default function ServerList() {
success: "Succesfully refreshed servers",
loading: "Refreshing...",
error: "Error while refreshing",
}
},
);
}}
>
@ -638,7 +671,7 @@ export default function ServerList() {
c.forEach(
(b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
}
},
);
setMotdList(updatedSL);
setServers(serverList.currentServers);
@ -680,7 +713,7 @@ export default function ServerList() {
c.forEach(
(b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
}
},
);
setMotdList(updatedSL);
setServers(serverList.currentServers);
@ -722,7 +755,7 @@ export default function ServerList() {
c.forEach(
(b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
}
},
);
setMotdList(updatedSL);
setServers(serverList.currentServers);
@ -735,7 +768,7 @@ export default function ServerList() {
error: "Error while changing filters",
loading: "Changing filters...",
success: "Changed filters!",
}
},
);
}}
value={(() => {
@ -822,7 +855,7 @@ export default function ServerList() {
c.forEach(
(b: { server: string; motd: string }) => {
updatedSL[b.server] = b.motd;
}
},
);
setMotdList(updatedSL);
setServers(serverList.currentServers);
@ -899,7 +932,23 @@ export default function ServerList() {
<MenubarTrigger>View</MenubarTrigger>
<MenubarContent>
<MenubarSub>
<MenubarSubTrigger>Grid</MenubarSubTrigger>
<MenubarSubTrigger>Mode</MenubarSubTrigger>
<MenubarSubContent>
<MenubarRadioGroup
value={presentationMode}
onValueChange={(v) =>
setPresentationMode(v as "grid" | "table")
}
>
<MenubarRadioItem value="grid">Grid</MenubarRadioItem>
<MenubarRadioItem value="table">Table</MenubarRadioItem>
</MenubarRadioGroup>
</MenubarSubContent>
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger disabled={presentationMode === "table"} className={presentationMode === "table" ? "text-muted-foreground" : ""}>
Grid
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarRadioGroup
value={ipr}
@ -913,7 +962,7 @@ export default function ServerList() {
onClick: () =>
router.push("/account/settings/options"),
},
}
},
);
setIPR(v);
}}
@ -945,7 +994,7 @@ export default function ServerList() {
onClick: () =>
router.push("/account/settings/options"),
},
}
},
);
setPadding(v);
}}
@ -968,24 +1017,6 @@ export default function ServerList() {
</MenubarCheckboxItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger>Sort</MenubarSubTrigger>
<MenubarSubContent>
<MenubarRadioGroup
value="joins"
onValueChange={(c) => {
if (c === "favorites") router.push("/sort/favorites");
}}
>
<MenubarRadioItem value="joins">
Players Online
</MenubarRadioItem>
<MenubarRadioItem value="favorites">
Favorites
</MenubarRadioItem>
</MenubarRadioGroup>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<SignedIn>
<MenubarCheckboxItem
@ -1076,7 +1107,7 @@ export default function ServerList() {
onClick={() => {
setTextCopied(true);
clipboard.writeText(
randomData.name + ".mshf.minehut.gg"
randomData.name + ".mshf.minehut.gg",
);
toast.success("Copied!");
setTimeout(() => setTextCopied(false), 1000);
@ -1096,6 +1127,7 @@ export default function ServerList() {
</Dialog>
<br />
{presentationMode === "grid" && (
<InfiniteScroll
dataLength={serverList.currentServers.length}
hasMore={serverList.hasMore}
@ -1149,6 +1181,7 @@ export default function ServerList() {
>
{servers.map((b: any, i: number) => (
<>
{/* <>
{i === Number(ipr) && affiliates.length != 0 && (
<div
className="border rounded h-[450px] shadow p-10 max-w-full dark:prose-invert prose grid grid-cols-3 max-xl:hidden"
@ -1185,18 +1218,158 @@ export default function ServerList() {
</div>
))}
</div>
)}
)} </> */}
<ServerCard b={b} motd={motdList[b.name]} />
</>
))}
</div>
</ClientFadeIn>
</InfiniteScroll>
)}
{presentationMode === "table" && (
<div className="h-[calc(100vh-430px)]">
<AgGridReact
rowData={serverList.servers}
columnDefs={[
{
field: "name",
cellRenderer: (c: CustomCellRendererProps) => {
return (
<>
<Tooltip>
<TooltipTrigger>
<HoverCard>
<HoverCardTrigger>
{c.data?.name}
</HoverCardTrigger>
</HoverCard>
</TooltipTrigger>
<TooltipContent>{c.data?.name}</TooltipContent>
</Tooltip>
</>
);
},
},
{
field: "playerData.playerCount",
headerName: "Players",
cellRenderer: (params: CustomCellRendererProps) => {
return (
<div className="flex items-center gap-2">
{params.value == 0 ? (
<div
className="items-center border"
style={{
width: ".5rem",
height: ".5rem",
borderRadius: "9999px",
}}
/>
) : (
<div
className="items-center"
style={{
backgroundColor: "#0cce6b",
width: ".5rem",
height: ".5rem",
borderRadius: "9999px",
}}
/>
)}{" "}
{params.value}
</div>
);
},
},
{
headerName: "Owner",
valueGetter: (p) => p.data?.author ?? "--",
cellRenderer: (c: CustomCellRendererProps) => {
if (c.data.author === "--") {
return <>--</>;
}
return (
<>
<Tooltip>
<TooltipTrigger>{c.data?.author}</TooltipTrigger>
<TooltipContent>{c.data?.author}</TooltipContent>
</Tooltip>
</>
);
},
},
{
headerName: "Tags",
valueGetter: (p) => (
<TagShower server={p.data as OnlineServer} />
),
cellRenderer: TagCR,
minWidth: 249,
},
{
headerName: "Actions",
minWidth: 107,
cellRenderer: (c: CustomCellRendererProps) => {
return (
<div>
<Button
size="icon"
variant="secondary"
className="md:min-w-[128px] md:max-w-[328px] h-[32px] mt-1 ml-2"
onClick={() => {
clipboard.writeText(
c.data.name + ".mshf.minehut.gg",
);
toast.success("Copied IP to clipboard");
}}
>
<Copy size={18} />
<code className="ml-2 max-md:hidden">
{c.data.name}
</code>
</Button>
<Tooltip>
<TooltipTrigger>
<Link href={"/server/" + c.data.name}>
<Button
size="icon"
variant="secondary"
className="w-[32px] h-[32px] mt-1 ml-2"
>
<Layers size={18} />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
Open up the server page to see more information
about the server
</TooltipContent>
</Tooltip>
</div>
);
},
},
]}
pagination={true}
paginationPageSize={50}
paginationPageSizeSelector={[10, 25, 50, 100]}
theme={resolvedTheme === "dark" ? themeDarkWarm : themeLightWarm}
defaultColDef={{
flex: 1,
}}
/>
</div>
)}
</div>
</div>
);
}
export function TagCR(params: CustomCellRendererProps) {
return <span className="w-full">{params.value}</span>;
}
export function TagShower(props: {
server: OnlineServer;
className?: string;
@ -1257,8 +1430,8 @@ export function TagShower(props: {
return (
<div className="font-normal tracking-normal ">
{compatiableTags.map((t) => (
<>
{compatiableTags.map((t, i) => (
<span key={i} className="mx-2">
{props.unclickable && (
<Badge variant={t.role} className={props.className}>
{t.name}
@ -1298,7 +1471,7 @@ export function TagShower(props: {
</DialogContent>
</Dialog>
)}
</>
</span>
))}
</div>
);

@ -58,8 +58,9 @@ import IconDisplay from "./IconDisplay";
import { useClerk, useUser } from "@clerk/nextjs";
import { LoaderIcon } from "react-hot-toast";
import { Separator } from "./ui/separator";
import { convert } from "@/components/NewChart";
import { convert } from "@/components/charts/NewChart";
import { LoadingSpinner } from "./ui/loading-spinner";
import { BadgeOfAffiliation } from "./Icon";
export default function ServerView(props: { server: string }) {
const [single, setSingle] = useState(new ServerSingle(props.server));
@ -120,12 +121,13 @@ export default function ServerView(props: { server: string }) {
<>
{single.grabOnline() == undefined && !single.grabOffline()?.online && (
<div className="grid pl-4 pr-4">
<X />
<div
className=" rounded p-2"
style={{ backgroundColor: "rgba(244, 63, 94, .16)" }}
>
<strong className="flex items-center">
This server is offline <X />
This server is offline
</strong>
<p>
This means that the server can{"'"}t loading some resources, like
@ -169,6 +171,22 @@ export default function ServerView(props: { server: string }) {
/>
)}
{props.server}
{props.server === "CoreBoxx" && (
<Tooltip>
<TooltipTrigger>
<BadgeOfAffiliation className="size-8 pl-2" />
</TooltipTrigger>
<TooltipContent className="font-normal tracking-normal flex items-center">
<BadgeOfAffiliation className="size-8 pr-2" />
<span>
CoreBoxx is an official affiliate of MHSF. This server was
<br /> found to be a high-quality server and should be a
good join for any player!
</span>
</TooltipContent>
</Tooltip>
)}
</CardTitle>
<CardDescription>
{/* 1704088800000 is the Unix time (in milliseconds) of (GMT) Monday, January 1, 2024 6:00:00 AM */}

@ -0,0 +1,174 @@
/*
* 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) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { TrendingDown, TrendingUp } from "lucide-react";
import {
Bar,
BarChart,
CartesianGrid,
LabelList,
Rectangle,
XAxis,
YAxis,
} from "recharts";
import { useEffect, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { Skeleton } from "../ui/skeleton";
import { getDailyData, getMonthlyData } from "@/lib/api";
import { useTrend } from "@/lib/hooks/use-daily-trend";
const chartConfig = {
result: {
label: "Players",
color: "hsl(var(--chart-3))",
},
} satisfies ChartConfig;
export function DailyChart({ server }: { server: string }) {
const [chartData, setChartData] = useState<{ day: string; result: number }[]>(
[],
);
const [loading, setLoading] = useState(true);
const { trend, percentage, success } = useTrend(chartData);
useEffect(() => {
getDailyData(server).then((c) => {
setChartData(c);
setLoading(false);
});
}, [server]);
if (loading) return <Skeleton className="w-full h-[437px]" />;
return (
<Card>
<CardHeader>
<CardTitle>Average daily players</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="">
<BarChart
accessibilityLayer
data={chartData}
layout="vertical"
margin={{
left: -20,
}}
>
<CartesianGrid horizontal={false} />
<XAxis type="number" dataKey="result" hide />
<YAxis
dataKey="day"
type="category"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Bar
dataKey="result"
fill="var(--color-result)"
radius={5}
strokeWidth={2}
activeIndex={chartData.findIndex(
(c) =>
c.day ===
new Date().toLocaleDateString("en-US", { weekday: "long" }),
)}
activeBar={({ ...props }) => {
return (
<Rectangle
{...props}
fill="hsl(var(--chart-4))"
stroke={props.payload.fill}
strokeDasharray={4}
strokeDashoffset={4}
/>
);
}}
>
<LabelList
dataKey="result"
position="insideLeft"
offset={8}
className="fill-[--color-label]"
fontSize={12}
/>
</Bar>
</BarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
{success ? (
<div
className={
"flex gap-2 items-center font-medium leading-none " +
(trend === "up" ? "text-green-400" : "text-red-400")
}
>
Trending {trend} by {percentage}% today{" "}
{trend === "up" ? (
<TrendingUp className="h-4 w-4" />
) : (
<TrendingDown className="h-4 w-4" />
)}
</div>
) : (
<div className={"flex gap-2 items-center font-medium leading-none"}>
Trending up by 0% today{" "}
<span className="text-muted-foreground">(Insufficient data)</span>
</div>
)}
<div className="leading-none text-muted-foreground">
Showing an average of all data for {server}
</div>
</CardFooter>
</Card>
);
}

@ -0,0 +1,191 @@
/*
* 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) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client"
import * as React from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const chartData = [
{ date: "2024-04-01", player_count: 91 },
{ date: "2024-04-02", player_count: 106 },
{ date: "2024-04-03", player_count: 104 },
{ date: "2024-04-04", player_count: 111 },
{ date: "2024-04-05", player_count: 113 },
{ date: "2024-04-06", player_count: 114 },
{ date: "2024-04-07", player_count: 108 },
{ date: "2024-04-08", player_count: 89 },
{ date: "2024-04-09", player_count: 96 },
{ date: "2024-04-10", player_count: 123 },
{ date: "2024-04-11", player_count: 120 },
{ date: "2024-04-12", player_count: 140 },
{ date: "2024-04-13", player_count: 128 },
{ date: "2024-04-14", player_count: 130 },
{ date: "2024-04-15", player_count: 114 },
{ date: "2024-04-16", player_count: 98 },
{ date: "2024-04-17", player_count: 102 },
{ date: "2024-04-18", player_count: 103 },
{ date: "2024-04-19", player_count: 102 },
{ date: "2024-04-20", player_count: 112},
{ date: "2024-04-21", player_count: 117 },
{ date: "2024-04-22", player_count: 119 },
{ date: "2024-04-23", player_count: 129 },
{ date: "2024-04-24", player_count: 121 },
{ date: "2024-04-25", player_count: 126 },
{ date: "2024-04-26", player_count: 98},
{ date: "2024-04-27", player_count: 102 },
{ date: "2024-04-28", player_count: 100 },
{ date: "2024-04-29", player_count: 101 },
{ date: "2024-04-30", player_count: 104 },
{ date: "2024-05-01", player_count: 109 },
{ date: "2024-05-02", player_count: 86 },
{ date: "2024-05-03", player_count: 93 },
{ date: "2024-05-04", player_count: 108 },
{ date: "2024-05-05", player_count: 112 },
{ date: "2024-05-06", player_count: 111 },
{ date: "2024-05-07", player_count: 96 },
{ date: "2024-05-08", player_count: 100 },
{ date: "2024-05-09", player_count: 124 },
{ date: "2024-05-10", player_count: 134 },
{ date: "2024-05-11", player_count: 144 },
{ date: "2024-05-12", player_count: 156 },
{ date: "2024-05-13", player_count: 180 },
{ date: "2024-05-14", player_count: 167 },
{ date: "2024-05-15", player_count: 154 },
{ date: "2024-05-16", player_count: 124 },
{ date: "2024-05-17", player_count: 112 },
{ date: "2024-05-18", player_count: 114 },
{ date: "2024-05-19", player_count: 121 },
{ date: "2024-05-20", player_count: 96 },
{ date: "2024-05-21", player_count: 102 },
{ date: "2024-05-22", player_count: 131 },
]
const chartConfig = {
player_count: {
label: "Players",
color: "hsl(var(--chart-1))",
}
} satisfies ChartConfig
export function ExampleChart() {
return (
<Card>
<CardHeader className="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row">
<div className="grid flex-1 gap-1 text-center sm:text-left">
<CardTitle className="text-sm">Player count over 3 months</CardTitle>
</div>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={chartData}>
<defs>
<linearGradient id="fillPlayers" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-player_count)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-player_count)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value)
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(value) => {
return new Date(value).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}}
indicator="dot"
/>
}
/>
<Area
dataKey="player_count"
type="natural"
fill="url(#fillPlayers)"
stroke="var(--color-player_count)"
stackId="a"
/>
<ChartLegend content={<ChartLegendContent />} />
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}

@ -0,0 +1,160 @@
/*
* 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) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { TrendingDown, TrendingUp } from "lucide-react";
import { Bar, BarChart, CartesianGrid, LabelList, Rectangle, XAxis, YAxis } from "recharts";
import { useEffect, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { Skeleton } from "../ui/skeleton";
import { useTrend } from "@/lib/hooks/use-trend";
import { getMonthlyData } from "@/lib/api";
const chartConfig = {
result: {
label: "Players",
color: "hsl(var(--chart-3))",
},
} satisfies ChartConfig;
export function MonthlyChart({ server }: { server: string }) {
const [chartData, setChartData] = useState<
{ month: string; result: number }[]
>([]);
const [loading, setLoading] = useState(true);
const { trend, percentage, success } = useTrend(chartData);
useEffect(() => {
getMonthlyData(server).then((c) => {
setChartData(c);
setLoading(false);
});
}, [server]);
if (loading) return <Skeleton className="w-full h-[437px]" />;
return (
<Card>
<CardHeader>
<CardTitle>Average monthly players</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="">
<BarChart
accessibilityLayer
data={chartData}
layout="vertical"
margin={{
left: -20,
}}
>
<CartesianGrid horizontal={false} />
<XAxis type="number" dataKey="result" hide />
<YAxis
dataKey="month"
type="category"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Bar dataKey="result" fill="var(--color-result)" radius={5} activeIndex={chartData.findIndex(
(c) =>
c.month ===
new Date().toLocaleDateString("en-US", { month: "long" }),
)}
activeBar={({ ...props }) => {
return (
<Rectangle
{...props}
fill="hsl(var(--chart-4))"
stroke={props.payload.fill}
strokeDasharray={4}
strokeDashoffset={4}
/>
);
}}>
<LabelList
dataKey="result"
position="insideLeft"
offset={8}
className="fill-[--color-label]"
fontSize={12}
/>
</Bar>
</BarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
{success ? (
<div
className={
"flex gap-2 items-center font-medium leading-none " +
(trend === "up" ? "text-green-400" : "text-red-400")
}
>
Trending {trend} by {percentage}% this month{" "}
{trend === "up" ? (
<TrendingUp className="h-4 w-4" />
) : (
<TrendingDown className="h-4 w-4" />
)}
</div>
) : (
<div className={"flex gap-2 items-center font-medium leading-none"}>
Trending up by 0% this month{" "}
<span className="text-muted-foreground">(Insufficient data)</span>
</div>
)}
<div className="leading-none text-muted-foreground">
Showing an average of all data for {server}
</div>
</CardFooter>
</Card>
);
}

@ -31,7 +31,14 @@
"use client";
import * as React from "react";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import {
CartesianGrid,
Line,
LineChart,
XAxis,
YAxis,
ReferenceLine,
} from "recharts";
import {
Card,
@ -49,8 +56,16 @@ import {
import { useEffectOnce } from "@/lib/useEffectOnce";
import { ServerResponse } from "@/lib/types/mh-server";
import { getCommunityServerFavorites, getShortTermData } from "@/lib/api";
import { Skeleton } from "./ui/skeleton";
import { Skeleton } from "../ui/skeleton";
import FadeIn from "react-fade-in/lib/FadeIn";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const chartConfig = {
player_count: {
@ -68,15 +83,20 @@ export function NewChart({ server }: { server: string }) {
React.useState<keyof typeof chartConfig>("player_count");
const [chartData, setChartData] = React.useState<any>([]);
const [allData, setAllData] = React.useState<any>([]);
const [joins, setJoins] = React.useState<any>(0);
const [loading, setLoading] = React.useState(true);
const [dataMax, setDataMax] = React.useState(0);
const [favorites, setFavorites] = React.useState<any>(0);
const [filter, setFilter] = React.useState("60");
const allNums = { player_count: joins, favorites };
useEffectOnce(() => {
getShortTermData(server, ["player_count", "favorites", "date"]).then(
(c) => {
setChartData(c);
setAllData(c.data);
setChartData(c.data.slice(-60));
setDataMax(c.dataMax);
getCommunityServerFavorites(server).then((b) => setFavorites(b));
fetch("https://api.minehut.com/server/" + server + "?byName=true").then(
(k) => {
@ -90,6 +110,14 @@ export function NewChart({ server }: { server: string }) {
);
});
React.useEffect(() => {
if (filter === "all") {
setChartData(allData);
} else {
setChartData(allData.slice(Number(filter) * -1));
}
}, [filter, allData]);
if (loading)
return (
<>
@ -105,7 +133,29 @@ export function NewChart({ server }: { server: string }) {
<CardTitle>
{chartConfig[activeChart].label} Chart for {server}
</CardTitle>
<CardDescription>Showing the past 30 entries.</CardDescription>
<CardDescription className="flex items-center">
Showing {filter !== "all" && "the last"}{" "}
<Select value={filter} onValueChange={setFilter}>
{" "}
<SelectTrigger className="max-w-[80px] mx-2">
<SelectValue placeholder="60" />
</SelectTrigger>
<SelectContent>
<SelectGroup className="max-h-[200px]">
<SelectItem value="30">30</SelectItem>
<SelectItem value="60">60</SelectItem>
<SelectItem value="90">90</SelectItem>
<SelectItem value="120">120</SelectItem>
<SelectItem value="150">150</SelectItem>
<SelectItem value="180">180</SelectItem>
<SelectItem value="210">210</SelectItem>
<SelectItem value="240">240</SelectItem>
<SelectItem value="all">all</SelectItem>
</SelectGroup>
</SelectContent>
</Select>{" "}
{filter === "all" && "of the"} entries.
</CardDescription>
</div>
<div className="flex">
{["player_count", "favorites"].map((key) => {
@ -149,9 +199,7 @@ export function NewChart({ server }: { server: string }) {
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
return new Date(value).toLocaleTimeString("en-US", {
timeStyle: "short",
});
return `${new Date(value).getMonth() + 1}/${new Date(value).getDate()}`;
}}
/>
<YAxis
@ -166,6 +214,9 @@ export function NewChart({ server }: { server: string }) {
: ` ${value == 1 ? "favorite" : "favrts."}`)
);
}}
domain={
activeChart === "player_count" ? [0, dataMax + 10] : undefined
}
/>
<ChartTooltip
content={
@ -174,13 +225,25 @@ export function NewChart({ server }: { server: string }) {
nameKey={activeChart}
indicator="line"
labelFormatter={(value) => {
return new Date(value).toLocaleTimeString("en-US", {
return `${new Date(value).toLocaleDateString("en-US", {
day: "numeric",
month: "short",
})} ${new Date(value).toLocaleTimeString("en-US", {
timeStyle: "short",
});
})}`;
}}
/>
}
/>
{activeChart === "player_count" && (
<ReferenceLine
y={dataMax}
stroke={`var(--color-${activeChart})`}
strokeWidth={2}
label="all-time max"
strokeDasharray="3 3"
/>
)}
<Line
dataKey={activeChart}
type="monotone"

@ -0,0 +1,163 @@
/*
* 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) 2024 dvelo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
"use client";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { Skeleton } from "../ui/skeleton";
import { useEffect, useState } from "react";
import { getRelativeServerData } from "@/lib/api";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { Info } from "lucide-react";
export function RelativeChart({ server }: { server: string }) {
const [chartData, setChartData] = useState<
Array<{ relativePercentage: number; date: string }>
>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
getRelativeServerData(server).then((d) => {
const newv: Array<{ relativePercentage: number; date: string }> = [];
d.forEach((c) =>
newv.push({
relativePercentage: c.relativePrecentage * 100,
date: c.date,
})
);
setChartData(newv);
setLoading(false);
});
}, [server]);
if (loading)
return (
<>
<Skeleton className="w-full h-[437px]" />
</>
);
return (
<Card>
<CardHeader className="w-full relative">
<CardTitle>Relative percentage of {server}</CardTitle>
<CardDescription className="flex items-center">
Shows the last 30 entries.{" "}
<Tooltip>
<TooltipTrigger>
<Info className="size-4 ml-2" />
</TooltipTrigger>
<TooltipContent>
This is the percentage of players on your server <br />
compared to the entire Minehut network. <br />
<code>Server players / Minehut players</code>
</TooltipContent>
</Tooltip>
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer
config={{
relativePercentage: {
label: "Relative percentage",
color: "hsl(var(--chart-2))",
},
}}
className="h-[250px] w-full aspect-auto"
>
<LineChart
accessibilityLayer
data={chartData}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) =>
`${new Date(value).toLocaleTimeString("en-US", {
timeStyle: "short",
})}`
}
/>
<YAxis
dataKey="relativePercentage"
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}%`}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="w-[150px]"
nameKey="relativePercentage"
indicator="line"
labelFormatter={(value) => {
return `${new Date(value).toLocaleDateString("en-US", {
day: "numeric",
month: "short",
})} ${new Date(value).toLocaleTimeString("en-US", {
timeStyle: "short",
})}`;
}}
/>
}
/>
<Line
dataKey="relativePercentage"
type="natural"
stroke="var(--color-relativePercentage)"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
);
}

@ -0,0 +1,94 @@
/*
* 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) 2024 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 {
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { useState } from "react";
import useClipboard from "@/lib/useClipboard";
import { Button } from "../ui/button";
import { Check } from "lucide-react";
export default function AffiliatePopup() {
const [textCopied, setTextCopied] = useState(false);
const clipboard = useClipboard();
return (
<DialogHeader>
<DialogTitle className="text-black dark:text-white">CoreBoxx</DialogTitle>
<DialogDescription className="text-black dark:text-white text-md">
We won't claim to be a "unique server" or any of that. But we do aim to
give you the highest quality BoxPvP! Here's what we have: <br /> - 50
progression mines over 11 worlds, plus many more for events and other
items <br /> - Balanced gear sets - just because someone is a bit ahead
of you doesn't mean you deserve to die in one shot <br /> - We're
non-P2W, and we mean it! All perks are for convenience or benefit
everyone online! <br /> - A rapidly-growing, tight-knit community with
events and giveaways <br /> - Quests and storyline in development, in
case mining gets boring <br />
<br />
<code className="border my-2 p-3 flex rounded">
CoreBoxx.minehut.gg{" "}
</code>
<div
className="border rounded p-2 mb-8 text-sm"
style={{ backgroundColor: "hsl(var(--background))" }}
>
CoreBoxx is an official affiliate of MHSF. This server was found to be
a high-quality server and should be a good join for any player!
</div>
</DialogDescription>
<span className="w-full grid grid-cols-3 gap-2 opacity-80 backdrop-blur">
<Button onClick={() => window.open("/server/CoreBoxx", "_blank")}>
Open Page
</Button>
<Button
onClick={() => {
setTextCopied(true);
clipboard.writeText("CoreBoxx.mhsf.minehut.gg");
setTimeout(() => setTextCopied(false), 1000);
}}
>
{textCopied ? (
<Check size={16} className="flex items-center" />
) : (
<p>Copy IP</p>
)}
</Button>
<DialogTrigger asChild>
<Button variant="outline">Close</Button>
</DialogTrigger>
</span>
</DialogHeader>
);
}

@ -52,7 +52,7 @@ export function MiniJoinsChart({ server }: { server: string }) {
useEffectOnce(() => {
getShortTermData(server, ["player_count", "date"]).then((result) => {
setChartData(result.slice(-20));
setChartData(result.data.slice(-20));
setLoading(false);
});
});

@ -61,7 +61,7 @@ export default function StickyTopbar({
return (
<div
className={`transition-all duration-300 ${isSticky ? "fixed left-0 w-full backdrop-blur shadow-lg " + className : "block w-full bg-transparent"}`}
className={`transition-all duration-300 z-[9] ${isSticky ? "fixed left-0 w-full backdrop-blur shadow-lg " + className : "block w-full bg-transparent"}`}
style={{
top: isSticky ? `${bannerSize * 32 + 38}px` : undefined,
}}

@ -29,7 +29,6 @@
*/
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
@ -52,7 +51,7 @@ const DialogOverlay = React.forwardRef<
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 backdrop-blur",
className,
className
)}
{...props}
/>
@ -69,7 +68,7 @@ const DialogContent = React.forwardRef<
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
className
)}
{...props}
>
@ -90,7 +89,7 @@ const DialogHeader = ({
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
className
)}
{...props}
/>
@ -104,7 +103,7 @@ const DialogFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
className
)}
{...props}
/>
@ -119,7 +118,7 @@ const DialogTitle = React.forwardRef<
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
className
)}
{...props}
/>

@ -1,41 +1,104 @@
import GradientBanner from "@/components/effects/gradient-banner";
import MainBanner from "@/components/feat/MainBanner";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { useState, useEffect } from "react";
import { DialogContent, Dialog } from "@/components/ui/dialog";
import AffiliatePopup from "@/components/misc/AffiliatePopup";
import { Gradient } from "stripe-gradient";
export const defaultBanners: {
bannerSpace: number;
bannerContent: React.ReactNode;
}[] = [
// The sponsor banner ALWAYS has to be first.
// {
// bannerSpace: 2,
// bannerContent: (
// <MainBanner size={2} className="max-h-[4rem] border-0">
// {" "}
// <GradientBanner>
// <strong>???</strong> — <i>an official affiliate of MHSF</i>{" "}
// <br />
// Lorem ipsum odor amet, consectetuer adipiscing elit. — check it out
// </GradientBanner>
// </MainBanner>
// ),
// },
// The affilation banner ALWAYS has to be first.
{
bannerSpace: 2,
bannerContent: (
<>
<AffiliateBanner />
</>
),
},
];
function AffiliateBanner() {
const [isOpen, setOpen] = useState(false);
useEffect(() => {
const gradient = new Gradient();
const initializeGradient = () => {
const canvasElement = document.getElementById(
"gradient-dialog"
) as HTMLCanvasElement;
if (canvasElement) gradient.initGradient("#gradient-dialog");
};
if (isOpen) {
const timeoutId = setTimeout(initializeGradient, 100); // Delay to ensure canvas is ready
return () => clearTimeout(timeoutId); // Cleanup timeout
}
}, [isOpen]);
return (
<>
<Dialog open={isOpen} onOpenChange={setOpen}>
<DialogContent>
<>
<canvas
id="gradient-dialog"
className="h-full absolute w-[512px] rounded blur-sm"
style={
{
"--gradient-color-1": "#6ec3f4",
"--gradient-color-2": "#3a3aff",
"--gradient-color-3": "#ff61ab",
"--gradient-color-4": "#E63946",
webKitMaskImage:
"linear-gradient(to top, black 0%, transparent 25%, transparent 80%, black 100%)",
maskImage:
"linear-gradient(to top, black 0%, transparent 25%, transparent 80%, black 100%)",
} as React.CSSProperties
}
/>
<div className="relative z-10">
<AffiliatePopup />
</div>
</>
</DialogContent>
</Dialog>
<div onClick={() => setOpen(true)} className="cursor-pointer">
<MainBanner size={2} className="max-h-[4rem] border-0">
<GradientBanner>
<strong>CoreBoxx</strong> <i>an official affiliate of MHSF</i>{" "}
<br />
Season 3 is out the doors for the best box server on Minehut
</GradientBanner>
</MainBanner>
</div>
</>
);
}
export const bannerHooks: (() =>
| { bannerSpace: number; bannerContent: React.ReactNode }
| undefined)[] = [
() => {
// if (process.env.NEXT_PUBLIC_VERCEL_ENV !== "production")
// return {
// bannerSpace: 1,
// bannerContent: (
// <MainBanner className="bg-orange-600">
// Your not in production!{" "}
// <Link href="https://mhsf.app">
// <Button variant="link" className="dark:text-black">
// Go to production
// </Button>
// </Link>
// </MainBanner>
// ),
// };
if (process.env.NEXT_PUBLIC_VERCEL_ENV !== "production")
return {
bannerSpace: 1,
bannerContent: (
<MainBanner className="bg-orange-600">
Your not in production!{" "}
<Link href="https://mhsf.app">
<Button variant="link" className="dark:text-black">
Go to production
</Button>
</Link>
</MainBanner>
),
};
return undefined;
},
];

@ -39,14 +39,14 @@ import { Achievement } from "./types/achievement";
const connector = (
endpoint: string,
options: { version: number; starting?: string },
options: { version: number; starting?: string }
) =>
`${options.starting == undefined ? "/" : `${options.starting}/`}api/v${options.version}${endpoint}`;
async function apiConstructor<K>(
connector: string,
requestInit: RequestInit,
modifier: (data: any) => K,
modifier: (data: any) => K
): Promise<K> {
try {
const response = await fetch(connector, requestInit);
@ -60,7 +60,7 @@ async function apiConstructor<K>(
}
}
export async function getMOTDFromServer(
list: Array<{ server: string; motd: string }>,
list: Array<{ server: string; motd: string }>
): Promise<Array<{ server: string; motd: string }>> {
const result = await fetch(
process.env.NEXT_PUBLIC_ALTERNATE_MOTD_ENDPOINT ??
@ -71,15 +71,48 @@ export async function getMOTDFromServer(
headers: {
"Content-Type": "application/json",
},
},
}
);
let json = await result.json();
return json.result;
}
export async function getRelativeServerData(
server: string
): Promise<Array<{ date: string; relativePrecentage: number }>> {
const result = await fetch(
connector("/history/" + server + "/get-relative-data", { version: 0 })
);
const json = await result.json();
return json.data;
}
export async function getMonthlyData(
server: string
): Promise<Array<{ month: string; result: number }>> {
const result = await fetch(
connector("/history/" + server + "/get-monthly-data", { version: 0 })
);
const json = await result.json();
return json.result;
}
export async function getDailyData(
server: string
): Promise<Array<{ day: string; result: number }>> {
const result = await fetch(
connector("/history/" + server + "/get-daily-data", { version: 0 })
);
const json = await result.json();
return json.result;
}
export async function getCommunityServerFavorites(
server: string,
server: string
): Promise<number> {
const result = await fetch(
connector(`/favorites/${server}/community-favorites`, { version: 0 }),
@ -88,7 +121,7 @@ export async function getCommunityServerFavorites(
headers: {
"Content-Type": "application/json",
},
},
}
);
let json = await result.json();
@ -105,7 +138,7 @@ export async function favoriteServer(server: string) {
headers: {
"Content-Type": "application/json",
},
},
}
);
} catch {
throw Error("Not authenticated with a user.");
@ -122,7 +155,7 @@ export async function isFavorited(server: string): Promise<boolean> {
headers: {
"Content-Type": "application/json",
},
},
}
);
return (await response.json()).result;
@ -141,7 +174,7 @@ export async function getAccountFavorites(): Promise<Array<string>> {
headers: {
"Content-Type": "application/json",
},
},
}
);
return (await response.json()).result;
@ -155,7 +188,7 @@ export async function getAccountFavorites(): Promise<Array<string>> {
*/
export async function getHistoricalData(
server: string,
scopes: Array<"player_count" | "favorites" | "server" | "time">,
scopes: Array<"player_count" | "favorites" | "server" | "time">
): Promise<
Array<{
player_count?: number;
@ -172,7 +205,7 @@ export async function getHistoricalData(
headers: {
"Content-Type": "application/json",
},
},
}
);
return (await response.json()).data;
@ -180,15 +213,16 @@ export async function getHistoricalData(
export async function getShortTermData(
server: string,
scopes: Array<"player_count" | "favorites" | "server" | "date">,
): Promise<
Array<{
scopes: Array<"player_count" | "favorites" | "server" | "date">
): Promise<{
data: Array<{
player_count?: number;
favorites?: number;
server?: string;
time?: number;
}>
> {
}>;
dataMax: number;
}> {
const response = await fetch(
connector(`/history/${server}/get-short-term-data`, { version: 0 }),
{
@ -197,14 +231,14 @@ export async function getShortTermData(
headers: {
"Content-Type": "application/json",
},
},
}
);
return (await response.json()).data;
return await response.json();
}
export async function getMetaShortTerm(
scopes: Array<"total_players" | "total_servers" | "date">,
scopes: Array<"total_players" | "total_servers" | "date">
): Promise<
Array<{
total_players?: number;
@ -220,7 +254,7 @@ export async function getMetaShortTerm(
headers: {
"Content-Type": "application/json",
},
},
}
);
return (await response.json()).data;
@ -237,7 +271,7 @@ export async function linkMCAccount(code: string): Promise<string | undefined> {
"Content-Type": "application/json",
},
body: JSON.stringify({ code }),
},
}
);
if (response.status == 400) {
@ -260,13 +294,13 @@ export async function unlinkMCAccount(): Promise<boolean> {
headers: {
"Content-Type": "application/json",
},
},
}
);
return true;
} catch {
throw Error(
"Not authenticated with a user or user already linked account.",
"Not authenticated with a user or user already linked account."
);
}
}
@ -281,7 +315,7 @@ export async function serverOwned(server: string): Promise<boolean> {
"Content-Type": "application/json",
},
body: JSON.stringify({ server }),
},
}
);
return (await response.json()).owned;
@ -301,7 +335,7 @@ export async function userOwnedServer(server: string): Promise<boolean> {
"Content-Type": "application/json",
},
body: JSON.stringify({ server }),
},
}
);
return (await response.json()).result;
@ -321,7 +355,7 @@ export async function ownServer(server: string): Promise<boolean> {
"Content-Type": "application/json",
},
body: JSON.stringify({ server }),
},
}
);
if (response.status >= 400) {
@ -345,7 +379,7 @@ export async function unownServer(server: string): Promise<boolean> {
"Content-Type": "application/json",
},
body: JSON.stringify({ server }),
},
}
);
if (response.status == 400) {
@ -361,7 +395,7 @@ export async function unownServer(server: string): Promise<boolean> {
/** requires authentication */
export async function setCustomization(
server: string,
customization: any,
customization: any
): Promise<boolean | Error> {
try {
const response = await fetch(
@ -372,7 +406,7 @@ export async function setCustomization(
"Content-Type": "application/json",
},
body: JSON.stringify({ customization }),
},
}
);
if (response.status == 400) {
@ -395,7 +429,7 @@ export async function getCustomization(server: string): Promise<any> {
headers: {
"Content-Type": "application/json",
},
},
}
);
if (response.status == 400) {
@ -419,7 +453,7 @@ export async function sortedFavorites(): Promise<
headers: {
"Content-Type": "application/json",
},
},
}
);
if (response.status == 400) {
@ -434,7 +468,7 @@ export async function sortedFavorites(): Promise<
export async function reportServer(
server: string,
reason: string,
reason: string
): Promise<boolean> {
try {
const response = await fetch(connector(`/report-server`, { version: 1 }), {
@ -457,7 +491,7 @@ export async function reportServer(
export async function setAccountSL(
data: number | boolean | null,
type: "srv" | "ipr" | "pad",
type: "srv" | "ipr" | "pad"
): Promise<boolean> {
try {
const response = await fetch(
@ -468,7 +502,7 @@ export async function setAccountSL(
"Content-Type": "application/json",
},
body: JSON.stringify({ data, type }),
},
}
);
if (response.status === 400) {
@ -485,5 +519,5 @@ export const getAchievements = async (server: string) =>
apiConstructor<{ _id: string; name: string; achievements: Achievement[] }[]>(
connector(`/achievements/${server}`, { version: 0 }),
{},
(data) => data.result,
(data) => data.result
);

@ -0,0 +1,80 @@
/*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://mhsf.app/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2024 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";
export function useTrend(data: { day: string; result: number }[]) {
const [success, setSuccess] = useState(true);
const [trend, setTrend] = useState<"up" | "down">("up");
const [percentage, setPercentage] = useState<number>(0);
useEffect(() => {
const today = new Date();
const previousDay = new Date();
previousDay.setDate(previousDay.getDate() - 1);
const previousDayData = data.find(
(x) => x.day === previousDay.toLocaleString('en-us', { weekday: 'long' }),
);
const todayData = data.find(
(x) => x.day === today.toLocaleString('en-us', { weekday: 'long' }),
);
if (previousDayData === undefined || todayData === undefined) {
setSuccess(false);
return;
}
if (previousDayData.result === 0) {
setSuccess(false);
return;
}
setSuccess(true);
setTrend(previousDayData.result < todayData.result ? "up" : "down");
setPercentage(
Math.abs(
Number(
(
((todayData.result - previousDayData.result) /
previousDayData.result) *
100
).toFixed(1),
),
),
);
}, [data]);
return {
success,
trend,
percentage,
};
}

@ -27,3 +27,24 @@
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import { useEffect, useState } from "react";
export function useServerExists(server: string) {
const [serverExists, setServerExists] = useState(true);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("https://api.minehut.com/server/" + server + "?byName=true")
.then((c) => c.json())
.then((d) => {
setServerExists(d.server != null);
setLoading(false);
});
});
return {
serverExists,
loading,
};
}

@ -0,0 +1,95 @@
/*
* 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) 2024 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";
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
export function useTrend(data: { month: string; result: number }[]) {
const [success, setSuccess] = useState(true);
const [trend, setTrend] = useState<"up" | "down">("up");
const [percentage, setPercentage] = useState<number>(0);
useEffect(() => {
const today = new Date();
const previousMonth = new Date();
previousMonth.setDate(0);
const previousMonthData = data.find(
(x) => x.month === months[previousMonth.getMonth()],
);
const todayMonthData = data.find(
(x) => x.month === months[today.getMonth()],
);
if (previousMonthData === undefined || todayMonthData === undefined) {
setSuccess(false);
return;
}
if (previousMonthData.result === 0) {
setSuccess(false);
return;
}
setSuccess(true);
setTrend(previousMonthData.result < todayMonthData.result ? "up" : "down");
setPercentage(
Math.abs(
Number(
(
((todayMonthData.result - previousMonthData.result) /
previousMonthData.result) *
100
).toFixed(1),
),
),
);
}, [data]);
return {
success,
trend,
percentage,
};
}

@ -5,7 +5,7 @@ import MiniMessage from "minimessage-js";
var numberOfItemsInView = 20;
export default class ServersList {
private servers: Array<OnlineServer> = [];
servers: Array<OnlineServer> = [];
currentServers: Array<OnlineServer> = [];
private filters: Array<(server: OnlineServer) => Promise<boolean>> = [];
extraData: any = { total_players: 0, total_servers: 0 };

@ -50,7 +50,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("auth_codes");
const entry = await collection.findOne({ code });

@ -45,7 +45,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
res.send({ owned: (await collection.findOne({ server })) != undefined });

@ -57,7 +57,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
if ((await collection.findOne({ server: server })) == undefined) {

@ -56,7 +56,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
res.send({

@ -44,7 +44,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const users = db.collection("claimed-users");
const user = await (await clerkClient()).users.getUser(userId);

@ -56,7 +56,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
const customization = db.collection("customization");

@ -41,7 +41,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("achievements");
res.send({ result: await collection.find({ name: server }).toArray() });

@ -39,7 +39,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("customization");
res.send({ results: await collection.findOne({ server }) });

@ -86,7 +86,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("owned-servers");
const customizationColl = db.collection("customization");
if (!((await collection.findOne({ server, author: userId })) == undefined)) {

@ -44,7 +44,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("customization");
const results: { server: string; customization: any }[] = [];

@ -40,7 +40,7 @@ export default async function handler(
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("meta");
const find = await collection.find({ server: server }).toArray();

@ -46,7 +46,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();

@ -45,7 +45,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();
if (find.length == 0) res.send({ result: false });

@ -44,7 +44,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("favorites");
const find = await collection.find({ user: userId }).toArray();

@ -0,0 +1,62 @@
/*
* 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) 2024 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";
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;
}));
client.close()
res.send({result: result.filter((c) => c !== undefined)});
}

@ -0,0 +1,62 @@
/*
* 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) 2024 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";
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;
}));
client.close()
res.send({result: result.filter((c) => c !== undefined)});
}

@ -0,0 +1,97 @@
/*
* 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) 2024 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.
*//*
* MHSF, Minehut Server List
* All external content is rather licensed under the ECA Agreement
* located here: https://list.mlnehut.com/docs/legal/external-content-agreement
*
* All code under MHSF is licensed under the MIT License
* by open source contributors
*
* Copyright (c) 2024 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";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history");
const mh = client.db("mhsf").collection("mh");
const server = req.query.server as string;
const allData = await db.find({ server }).toArray();
const data: any[] = [];
if (server === "peww") console.log(allData.slice(-30));
for (const d of allData.slice(-30)) {
const dateOfEntry = new Date(d.date);
const result = await mh
.find({
date: {
$gte: new Date(dateOfEntry.getTime() - 1000 * 60 * 60),
$lt: new Date(dateOfEntry.getTime() + 1000 * 60 * 60),
},
})
.toArray();
if (result.length > 0) {
const resultedData = result[0];
data.push({
relativePrecentage: d.player_count / resultedData.total_players,
date: dateOfEntry,
});
}
}
client.close();
res.send({ data });
}

@ -38,11 +38,16 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history");
const server = req.query.server as string;
let dataMax = 0;
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes);
const allData = await db.find({ server }).toArray();
const data: any[] = [];
dataMax = (
await db.find({ server }).sort({ player_count: -1 }).limit(1).toArray()
)[0].player_count;
allData.forEach((d) => {
const result: any = {};
scopes.forEach((b) => {
@ -52,7 +57,7 @@ export default async function handler(
});
client.close();
res.send({ data });
res.send({ data, dataMax });
}
function checkForInfoOrLeave(res: NextApiResponse, info: any) {

@ -38,7 +38,7 @@ export default async function handler(
const client = new MongoClient(process.env.MONGO_DB as string);
await client.connect();
const db = client.db("mhsf");
const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf");
const collection = db.collection("meta");
const all = await collection.find().toArray();

@ -748,7 +748,7 @@
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/sourcemap-codec@^1.4.15":
"@jridgewell/sourcemap-codec@^1.4.15", "@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
@ -2494,6 +2494,11 @@
resolved "https://registry.yarnpkg.com/@unocss/core/-/core-0.61.5.tgz#09f4da600f6f50dbb68a173f23566e9171d5cd3d"
integrity sha512-hB8zr2rnrCzz9x8ho2SAXQiYTEjwAPMiBzpaEe2C0+CFWeL1179h9508YVyZHHAzMyZILIG9YrVAWrrMdt2/Xg==
"@unocss/core@0.65.3", "@unocss/core@^0.65.3":
version "0.65.3"
resolved "https://registry.yarnpkg.com/@unocss/core/-/core-0.65.3.tgz#c49c90e4be627cae1a29800e089588835f1f087a"
integrity sha512-xYkJ63lIadL6KqvGcaE2fFeLvo6rC1F+e+R9EFn0Aj0ArMRhiltZk8vvLFHP7iYjjdTdqDkAr/7IdrTosTo8Pg==
"@unocss/eslint-config@^0.61.5":
version "0.61.5"
resolved "https://registry.yarnpkg.com/@unocss/eslint-config/-/eslint-config-0.61.5.tgz#01a8ebb4a626d22d6e29d9800c31d2f87bed2b6b"
@ -2512,12 +2517,12 @@
magic-string "^0.30.10"
synckit "^0.9.1"
"@unocss/extractor-arbitrary-variants@0.61.5":
version "0.61.5"
resolved "https://registry.yarnpkg.com/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-0.61.5.tgz#478b15f0ae298b0f05d9978ef528b08723c22cb5"
integrity sha512-UB1EweAaJrUxv+h3n5FqoizKHrnUgUzkdmOdJTfV6xvow90ITqbUoza+L6iVMNfcrcXTx8QpDnWh6rhLRyKY+g==
"@unocss/extractor-arbitrary-variants@0.65.3":
version "0.65.3"
resolved "https://registry.yarnpkg.com/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-0.65.3.tgz#ed65b2aac189f9fcf083be6d8a0ec449cc9832ed"
integrity sha512-ZVGCjOZuU8daGxY7MUJQrI7aVKzZi1llRk53QgEUTU1q60X/fi8M2+A9mwEgG9MBVHBdsuvxqZ9Dp79IktSyLw==
dependencies:
"@unocss/core" "0.61.5"
"@unocss/core" "0.65.3"
"@unocss/postcss@^0.61.5":
version "0.61.5"
@ -2532,33 +2537,33 @@
magic-string "^0.30.10"
postcss "^8.4.39"
"@unocss/preset-mini@0.61.5":
version "0.61.5"
resolved "https://registry.yarnpkg.com/@unocss/preset-mini/-/preset-mini-0.61.5.tgz#60dc1aa2c05e415b6e9860493fb92a20acce62da"
integrity sha512-gVm7Z9X0krx8CK/+pKAqcVmpqzRk1+SH7bfgRxKtKhyFSxJlwpjNp1rKm3gCT0F1Tlp3d8aufYRksaXGZhs8Ow==
"@unocss/preset-mini@0.65.3":
version "0.65.3"
resolved "https://registry.yarnpkg.com/@unocss/preset-mini/-/preset-mini-0.65.3.tgz#e72c6918af329875dbd102534ebfe31f9b465a23"
integrity sha512-HG7mRfq0S2VKkw40duumoyIYaMBQGW1Uxb+Kw8HLGvoamnDmOZKb+TOXxys17Z5Z0vloi2CN1qqyJhYC0G6MSg==
dependencies:
"@unocss/core" "0.61.5"
"@unocss/extractor-arbitrary-variants" "0.61.5"
"@unocss/rule-utils" "0.61.5"
"@unocss/core" "0.65.3"
"@unocss/extractor-arbitrary-variants" "0.65.3"
"@unocss/rule-utils" "0.65.3"
"@unocss/preset-uno@^0.61.5":
version "0.61.5"
resolved "https://registry.yarnpkg.com/@unocss/preset-uno/-/preset-uno-0.61.5.tgz#80c85edaf4ed364c91df3400dae5abfe3976f21e"
integrity sha512-CflB0l9CeZx+b/Q8mA4Ow4d63Caf+vFJ+1EGA06jG9qYjPLy76Rkci//0m9cEtO+vPnYtgLc7HZAZv0X6wh4Tg==
"@unocss/preset-uno@^0.65.3":
version "0.65.3"
resolved "https://registry.yarnpkg.com/@unocss/preset-uno/-/preset-uno-0.65.3.tgz#89634394fefa602aec950d1d4e43286e7c3afc09"
integrity sha512-1O9qVAG/W7t4X9VExuUPGGy+4n8yxfpuQ3NeFgXlEkT1Mi3cokS0Eb0quvttgLGbjQ2waoS4MWbGyMmDGHWnYQ==
dependencies:
"@unocss/core" "0.61.5"
"@unocss/preset-mini" "0.61.5"
"@unocss/preset-wind" "0.61.5"
"@unocss/rule-utils" "0.61.5"
"@unocss/core" "0.65.3"
"@unocss/preset-mini" "0.65.3"
"@unocss/preset-wind" "0.65.3"
"@unocss/rule-utils" "0.65.3"
"@unocss/preset-wind@0.61.5":
version "0.61.5"
resolved "https://registry.yarnpkg.com/@unocss/preset-wind/-/preset-wind-0.61.5.tgz#049f4cf3d15be5d5bf1bb3c2108cff22a69d0884"
integrity sha512-n4uepxv3gVoVQb0tv7iV8M4W0CgwLw0QaMX+3ECYzFLMynjCkZmFDtdQAX720yTvLZxwCxEZfQCgydOSt0qjZA==
"@unocss/preset-wind@0.65.3":
version "0.65.3"
resolved "https://registry.yarnpkg.com/@unocss/preset-wind/-/preset-wind-0.65.3.tgz#0ea857ee19b7a4ca3654bb3fed57c3e1c28ec401"
integrity sha512-esptoeJEN1QZEXwMIU3OXumSi3TEbIXZg1SuuUYqOWXzldxANsfXSMdHtsiXUSMNwNsfmQl4XfBlGNYYK/7eyg==
dependencies:
"@unocss/core" "0.61.5"
"@unocss/preset-mini" "0.61.5"
"@unocss/rule-utils" "0.61.5"
"@unocss/core" "0.65.3"
"@unocss/preset-mini" "0.65.3"
"@unocss/rule-utils" "0.65.3"
"@unocss/rule-utils@0.61.5":
version "0.61.5"
@ -2568,6 +2573,14 @@
"@unocss/core" "^0.61.5"
magic-string "^0.30.10"
"@unocss/rule-utils@0.65.3":
version "0.65.3"
resolved "https://registry.yarnpkg.com/@unocss/rule-utils/-/rule-utils-0.65.3.tgz#dd00ca5469a693d3c4b3554d38e9c4a195ea577d"
integrity sha512-jndyth0X11FbvIDForYq90b+N5xsR31FRsmvp7AC7dcW71clemUEDHCwqzSJn8cVFwahgvlwWbEoYHPEgQrtIQ==
dependencies:
"@unocss/core" "^0.65.3"
magic-string "^0.30.17"
"@unocss/transformer-compile-class@^0.61.5":
version "0.61.5"
resolved "https://registry.yarnpkg.com/@unocss/transformer-compile-class/-/transformer-compile-class-0.61.5.tgz#be992b1a9e2300314618b1866854f1f6ea95419c"
@ -2639,6 +2652,26 @@ acorn@^8.9.0:
resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
ag-charts-types@11.0.3:
version "11.0.3"
resolved "https://registry.yarnpkg.com/ag-charts-types/-/ag-charts-types-11.0.3.tgz#a4aa4825b298014c1323a1e5832dec0ef4e5e135"
integrity sha512-q7O2viQXPyO014QVH7KjFCUmQ/NvMBx9ReZtramQ44u+/aAa0ttLi/coK+V0Hse4/MG1eB/2XhgymdooQ0Kalg==
ag-grid-community@33.0.3:
version "33.0.3"
resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-33.0.3.tgz#51511d5b048eedf24b3de596967e74a3272bfe88"
integrity sha512-HZeVmVieZ5Gm9j09Itecqm+OIX8X6cU4TJToUbsXv3DxdO9SK5/s8aAJAwBHtNRXOhHrhqQYwrY2yc3OtzWwcQ==
dependencies:
ag-charts-types "11.0.3"
ag-grid-react@^33.0.3:
version "33.0.3"
resolved "https://registry.yarnpkg.com/ag-grid-react/-/ag-grid-react-33.0.3.tgz#24f0c65ba48363e53db915aab13bb537b8e35025"
integrity sha512-6IcraSVqsUG/hzTeZ0D0dtddAcZKcWdN75ek/O+lCA6r22abJPe33nHBYVezkTV8k6D3JhA24mlTwduzn0GZLQ==
dependencies:
ag-grid-community "33.0.3"
prop-types "^15.8.1"
ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
@ -5312,6 +5345,13 @@ magic-string@^0.30.10:
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
magic-string@^0.30.17:
version "0.30.17"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
mangle-css-class-webpack-plugin@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/mangle-css-class-webpack-plugin/-/mangle-css-class-webpack-plugin-5.1.0.tgz#42008a8fbe0257f491968796320eb94b17a36321"