new release: 0.6

This commit is contained in:
dvelo 2024-08-03 09:51:45 -05:00
parent 473332cf69
commit 724c301a70
42 changed files with 1043 additions and 380 deletions

@ -25,7 +25,7 @@ Clone the repo!
First, you must supply the following services with API keys: First, you must supply the following services with API keys:
- [Clerk](https://clerk.com): Create an app and put the respective keys in `.env.local`. Also, add `IS_AUTH=true`. - [Clerk](https://clerk.com): Create an app and put the respective keys in `.env.local`
- MongoDB: Create a database, can be anywhere, and put the location to connect in `.env.local` for the key `MONGO_DB` (this isn't required by any means, but if you want to store any short term or historical data, use this.) - MongoDB: Create a database, can be anywhere, and put the location to connect in `.env.local` for the key `MONGO_DB` (this isn't required by any means, but if you want to store any short term or historical data, use this.)
- Inngest: Inngest is a smaller library, but runs the `cron` jobs which will make servers automaticly get added to the database. - Inngest: Inngest is a smaller library, but runs the `cron` jobs which will make servers automaticly get added to the database.

@ -29,6 +29,7 @@
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@types/nprogress": "^0.2.3",
"@types/react-twemoji": "^0.4.3", "@types/react-twemoji": "^0.4.3",
"@unocss/eslint-plugin": "^0.61.5", "@unocss/eslint-plugin": "^0.61.5",
"@unocss/postcss": "^0.61.5", "@unocss/postcss": "^0.61.5",
@ -47,6 +48,8 @@
"next": "14.2.3", "next": "14.2.3",
"next-css-obfuscator": "^2.2.16", "next-css-obfuscator": "^2.2.16",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"nextjs-toploader": "^1.6.12",
"nprogress": "^0.2.0",
"postcss-obfuscator": "^1.6.1", "postcss-obfuscator": "^1.6.1",
"prettier": "^3.3.1", "prettier": "^3.3.1",
"react": "^18", "react": "^18",

@ -1,4 +1,4 @@
import { OnlineServer, ServerResponse } from "./components/ServerView"; import { OnlineServer, ServerResponse } from "./lib/types/server";
const serverCache: any = {}; const serverCache: any = {};
@ -262,7 +262,7 @@ export var allCategories: Array<{
async function requestServer(s: OnlineServer): Promise<ServerResponse> { async function requestServer(s: OnlineServer): Promise<ServerResponse> {
if (serverCache[s.name] == undefined) { if (serverCache[s.name] == undefined) {
const re = await fetch( const re = await fetch(
"https://api.minehut.com/server/" + s.name + "?byName=true", "https://api.minehut.com/server/" + s.name + "?byName=true"
); );
const json = await re.json(); const json = await re.json();
serverCache[s.name] = json.server; serverCache[s.name] = json.server;

@ -95,6 +95,11 @@
} }
} }
.backdrop-blur {
-webkit-backdrop-filter: blur(8px)!important;
backdrop-filter: blur(8px)!important;
}
/* width */ /* width */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;

@ -1,6 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { GeistSans } from "geist/font/sans"; import { GeistSans } from "geist/font/sans";
import { Github, CodeXml, Server } from "lucide-react"; import { Github, CodeXml, Server, Command } from "lucide-react";
import "./globals.css"; import "./globals.css";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
@ -12,6 +12,7 @@ import { ThemeProvider } from "@/components/ThemeProvider";
import Image from "next/image"; import Image from "next/image";
import { ClerkThemeProvider } from "@/components/clerk/ClerkThemeProvider"; import { ClerkThemeProvider } from "@/components/clerk/ClerkThemeProvider";
import { useEffectOnce } from "@/lib/useEffectOnce"; import { useEffectOnce } from "@/lib/useEffectOnce";
import NextTopLoader from '@/lib/top-loader';
import { banner } from "@/banner"; import { banner } from "@/banner";
import { import {
Breadcrumb, Breadcrumb,
@ -24,6 +25,12 @@ import Link from "next/link";
import TopBar from "@/components/clerk/Topbar"; import TopBar from "@/components/clerk/Topbar";
import TextFromPathname from "@/components/TextFromPathname"; import TextFromPathname from "@/components/TextFromPathname";
import { Inter as interFont } from "next/font/google"; import { Inter as interFont } from "next/font/google";
import {
CommandBar,
CommandBarer,
SearchCommandBar,
SubLinkCommandBar,
} from "@/components/CommandBar";
const inter = interFont({ variable: "--font-inter", subsets: ["latin"] }); const inter = interFont({ variable: "--font-inter", subsets: ["latin"] });
export default async function RootLayout({ export default async function RootLayout({
@ -40,6 +47,7 @@ export default async function RootLayout({
disableTransitionOnChange disableTransitionOnChange
> >
<TooltipProvider> <TooltipProvider>
{banner.isBanner && ( {banner.isBanner && (
<div className="bg-orange-600 w-screen h-8 border-b fixed text-black flex items-center text-center font-medium pl-2"> <div className="bg-orange-600 w-screen h-8 border-b fixed text-black flex items-center text-center font-medium pl-2">
{banner.bannerText} {banner.bannerText}
@ -65,8 +73,9 @@ export default async function RootLayout({
</div> </div>
<TopBar inter={inter.className} /> <TopBar inter={inter.className} />
</div> </div>
<div>{children}</div>{" "} <div><NextTopLoader/>{children}</div>{" "}
<Toaster position="bottom-center" reverseOrder={false} /> <Toaster position="bottom-center" reverseOrder={false} />
<CommandBarer />
</TooltipProvider> </TooltipProvider>
</ThemeProvider> </ThemeProvider>
</ClerkThemeProvider> </ClerkThemeProvider>

@ -0,0 +1,335 @@
"use client";
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/ui/command";
import { TagShower } from "./ServerList";
import { useEffect, useState } from "react";
import { OnlineServer } from "@/lib/types/server";
import events from "@/lib/commandEvent";
import { useHotkeys } from "react-hotkeys-hook";
import Link from "next/link";
import {
ArrowDown01,
ArrowLeft,
CommandIcon,
LinkIcon,
Server,
Settings,
Star,
} from "lucide-react";
import { useEffectOnce } from "@/lib/useEffectOnce";
import { useClerk, useUser } from "@clerk/nextjs";
import { useRouter } from '@/lib/useRouter'
export function SearchCommandBar() {
const [serverList, setServerList] = useState<OnlineServer[]>([]);
const [open, setOpen] = useState(false);
const [backEnabled, setBackEnabled] = useState(false);
const [searchRes, setSearchRes] = useState<any>(undefined);
const router = useRouter()
useHotkeys("mod+shift+k", () => setOpen(true), []);
useEffectOnce(() => {
events.on("search-request-event", () => {
setOpen(true);
});
events.on("search-request-event-back", () => {
setOpen(true);
setBackEnabled(true);
});
fetch("https://api.minehut.com/servers").then((c) =>
c.json().then((b: { servers: OnlineServer[] }) => {
setServerList(b.servers.slice(0, 20));
})
);
});
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput
placeholder="Search for a server (offline or online)"
onValueChange={(c) => {
fetch("https://api.minehut.com/server/" + c + "?byName=true").then(
(l) => {
if (l.ok) {
console.log("found!");
l.json().then((m: any) => {
setSearchRes(m.server);
console.log(searchRes);
});
} else {
setSearchRes(undefined);
}
}
);
}}
/>
<CommandList>
<CommandGroup heading="">
<CommandItem
onSelect={() => {
setOpen(false);
if (backEnabled) events.emit("cmd-event");
setBackEnabled(false);
}}
>
<ArrowLeft className="mr-2 h-4 w-4" />
<span>Go back</span>
</CommandItem>
</CommandGroup>
<CommandEmpty>
No results found. (Minehut deleted legacy servers)
</CommandEmpty>
{searchRes == undefined ? (
""
) : (
<CommandGroup heading="Search Results">
<CommandItem
onSelect={() => {
router.push("/server/" + searchRes.name);
}}
>
<div className="block">
<span className="font-medium">{searchRes.name}</span> <br />
<code className="text-gray-500 text-[14px]">
{searchRes.joins} total joins {" "}
{searchRes.online ? "Online" : "Offline"}
</code>
</div>
</CommandItem>
</CommandGroup>
)}
<CommandSeparator />
<CommandGroup heading="Popular Servers">
{serverList.map((b: OnlineServer) => (
<CommandItem
key={b.name}
onSelect={() => {
router.push("/server/" + b.name);
}}
>
<div className="block">
<span className="font-medium">{b.name}</span> <br />
<code className="text-gray-500 text-[14px]">
<TagShower server={b} />
</code>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
export function CommandBar() {
const [open, setOpen] = useState(false);
const clerk = useClerk();
const { user } = useUser();
useHotkeys("mod+k", () => setOpen(true), []);
useEffectOnce(() => {
events.on("cmd-event", () => {
setOpen(true);
});
});
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem
onSelect={() => {
setOpen(false);
events.emit("search-request-event-back");
}}
>
<Server className="mr-2 h-4 w-4" />
<span>Servers</span>
<CommandShortcut className="flex items-center">
<CommandIcon size={10} />
+Shift+K
</CommandShortcut>
</CommandItem>
<CommandItem disabled>
<ArrowDown01 className="mr-2 h-4 w-4" />
<span>
Sort Servers - <i>coming soon</i>
</span>
</CommandItem>
<CommandItem
onSelect={() => {
setOpen(false);
events.emit("cmd-event-link");
}}
>
<LinkIcon className="mr-2 h-4 w-4" />
<span>Links</span>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Profile">
<CommandItem onSelect={() => events.emit("cmd-event-favorites")}>
<Star className="mr-2 h-4 w-4" />
<span>Favorites</span>
</CommandItem>
<CommandItem
onSelect={() => {
setOpen(false);
try {
clerk.openUserProfile();
} catch {
clerk.openSignIn();
}
}}
>
<Settings className="mr-2 h-4 w-4" />
<span>User Settings</span>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
export function SubLinkCommandBar() {
const [open, setOpen] = useState(false);
useEffectOnce(() => {
events.on("cmd-event-link", () => {
setOpen(true);
});
});
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandItem
onSelect={() => {
setOpen(false);
events.emit("cmd-event");
}}
>
<ArrowLeft className="mr-2 h-4 w-4" />
<span>Go back</span>
</CommandItem>
<CommandGroup heading="Suggestions">
<CommandItem
onSelect={() => {
window
.open("https://github.com/DeveloLongScript/MHSF", "_blank")
?.focus();
}}
>
<Github className="mr-2 h-4 w-4" />
<span>GitHub</span>
</CommandItem>
<CommandItem
onSelect={() => {
window.open("https://mhsf.betteruptime.com", "_blank")?.focus();
}}
>
<ArrowDown01 className="mr-2 h-4 w-4" />
<span>Status Page</span>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
import * as React from "react";
import type { SVGProps } from "react";
import { getAccountFavorites } from "@/lib/api";
const Github = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 256 250"
width="1em"
height="1em"
fill="#24292f"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
{...props}
>
<path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" />
</svg>
);
export function FavoriteBar() {
const [isOpen, setOpen] = useState(false);
const [favorites, setFavorites] = useState<Array<string> | undefined>(
undefined
);
const clerk = useClerk();
const router = useRouter()
useEffectOnce(() => {
events.on("cmd-event-favorites", () => setOpen(true));
getAccountFavorites().then((c) => setFavorites(c));
});
return (
<CommandDialog open={isOpen} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="">
<CommandItem
onSelect={() => {
setOpen(false);
events.emit("cmd-event");
}}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Go back
</CommandItem>
</CommandGroup>
<CommandGroup heading="Favorites">
{favorites == undefined && (
<CommandItem onSelect={() => clerk.openSignIn()}>
Login to see favorites
</CommandItem>
)}
{favorites != undefined && (
<>
{favorites.map((c) => (
<CommandItem
key={c}
onSelect={() => {
router.push("/server/" + c);
}}
>
{c}
</CommandItem>
))}
</>
)}
</CommandGroup>
</CommandList>
</CommandDialog>
);
}
export function CommandBarer() {
return (
<>
<FavoriteBar />
<SubLinkCommandBar />
<CommandBar />
<SearchCommandBar />
</>
);
}

@ -2,37 +2,38 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Spinner } from "./ui/spinner"; import { Spinner } from "./ui/spinner";
import { Card, CardHeader, CardTitle } from "./ui/card"; import { Card, CardHeader, CardTitle } from "./ui/card";
import { ServerResponse } from "./ServerView"; import { ServerResponse } from "@/lib/types/server";
import { useEffectOnce } from "@/lib/useEffectOnce"; import { useEffectOnce } from "@/lib/useEffectOnce";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Copy, Layers, X, XIcon } from "lucide-react"; import { Copy, Layers, XIcon } from "lucide-react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { getAccountFavorites } from "@/lib/api";
import { useRouter } from '@/lib/useRouter'
export default function FavoritesView() { export default function FavoritesView() {
const [apiFavorites, setApiFavorites] = useState<any>([]); const [apiFavorites, setApiFavorites] = useState<any>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const router = useRouter()
useEffectOnce(() => { useEffectOnce(() => {
fetch("/api/favorites/getAllFavorites").then((c) => { getAccountFavorites().then((d) => {
c.json().then((d) => { let num = 0;
let num = 0; d.forEach((a: any, i: number) => {
d.result.forEach((a: any, i: number) => { fetch("https://api.minehut.com/server/" + a + "?byName=true").then(
fetch("https://api.minehut.com/server/" + a + "?byName=true").then( (b) =>
(b) => b.json().then((c) => {
b.json().then((c) => { num++;
num++; var apiClone = apiFavorites;
var apiClone = apiFavorites; apiClone.push(c.server);
apiClone.push(c.server); setApiFavorites(apiClone);
setApiFavorites(apiClone); if (num == d.length) {
if (num == d.result.length) { setLoading(false);
setLoading(false); }
} })
}) );
);
});
if (d.result.length == 0) setLoading(false);
}); });
if (d.length == 0) setLoading(false);
}); });
}); });
@ -80,7 +81,7 @@ export default function FavoritesView() {
variant="secondary" variant="secondary"
className=" w-[32px] h-[32px] mb-2 ml-2 max-md:hidden" className=" w-[32px] h-[32px] mb-2 ml-2 max-md:hidden"
onClick={() => { onClick={() => {
window.location.href = "/server/" + server.name; router.push("/server/" + server.name);
}} }}
> >
<Layers size={18} /> <Layers size={18} />

@ -17,7 +17,8 @@ import {
ChartTooltipContent, ChartTooltipContent,
} from "@/components/ui/chart"; } from "@/components/ui/chart";
import { useEffectOnce } from "@/lib/useEffectOnce"; import { useEffectOnce } from "@/lib/useEffectOnce";
import { ServerResponse } from "./ServerView"; import { ServerResponse } from "@/lib/types/server";
import { getCommunityServerFavorites, getShortTermData } from "@/lib/api";
const chartConfig = { const chartConfig = {
player_count: { player_count: {
@ -40,25 +41,10 @@ export function NewChart({ server }: { server: string }) {
const allNums = { player_count: joins, favorites }; const allNums = { player_count: joins, favorites };
useEffectOnce(() => { useEffectOnce(() => {
fetch("/api/history/getShortTermData", { getShortTermData(server, ["player_count", "favorites", "time"]).then(
headers: { "Content-Type": "application/json" }, (c) => {
body: JSON.stringify({ setChartData(c);
scopes: ["player_count", "favorites", "time"], getCommunityServerFavorites(server).then((b) => setFavorites(b));
server,
}),
method: "POST",
}).then((c) => {
c.json().then((b) => {
setChartData(b.data);
fetch("/api/favorites/getCommunityNum", {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ server }),
method: "POST",
}).then((b) =>
b.json().then((f) => {
setFavorites(f.result);
})
);
fetch("https://api.minehut.com/server/" + server + "?byName=true").then( fetch("https://api.minehut.com/server/" + server + "?byName=true").then(
(k) => { (k) => {
k.json().then((p: { server: ServerResponse }) => { k.json().then((p: { server: ServerResponse }) => {
@ -66,8 +52,8 @@ export function NewChart({ server }: { server: string }) {
}); });
} }
); );
}); }
}); );
}); });
return ( return (

@ -15,7 +15,7 @@ import {
} from "./ui/card"; } from "./ui/card";
import IconDisplay from "./IconDisplay"; import IconDisplay from "./IconDisplay";
import { TagShower } from "./ServerList"; import { TagShower } from "./ServerList";
import { Copy, EllipsisVertical, Layers, MoveRight } from "lucide-react"; import { Copy, EllipsisVertical, Layers, MoveRight, Router } from "lucide-react";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { import {
Drawer, Drawer,
@ -30,8 +30,11 @@ import {
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Tooltip } from "@radix-ui/react-tooltip"; import { Tooltip } from "@radix-ui/react-tooltip";
import { TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { useRouter } from '@/lib/useRouter'
import Link from "next/link";
export default function ServerCard({ b, motd }: any) { export default function ServerCard({ b, motd }: any) {
const router = useRouter()
return ( return (
<ContextMenu> <ContextMenu>
<ContextMenuTrigger> <ContextMenuTrigger>
@ -72,7 +75,7 @@ export default function ServerCard({ b, motd }: any) {
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
window.location.href = "/server/" + b.name; router.push("/server/" + b.name);
}} }}
> >
Open server page Open server page
@ -138,16 +141,15 @@ export default function ServerCard({ b, motd }: any) {
</Button> </Button>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<Link href={"/server/" + b.name}>
<Button <Button
size="icon" size="icon"
variant="secondary" variant="secondary"
className=" w-[32px] h-[32px] mt-2 ml-2 max-md:hidden" className=" w-[32px] h-[32px] mt-2 ml-2 max-md:hidden"
onClick={() => {
window.location.href = "/server/" + b.name;
}}
> >
<Layers size={18} /> <Layers size={18} />
</Button> </Button>
</Link>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
Open up the server page to see more information about Open up the server page to see more information about
@ -171,20 +173,19 @@ export default function ServerCard({ b, motd }: any) {
</div> </div>
</ContextMenuItem> </ContextMenuItem>
<ContextMenuSeparator /> <ContextMenuSeparator />
<Link href={"/server/" + b.name}>
<ContextMenuItem <ContextMenuItem
onClick={() => {
window.location.href = "/server/" + b.name;
}}
> >
Open server page Open server page
</ContextMenuItem> </ContextMenuItem></Link>
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
</CardDescription> </CardDescription>
<CardContent> <CardContent>
{b.name != "Skylegendz" && (
<span dangerouslySetInnerHTML={{ __html: motd }} /> <span dangerouslySetInnerHTML={{ __html: motd }} className="max-w-[12px] text-center"/>
)}
</CardContent> </CardContent>
</CardHeader> </CardHeader>
</Card> </Card>
@ -204,7 +205,7 @@ export default function ServerCard({ b, motd }: any) {
<ContextMenuSeparator /> <ContextMenuSeparator />
<ContextMenuItem <ContextMenuItem
onClick={() => { onClick={() => {
window.location.href = "/server/" + b.name; router.push("/server/" + b.name);
}} }}
> >
Open server page Open server page

@ -79,24 +79,25 @@ import {
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Spinner } from "./ui/spinner"; import { Spinner } from "./ui/spinner";
import { CommandIcon } from "lucide-react";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { OnlineServer, ServerResponse } from "./ServerView"; import { OnlineServer, ServerResponse } from "@/lib/types/server";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { useEffectOnce } from "@/lib/useEffectOnce"; import { useEffectOnce } from "@/lib/useEffectOnce";
import ServerCard from "./ServerCard"; import ServerCard from "./ServerCard";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import events from "@/lib/commandEvent";
import { BorderBeam } from "@/components/effects/border-beam";
export default function ServerList() { export default function ServerList() {
const [loading, setLoading]: any = useState(true); const [loading, setLoading]: any = useState(true);
const [command, setCommand] = useState(false);
const [randomText, setRandomText] = useState(""); const [randomText, setRandomText] = useState("");
const [motdList, setMotdList] = useState<any>({}); const [motdList, setMotdList] = useState<any>({});
const allText = [""]; const allText = [""];
const getRandomText = () => { const getRandomText = () => {
return allText[Math.floor(Math.random() * allText.length)]; return allText[Math.floor(Math.random() * allText.length)];
}; };
const [searchRes, setSearchRes] = useState<any>(undefined);
const [templateFilter, setTemplateFilter] = useState(false); const [templateFilter, setTemplateFilter] = useState(false);
const [random, setRandom] = useState(false); const [random, setRandom] = useState(false);
const [serverList, setServerList] = useState(new ServersList([])); const [serverList, setServerList] = useState(new ServersList([]));
@ -108,14 +109,13 @@ export default function ServerList() {
server.playerData.playerCount < 15 && server.playerData.playerCount < 15 &&
server.playerData.playerCount > 7; server.playerData.playerCount > 7;
const [nameFilters, setNameFilters] = useState<any>({}); const [nameFilters, setNameFilters] = useState<any>({});
useHotkeys("ctrl+k", () => setCommand(true), []);
const [inErrState, setErrState] = useState(false); const [inErrState, setErrState] = useState(false);
const [servers, setServers] = useState<Array<OnlineServer>>([]); const [servers, setServers] = useState<Array<OnlineServer>>([]);
const [filters, setFilters] = useState< const [filters, setFilters] = useState<
Array<(server: OnlineServer) => Promise<boolean>> Array<(server: OnlineServer) => Promise<boolean>>
>([]); >([]);
const [randomData, setRandomData] = useState<OnlineServer | undefined>( const [randomData, setRandomData] = useState<OnlineServer | undefined>(
undefined, undefined
); );
useEffectOnce(() => { useEffectOnce(() => {
@ -175,9 +175,10 @@ export default function ServerList() {
<div className=" max-lg:grid-cols-2 grid grid-cols-3 gap-4 "> <div className=" max-lg:grid-cols-2 grid grid-cols-3 gap-4 ">
<Stat <Stat
title="Players online" title="Players online"
className="relative"
desc={serverList.getExtraData().total_players.toString()} desc={serverList.getExtraData().total_players.toString()}
icon={CircleUser} icon={CircleUser}
/> ><BorderBeam size={135} duration={12} delay={9}/></Stat>
<Stat <Stat
title="Servers online" title="Servers online"
desc={serverList.getExtraData().total_servers.toString()} desc={serverList.getExtraData().total_servers.toString()}
@ -203,11 +204,15 @@ export default function ServerList() {
<Separator /> <Separator />
<div className=" mt-3 ml-3"> <div className=" mt-3 ml-3">
<Button <Button
onClick={() => setCommand(true)} onClick={() => events.emit("search-request-event")}
variant="secondary" variant="secondary"
className=" max-lg:mb-3" className=" max-lg:mb-3"
> >
Search <code className="ml-2">Ctrl+K</code> Search{" "}
<code className="ml-2 flex items-center">
<CommandIcon size={14}/>
+Shift+K
</code>
</Button> </Button>
<Popover> <Popover>
<PopoverTrigger> <PopoverTrigger>
@ -338,7 +343,7 @@ export default function ServerList() {
error: "Error while changing filters", error: "Error while changing filters",
loading: "Changing filters...", loading: "Changing filters...",
success: "Changed filters!", success: "Changed filters!",
}, }
); );
}} }}
defaultValue={(() => { defaultValue={(() => {
@ -571,7 +576,7 @@ export default function ServerList() {
success: "Succesfully refreshed servers", success: "Succesfully refreshed servers",
loading: "Refreshing...", loading: "Refreshing...",
error: "Error while refreshing", error: "Error while refreshing",
}, }
); );
}} }}
> >
@ -636,7 +641,7 @@ export default function ServerList() {
onClick={() => { onClick={() => {
setTextCopied(true); setTextCopied(true);
navigator.clipboard.writeText( navigator.clipboard.writeText(
randomData.name + ".mshf.minehut.gg", randomData.name + ".mshf.minehut.gg"
); );
toast.success("Copied!"); toast.success("Copied!");
setTimeout(() => setTextCopied(false), 1000); setTimeout(() => setTextCopied(false), 1000);
@ -655,68 +660,6 @@ export default function ServerList() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>
<CommandDialog open={command} onOpenChange={setCommand}>
<CommandInput
placeholder="Search for a server (offline or online)"
onValueChange={(c) => {
fetch("https://api.minehut.com/server/" + c + "?byName=true").then(
(l) => {
if (l.ok) {
console.log("found!");
l.json().then((m: any) => {
setSearchRes(m.server);
console.log(searchRes);
});
} else {
setSearchRes(undefined);
}
},
);
}}
/>
<CommandList>
<CommandEmpty>
No results found. (Minehut deleted legacy servers)
</CommandEmpty>
{searchRes == undefined ? (
""
) : (
<CommandGroup heading="Search Results">
<CommandItem
onSelect={() => {
window.location.replace("/server/" + searchRes.name);
}}
>
<div className="block">
<span className="font-medium">{searchRes.name}</span> <br />
<code className="text-gray-500 text-[14px]">
{searchRes.joins} total joins {" "}
{searchRes.online ? "Online" : "Offline"}
</code>
</div>
</CommandItem>
</CommandGroup>
)}
<CommandGroup heading="Popular Servers">
{serverList.currentServers.map((b: OnlineServer) => (
<CommandItem
key={b.name}
onSelect={() => {
window.location.replace("/server/" + b.name);
}}
>
<div className="block">
<span className="font-medium">{b.name}</span> <br />
<code className="text-gray-500 text-[14px]">
<TagShower server={b} />
</code>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
<br /> <br />
<InfiniteScroll <InfiniteScroll
dataLength={serverList.currentServers.length} dataLength={serverList.currentServers.length}

@ -38,11 +38,14 @@ import toast from "react-hot-toast";
import { SignedIn, SignedOut, SignInButton } from "@clerk/nextjs"; import { SignedIn, SignedOut, SignInButton } from "@clerk/nextjs";
import SignInPopoverButton from "./clerk/SignInPopoverButton"; import SignInPopoverButton from "./clerk/SignInPopoverButton";
import { Sparkle, Star, X } from "lucide-react"; import { Sparkle, Star, X } from "lucide-react";
import { favoriteServer, isFavorited } from "@/lib/api";
import { LoadingButton } from "./ui/loading-button";
export default function ServerView(props: { server: string }) { export default function ServerView(props: { server: string }) {
const [single, setSingle] = useState(new ServerSingle(props.server)); const [single, setSingle] = useState(new ServerSingle(props.server));
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [favorited, setFavorited] = useState(false); const [favorited, setFavorited] = useState(false);
const [loadingFavorite, setLoadingFavorite] = useState(false);
const [randomText, setRandomText] = useState(""); const [randomText, setRandomText] = useState("");
const [lastOnline, setLastOnline] = useState(0); const [lastOnline, setLastOnline] = useState(0);
const [format, setFormat] = useState(""); const [format, setFormat] = useState("");
@ -54,22 +57,14 @@ export default function ServerView(props: { server: string }) {
useEffect(() => { useEffect(() => {
setRandomText(getRandomText()); setRandomText(getRandomText());
single.init().then(() => { single.init().then(() => {
fetch("/api/favorites/isFavorited", { isFavorited(single.grabOffline()?.name as string)
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
server: single.grabOffline()?.name,
}),
})
.then((b) => { .then((b) => {
b.json().then((c) => { setFavorited(b);
setFavorited(c.result); setLoading(false);
setLoading(false); var online = single.grabOffline()?.last_online;
var online = single.grabOffline()?.last_online; if (online != undefined) {
if (online != undefined) { setLastOnline(online);
setLastOnline(online); }
}
});
}) })
.catch(() => { .catch(() => {
setLoading(false); setLoading(false);
@ -208,42 +203,46 @@ export default function ServerView(props: { server: string }) {
<SignInPopoverButton /> <SignInPopoverButton />
</SignedOut> </SignedOut>
<SignedIn> <SignedIn>
<Button {loadingFavorite && (
variant="outline" <LoadingButton variant="outline">Favorite Server</LoadingButton>
onClick={() => { )}
fetch("/api/favorites/favoriteServer", { {!loadingFavorite && (
headers: { "Content-Type": "application/json" }, <Button
body: JSON.stringify({ variant="outline"
server: single.grabOffline()?.name, onClick={() => {
}), setLoadingFavorite(true);
method: "POST", favoriteServer(single.grabOffline()?.name as string).then(
}).then(() => {}); () => {
setFavorited(!favorited); setFavorited(!favorited);
}} setLoadingFavorite(false);
> }
{favorited && ( );
<motion.div }}
animate={{ color: "yellow", fill: "yellow" }} >
transition={{ duration: 2 }} {favorited && (
> <motion.div
<Star animate={{ color: "yellow", fill: "yellow" }}
className="mr-2" transition={{ duration: 2 }}
size="16" >
color="yellow" <Star
fill="yellow" className="mr-2"
/> size="16"
</motion.div> color="yellow"
)} fill="yellow"
{!favorited && ( />
<motion.div </motion.div>
transition={{ duration: 1 }} )}
animate={{ color: "yellow", fill: "yellow" }} {!favorited && (
> <motion.div
<Star className="mr-2" size="16" /> transition={{ duration: 1 }}
</motion.div> animate={{ color: "yellow", fill: "yellow" }}
)} >
Favorite Server <Star className="mr-2" size="16" />
</Button> </motion.div>
)}
{favorited && "Unf"}{!favorited && "F"}avorite Server
</Button>
)}
</SignedIn> </SignedIn>
</CardContent> </CardContent>
<CardFooter> <CardFooter>
@ -269,85 +268,3 @@ function timeConverter(UNIX_timestamp: any) {
var time = month + "/" + date + "/" + year; var time = month + "/" + date + "/" + year;
return time; return time;
} }
export interface ServerResponse {
__unix?: string;
deletion?: Deletion;
_id: string;
categories: string[];
inheritedCategories: any[];
purchased_icons: string[];
backup_slots: number;
suspended: boolean;
server_version_type: string;
proxy: boolean;
connectedServers: any[];
motd: string;
visibility: boolean;
server_plan: string;
storage_node: string;
default_banner_image: string;
default_banner_tint: string;
owner: string;
name: string;
name_lower: string;
creation: number;
platform: string;
credits_per_day: number;
in_game: boolean;
using_cosmetics: boolean;
__v: number;
port: number;
last_online: number;
joins: number;
active_icon: string;
expired: boolean;
icon: string;
online: boolean;
maxPlayers: number;
playerCount: number;
rawPlan: string;
activeServerPlan: string;
}
export interface Deletion {
started: boolean;
started_at: number;
reason: string;
completed: boolean;
completed_at: number;
storage_completed: boolean;
storage_completed_at: number;
}
export interface OnlineServer {
staticInfo: StaticInfo;
maxPlayers: number;
name: string;
motd: string;
icon: string;
playerData: PlayerData;
connectable: boolean;
visibility: boolean;
allCategories: string[];
usingCosmetics: boolean;
author?: string;
authorRank: string;
}
export interface StaticInfo {
_id: string;
serverPlan: string;
serviceStartDate: number;
platform: string;
planMaxPlayers: number;
planRam: number;
alwaysOnline: boolean;
rawPlan: string;
connectedServers: any[];
}
export interface PlayerData {
playerCount: number;
timeNoPlayers: number;
}

@ -1,14 +1,17 @@
import { DollarSign } from "lucide-react"; import { DollarSign } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Children } from "react";
export default function Component(props: { export default function Component(props: {
title: string; title: string;
desc: string | JSX.Element; desc: string | JSX.Element;
icon: any; icon: any;
className?: string; className?: string;
children?: any;
}) { }) {
return ( return (
<Card className={props.className}> <Card className={props.className}>
{props.children}
<CardHeader className=" flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className=" flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className=" text-sm font-medium m-0"> <CardTitle className=" text-sm font-medium m-0">
{props.title} {props.title}

@ -1,9 +1,11 @@
import { useClerk, useUser } from "@clerk/nextjs"; import { useClerk, useUser } from "@clerk/nextjs";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Star, UserCog, X } from "lucide-react"; import { Star, UserCog, X } from "lucide-react";
import { useRouter } from '@/lib/useRouter'
export default function LoggedInPopover() { export default function LoggedInPopover() {
const clerk = useClerk(); const clerk = useClerk();
const router = useRouter()
const { user } = useUser(); const { user } = useUser();
return ( return (
@ -19,7 +21,7 @@ export default function LoggedInPopover() {
</Button> </Button>
<Button <Button
variant={"ghost"} variant={"ghost"}
onClick={() => window.location.replace("/account/favorites")} onClick={() => router.push("/account/favorites")}
> >
<Star size={18} className=" mr-2" /> Favorites <Star size={18} className=" mr-2" /> Favorites
</Button> </Button>

@ -11,15 +11,17 @@ import { SignIn, useClerk } from "@clerk/nextjs";
export default function SignInPopoverButton({ export default function SignInPopoverButton({
className, className,
variant
}: { }: {
className?: string; className?: string;
variant?: "default" | "destructive" | "secondary" | "outline" | "ghost" | "link";
}) { }) {
const clerk = useClerk(); const clerk = useClerk();
return ( return (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button className={className}>Sign In</Button> <Button className={className} variant={variant}>Sign In</Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full"> <PopoverContent className="w-full">
<div className=" grid w-[200px]"> <div className=" grid w-[200px]">

@ -14,28 +14,24 @@ import InfoPopover from "../misc/InfoPopover";
import Link from "next/link"; import Link from "next/link";
export default function TopBar({ inter }: { inter: string }) { export default function TopBar({ inter }: { inter: string }) {
const [loading, setLoading] = useState(true);
const [isAuthenticating, setAuthenticating] = useState(false);
const clerk = useClerk(); const clerk = useClerk();
const { user } = useUser(); const { user } = useUser();
useEffect(() => {
fetch("/api/isAuthenticating").then((b) => {
b.json().then((m) => {
setAuthenticating(m.message);
setLoading(false);
});
});
}, []);
return ( return (
<> <>
<SignedOut> <SignedOut>
<div className=" mt-1 gap-1 grid grid-cols-5"> <div className=" mt-1 gap-1 grid grid-cols-5">
{isAuthenticating && <SignInPopoverButton className="col-span-2" />} <SignInPopoverButton className="col-span-2" variant="outline"/>
<Button size="icon" variant="ghost"> <Popover>
<InfoIcon size={18} /> <PopoverTrigger>
</Button> <Button size="icon" variant="ghost">
<InfoIcon size={18} />
</Button>
</PopoverTrigger>
<PopoverContent>
<InfoPopover />
</PopoverContent>
</Popover>
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<svg <svg
viewBox="0 0 438.549 438.549" viewBox="0 0 438.549 438.549"
@ -54,21 +50,19 @@ export default function TopBar({ inter }: { inter: string }) {
<div className="mt-1 grid grid-cols-4 gap-1"> <div className="mt-1 grid grid-cols-4 gap-1">
<Popover> <Popover>
<PopoverTrigger> <PopoverTrigger>
{isAuthenticating && !loading && ( <Button size="icon" variant="ghost" className="mb-1">
<Button size="icon" variant="ghost" className="mb-1"> <Image
<Image alt="Clerk Image"
alt="Clerk Image" src={
src={ user?.imageUrl == undefined
user?.imageUrl == undefined ? "https://img.clerk.com/preview.png?size=144&seed=seed&initials=AD&isSquare=true&bgType=marble&bgColor=6c47ff&fgType=silhouette&fgColor=FFFFFF&type=user&w=48&q=75"
? "https://img.clerk.com/preview.png?size=144&seed=seed&initials=AD&isSquare=true&bgType=marble&bgColor=6c47ff&fgType=silhouette&fgColor=FFFFFF&type=user&w=48&q=75" : user?.imageUrl
: user?.imageUrl }
} width={26}
width={26} height={26}
height={26} className="rounded-full"
className="rounded-full" />
/> </Button>
</Button>
)}
</PopoverTrigger> </PopoverTrigger>
<PopoverContent> <PopoverContent>
<LoggedInPopover /> <LoggedInPopover />

@ -0,0 +1,49 @@
import { cn } from "@/lib/utils";
interface BorderBeamProps {
className?: string;
size?: number;
duration?: number;
borderWidth?: number;
anchor?: number;
colorFrom?: string;
colorTo?: string;
delay?: number;
}
export const BorderBeam = ({
className,
size = 200,
duration = 15,
anchor = 90,
borderWidth = 1.5,
colorFrom = "#ffaa40",
colorTo = "#9c40ff",
delay = 0,
}: BorderBeamProps) => {
return (
<div
style={
{
"--size": size,
"--duration": duration,
"--anchor": anchor,
"--border-width": borderWidth,
"--color-from": colorFrom,
"--color-to": colorTo,
"--delay": `-${delay}s`,
} as React.CSSProperties
}
className={cn(
"pointer-events-none absolute inset-0 rounded-[inherit] [border:calc(var(--border-width)*1px)_solid_transparent]",
// mask styles
"![mask-clip:padding-box,border-box] ![mask-composite:intersect] [mask:linear-gradient(transparent,transparent),linear-gradient(white,white)]",
// pseudo styles
"after:absolute after:aspect-square after:w-[calc(var(--size)*1px)] after:animate-border-beam after:[animation-delay:var(--delay)] after:[background:linear-gradient(to_left,var(--color-from),var(--color-to),transparent)] after:[offset-anchor:calc(var(--anchor)*1%)_50%] after:[offset-path:rect(0_auto_auto_0_round_calc(var(--size)*1px))]",
className,
)}
/>
);
};

@ -1,6 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Activity, Calendar, Star } from "lucide-react"; import { Activity, Calendar, Star, TerminalIcon } from "lucide-react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -11,6 +11,7 @@ import {
} from "../ui/dialog"; } from "../ui/dialog";
import { useState } from "react"; import { useState } from "react";
import { Changelog, version } from "@/version"; import { Changelog, version } from "@/version";
import events from "@/lib/commandEvent"
export default function InfoPopover() { export default function InfoPopover() {
const [changeLog, setChangelog] = useState(false); const [changeLog, setChangelog] = useState(false);
@ -48,13 +49,8 @@ export default function InfoPopover() {
> >
<Star size={18} className="mr-2" /> Star on GitHub <Star size={18} className="mr-2" /> Star on GitHub
</Button> </Button>
<Button <Button variant="ghost" onClick={() => events.emit("cmd-event")}>
variant="ghost" <TerminalIcon size={18} className="mr-2" /> Open commands
onClick={() =>
window.open("https://mhsf.betteruptime.com/", "_blank")?.focus()
}
>
<Activity size={18} className="mr-2" /> View status
</Button> </Button>
</div> </div>
); );

@ -3,6 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import { Tabs, TabsList, TabsTrigger } from "../ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "../ui/tabs";
import { Spinner } from "../ui/spinner"; import { Spinner } from "../ui/spinner";
import { useRouter } from '@/lib/useRouter'
export default function TabServer({ export default function TabServer({
server, server,
@ -13,6 +14,7 @@ export default function TabServer({
}) { }) {
const [tab, setTab] = useState(tabDef); const [tab, setTab] = useState(tabDef);
const [tabLoading, setTabLoading] = useState(false); const [tabLoading, setTabLoading] = useState(false);
const router = useRouter()
return ( return (
<div className="w-full flex justify-center"> <div className="w-full flex justify-center">
@ -22,8 +24,8 @@ export default function TabServer({
setTab(tac); setTab(tac);
setTabLoading(true); setTabLoading(true);
if (tac == "historical") if (tac == "historical")
window.location.replace(`/server/${server}/short-term`); router.push(`/server/${server}/short-term`);
if (tac == "general") window.location.replace(`/server/${server}`); if (tac == "general") router.push(`/server/${server}`);
}} }}
className="w-[300px]" className="w-[300px]"
> >

@ -0,0 +1,81 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { Loader2 } from 'lucide-react';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
}
const LoadingButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, loading, children, ...props }, ref) => {
if (asChild) {
return (
<Slot ref={ref} {...props}>
<>
{React.Children.map(children as React.ReactElement, (child: React.ReactElement) => {
return React.cloneElement(child, {
className: cn(buttonVariants({ variant, size }), className),
children: (
<>
{loading && (
<Loader2 className={cn('h-4 w-4 animate-spin', children && 'mr-2')} />
)}
{child.props.children}
</>
),
});
})}
</>
</Slot>
);
}
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
disabled={loading}
ref={ref}
{...props}
>
<>
<Loader2 className={cn('h-4 w-4 animate-spin', children && 'mr-2')}/>
{children}
</>
</button>
);
},
);
LoadingButton.displayName = 'LoadingButton';
export { LoadingButton, buttonVariants };

175
src/lib/api.ts Normal file

@ -0,0 +1,175 @@
/**
* New API file for easier API access
* Could be used for a JavaScript library :eyes:
* @author DeveloLongScript
*/
//
const connector = (
endpoint: string,
options: { version: number; starting?: string }
) =>
`${options.starting == undefined ? "/" : `${options.starting}/`}api/v${options.version}${endpoint}`;
export async function getMOTDFromServer(
list: Array<{ server: string; motd: string }>
): Promise<Array<{ server: string; motd: string }>> {
const result = await fetch(connector("/motd", { version: 1 }), {
body: JSON.stringify({ motd: list }),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
let json = await result.json();
return json.result;
}
export async function getCommunityServerFavorites(
server: string
): Promise<number> {
const result = await fetch(
connector(`/favorites/${server}/community-favorites`, { version: 0 }),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
let json = await result.json();
return json.result;
}
/** requires authentication */
export async function favoriteServer(server: string) {
try {
await fetch(
connector(`/favorites/${server}/favorite-server`, { version: 0 }),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
} catch {
throw Error("Not authenticated with a user.");
}
}
/** requires authentication */
export async function isFavorited(server: string): Promise<boolean> {
try {
const response = await fetch(
connector(`/favorites/${server}/favorited`, { version: 0 }),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
return (await response.json()).data;
} catch {
throw Error("Not authenticated with a user.");
}
}
/** requires authentication */
export async function getAccountFavorites(): Promise<Array<string>> {
try {
const response = await fetch(
connector(`/favorites/account-favorites`, { version: 0 }),
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
return (await response.json()).result;
} catch {
throw Error("Not authenticated with a user.");
}
}
/**
* currently not used in frontend yet
*/
export async function getHistoricalData(
server: string,
scopes: Array<"player_count" | "favorites" | "server" | "time">
): Promise<
Array<{
player_count?: number;
favorites?: number;
server?: string;
time?: number;
}>
> {
const response = await fetch(
connector(`/history/${server}/get-historical-data`, { version: 0 }),
{
method: "POST",
body: JSON.stringify({ scopes }),
headers: {
"Content-Type": "application/json",
},
}
);
return (await response.json()).data;
}
export async function getShortTermData(
server: string,
scopes: Array<"player_count" | "favorites" | "server" | "time">
): Promise<
Array<{
player_count?: number;
favorites?: number;
server?: string;
time?: number;
}>
> {
const response = await fetch(
connector(`/history/${server}/get-short-term-data`, { version: 0 }),
{
method: "POST",
body: JSON.stringify({ scopes }),
headers: {
"Content-Type": "application/json",
},
}
);
return (await response.json()).data;
}
export async function getMetaShortTerm(
scopes: Array<"total_players" | "total_servers" | "unix">
): Promise<
Array<{
total_players?: number;
total_servers?: number;
unix?: number;
}>
> {
const response = await fetch(
connector(`/history/meta-short-term-data`, { version: 0 }),
{
method: "POST",
body: JSON.stringify({ scopes }),
headers: {
"Content-Type": "application/json",
},
}
);
return (await response.json()).data;
}

23
src/lib/commandEvent.ts Normal file

@ -0,0 +1,23 @@
class CommandEvents {
eventTarget;
constructor() {
this.eventTarget = new EventTarget();
}
// Method to emit events
emit(eventName: string) {
const event = new CustomEvent(eventName);
this.eventTarget.dispatchEvent(event);
}
// Method to listen for events
on(eventName: string, callback: () => void) {
this.eventTarget.addEventListener(eventName, () => {
callback();
});
}
}
const events = new CommandEvents();
export default events;

@ -1,5 +1,6 @@
import { OnlineServer } from "@/components/ServerView"; import { OnlineServer } from "./types/server";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { getMOTDFromServer } from "./api";
var numberOfItemsInView = 20; var numberOfItemsInView = 20;
@ -28,7 +29,7 @@ export default class ServersList {
console.log( console.log(
"%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the fetch object for debugging: ", "%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the fetch object for debugging: ",
"font-weight: bold", "font-weight: bold",
b, b
); );
toast.error(` toast.error(`
Error while grabbing servers from API. Error while grabbing servers from API.
@ -82,7 +83,7 @@ export default class ServersList {
console.log( console.log(
"%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the error for debugging: ", "%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the error for debugging: ",
"font-weight: bold", "font-weight: bold",
b, b
); );
bc(); bc();
}); });
@ -92,14 +93,14 @@ export default class ServersList {
moveListDown() { moveListDown() {
const slicedArray = this.servers.slice( const slicedArray = this.servers.slice(
this.it * numberOfItemsInView, this.it * numberOfItemsInView,
this.it * numberOfItemsInView + numberOfItemsInView, this.it * numberOfItemsInView + numberOfItemsInView
); );
this.currentServers = this.currentServers.concat(slicedArray); this.currentServers = this.currentServers.concat(slicedArray);
this.it++; this.it++;
console.log( console.log(
"%c[MHSF] Moved list down! Updated entries: ", "%c[MHSF] Moved list down! Updated entries: ",
"font-weight: bold", "font-weight: bold",
slicedArray, slicedArray
); );
if (slicedArray.length != numberOfItemsInView) { if (slicedArray.length != numberOfItemsInView) {
this.hasMore = false; this.hasMore = false;
@ -114,16 +115,10 @@ export default class ServersList {
this.hasMore = true; this.hasMore = true;
} }
async getMOTDs(list: Array<{ server: string; motd: string }>) { async getMOTDs(
let response = await fetch("/api/getMOTD", { list: Array<{ server: string; motd: string }>
body: JSON.stringify({ motd: list }), ): Promise<Array<{ server: string; motd: string }>> {
method: "POST", return await getMOTDFromServer(list);
headers: {
"Content-Type": "application/json",
},
});
let json = await response.json();
return json.result;
} }
} }

@ -1,4 +1,5 @@
import { twi } from "tw-to-css"; // rendering engine for MOTDs (aka Minehut)
const divList: any = { const divList: any = {
black: "000000", black: "000000",
dark_blue: "002bff", dark_blue: "002bff",
@ -147,7 +148,7 @@ function createHTML(
tag: string, tag: string,
className: string, className: string,
contents: string, contents: string,
tw?: boolean, tw?: boolean
) { ) {
if (className == undefined) className = ""; if (className == undefined) className = "";
if (contents == undefined) contents = ""; if (contents == undefined) contents = "";

@ -1,4 +1,4 @@
import { OnlineServer, ServerResponse } from "@/components/ServerView"; import { OnlineServer, ServerResponse } from "./types/server";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
export default class ServerSingle { export default class ServerSingle {
@ -27,7 +27,7 @@ export default class ServerSingle {
g(true); g(true);
} }
}); });
}), })
); );
} else g(true); } else g(true);
}); });
@ -35,7 +35,7 @@ export default class ServerSingle {
console.log( console.log(
"%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the fetch object for debugging: ", "%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the fetch object for debugging: ",
"font-weight: bold", "font-weight: bold",
d, d
); );
toast.error(` toast.error(`
Error while grabbing servers from API. Error while grabbing servers from API.
@ -52,7 +52,7 @@ export default class ServerSingle {
console.log( console.log(
"%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the error for debugging: ", "%c[MHSF] STOP! There was an error while requesting Minehut's API! Heres the error for debugging: ",
"font-weight: bold", "font-weight: bold",
b, b
); );
bc(); bc();
}); });

21
src/lib/top-loader.tsx Normal file

@ -0,0 +1,21 @@
// NextTopLoader.tsx
'use client';
import Loader from 'nextjs-toploader';
import { usePathname } from 'next/navigation';
import {useEffect} from "react"
import * as NProgress from "nprogress";
import { useTheme } from 'next-themes';
export default function NextTopLoader() {
const pathname = usePathname();
const theme = useTheme()
useEffect(() => {
NProgress.done();
}, [pathname]);
return (
<Loader color={theme.resolvedTheme == "dark" ? "white" : "black"} shadow={false}/>
)
}

81
src/lib/types/server.ts Normal file

@ -0,0 +1,81 @@
export interface ServerResponse {
__unix?: string;
deletion?: Deletion;
_id: string;
categories: string[];
inheritedCategories: any[];
purchased_icons: string[];
backup_slots: number;
suspended: boolean;
server_version_type: string;
proxy: boolean;
connectedServers: any[];
motd: string;
visibility: boolean;
server_plan: string;
storage_node: string;
default_banner_image: string;
default_banner_tint: string;
owner: string;
name: string;
name_lower: string;
creation: number;
platform: string;
credits_per_day: number;
in_game: boolean;
using_cosmetics: boolean;
__v: number;
port: number;
last_online: number;
joins: number;
active_icon: string;
expired: boolean;
icon: string;
online: boolean;
maxPlayers: number;
playerCount: number;
rawPlan: string;
activeServerPlan: string;
}
export interface Deletion {
started: boolean;
started_at: number;
reason: string;
completed: boolean;
completed_at: number;
storage_completed: boolean;
storage_completed_at: number;
}
export interface OnlineServer {
staticInfo: StaticInfo;
maxPlayers: number;
name: string;
motd: string;
icon: string;
playerData: PlayerData;
connectable: boolean;
visibility: boolean;
allCategories: string[];
usingCosmetics: boolean;
author?: string;
authorRank: string;
}
export interface StaticInfo {
_id: string;
serverPlan: string;
serviceStartDate: number;
platform: string;
planMaxPlayers: number;
planRam: number;
alwaysOnline: boolean;
rawPlan: string;
connectedServers: any[];
}
export interface PlayerData {
playerCount: number;
timeNoPlayers: number;
}

32
src/lib/useRouter.tsx Normal file

@ -0,0 +1,32 @@
// useRouter.ts
import { NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime';
import { useRouter as useNextRouter, usePathname } from 'next/navigation';
import { useCallback } from 'react';
import NProgress from 'nprogress';
export const useRouter = () => {
const router = useNextRouter();
const pathname = usePathname();
const replace = useCallback(
(href: string, options?: NavigateOptions) => {
href !== pathname && NProgress.start();
router.replace(href, options);
},
[router, pathname],
);
const push = useCallback(
(href: string, options?: NavigateOptions) => {
href !== pathname && NProgress.start();
router.push(href, options);
},
[router, pathname],
);
return {
...router,
replace,
push,
};
};

@ -2,7 +2,7 @@
// its fully automatic // its fully automatic
import Favorites from "@/app/account/favorites/page"; import Favorites from "@/app/account/favorites/page";
import { OnlineServer } from "@/components/ServerView"; import { OnlineServer } from "@/lib/types/server";
import { Inngest } from "inngest"; import { Inngest } from "inngest";
import { serve } from "inngest/next"; import { serve } from "inngest/next";
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
@ -16,8 +16,8 @@ export default serve({
client: inngest, client: inngest,
functions: [ functions: [
inngest.createFunction( inngest.createFunction(
{ id: "every-60-min" }, { id: "every-30-min" },
[{ cron: "*/30 * * * *" }], [{ cron: "*/30 * * * *" }, { event: "test/30-min" }],
async ({ event, step }) => { async ({ event, step }) => {
const mongo = new MongoClient(process.env.MONGO_DB as string); const mongo = new MongoClient(process.env.MONGO_DB as string);
try { try {
@ -79,7 +79,7 @@ export default serve({
mongo.close(); mongo.close();
return { event, body: "Cloudflare.. aborting " + e }; return { event, body: "Cloudflare.. aborting " + e };
} }
}, }
), ),
inngest.createFunction( inngest.createFunction(
{ id: "every-two-months" }, { id: "every-two-months" },
@ -118,7 +118,7 @@ export default serve({
event, event,
body: "Dropped database. ", body: "Dropped database. ",
}; };
}, }
), ),
], ],
}); });

@ -1,8 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.send({ message: process.env.IS_AUTH == "true" });
}

@ -6,7 +6,7 @@ export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse
) { ) {
const server = checkForInfoOrLeave(res, req.body.server); const { server } = req.query;
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();
@ -25,12 +25,6 @@ export default async function handler(
client.close(); client.close();
} }
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
}
export async function increaseNum(client: MongoClient, server: string) { export async function increaseNum(client: MongoClient, server: string) {
const db = client.db("mhsf"); const db = client.db("mhsf");
const collection = db.collection("meta"); const collection = db.collection("meta");

@ -1,7 +1,7 @@
import type { NextApiResponse, NextApiRequest } from "next"; import type { NextApiResponse, NextApiRequest } from "next";
import { MongoClient, ObjectId } from "mongodb"; import { MongoClient, ObjectId } from "mongodb";
import { getAuth } from "@clerk/nextjs/server"; import { getAuth } from "@clerk/nextjs/server";
import { decreaseNum, increaseNum } from "./getCommunityNum"; import { decreaseNum, increaseNum } from "./community-favorites";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
@ -12,7 +12,7 @@ export default async function handler(
if (!userId) { if (!userId) {
return res.status(401).json({ error: "Unauthorized" }); return res.status(401).json({ error: "Unauthorized" });
} }
const server = checkForInfoOrLeave(res, req.body.server); const server = req.query.server as string;
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();
@ -58,9 +58,3 @@ export default async function handler(
} }
} }
} }
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
}

@ -4,14 +4,14 @@ import { getAuth } from "@clerk/nextjs/server";
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 server = checkForInfoOrLeave(res, req.body.server); const server = req.query.server as string;
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();
@ -24,9 +24,3 @@ export default async function handler(
} }
client.close(); client.close();
} }
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
}

@ -4,7 +4,7 @@ import { getAuth } from "@clerk/nextjs/server";
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);
@ -25,9 +25,3 @@ export default async function handler(
} }
client.close(); client.close();
} }
function checkForInfoOrLeave(res: NextApiResponse, info: any) {
if (info == undefined)
res.status(400).json({ message: "Information wasn't supplied" });
return info;
}

@ -3,11 +3,11 @@ import { NextApiRequest, NextApiResponse } from "next";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse
) { ) {
const client = new MongoClient(process.env.MONGO_DB as string); const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("historical"); const db = client.db("mhsf").collection("historical");
const server = checkForInfoOrLeave(res, req.body.server); const server = req.query.server as string;
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes); const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes);
const allData = await db.find({ server }).toArray(); const allData = await db.find({ server }).toArray();

@ -3,11 +3,11 @@ import { NextApiRequest, NextApiResponse } from "next";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse
) { ) {
const client = new MongoClient(process.env.MONGO_DB as string); const client = new MongoClient(process.env.MONGO_DB as string);
const db = client.db("mhsf").collection("history"); const db = client.db("mhsf").collection("history");
const server = checkForInfoOrLeave(res, req.body.server); const server = req.query.server as string;
const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes); const scopes: Array<string> = checkForInfoOrLeave(res, req.body.scopes);
const allData = await db.find({ server }).toArray(); const allData = await db.find({ server }).toArray();

@ -1,11 +1,11 @@
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import parseToHTML from "@/lib/miniMessage2HTML"; import parseToHTML from "@/lib/motdEngine";
let num = 0; let num = 0;
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse
) { ) {
num++; num++;
var body: Array<{ server: string; motd: string }> = req.body.motd; var body: Array<{ server: string; motd: string }> = req.body.motd;

@ -1,4 +1,4 @@
export const version = "b-0.4.5"; export const version = "b-0.6.0";
const User = ({ user }: { user: string }) => ( const User = ({ user }: { user: string }) => (
<span className="cursor-pointer bg-[rgba(255,165,0,0.25);] rounded p-[2.5px]"> <span className="cursor-pointer bg-[rgba(255,165,0,0.25);] rounded p-[2.5px]">
@ -8,6 +8,18 @@ const User = ({ user }: { user: string }) => (
export const Changelog = () => ( export const Changelog = () => (
<> <>
<div>
<strong className="flex items-center">
Version b-0.6.0 (August 3rd 2024)
</strong>
<ul>
<li> Enhanced shortcuts</li>
<li> Added gradient beam to player count</li>
<li> Updated loading animations</li>
<li> Lots of bugfixes</li>
</ul>
</div>
<br />
<div> <div>
<strong className="flex items-center"> <strong className="flex items-center">
Version b-0.4.5 (July 26th 2024): Version b-0.4.5 (July 26th 2024):

@ -59,6 +59,12 @@ const config = {
sm: "calc(var(--radius) - 4px)", sm: "calc(var(--radius) - 4px)",
}, },
keyframes: { keyframes: {
"border-beam": {
"100%": {
"offset-distance": "100%",
},
},
"accordion-down": { "accordion-down": {
from: { height: "0" }, from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" }, to: { height: "var(--radix-accordion-content-height)" },
@ -71,6 +77,8 @@ const config = {
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
}, },
}, },
}, },

@ -1331,6 +1331,11 @@
dependencies: dependencies:
undici-types "~5.26.4" undici-types "~5.26.4"
"@types/nprogress@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.3.tgz#b2150b054a13622fabcba12cf6f0b54c48b14287"
integrity sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==
"@types/prop-types@*": "@types/prop-types@*":
version "15.7.12" version "15.7.12"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz"
@ -4695,6 +4700,14 @@ next@14.2.3:
"@next/swc-win32-ia32-msvc" "14.2.3" "@next/swc-win32-ia32-msvc" "14.2.3"
"@next/swc-win32-x64-msvc" "14.2.3" "@next/swc-win32-x64-msvc" "14.2.3"
nextjs-toploader@^1.6.12:
version "1.6.12"
resolved "https://registry.yarnpkg.com/nextjs-toploader/-/nextjs-toploader-1.6.12.tgz#5b9f951e0de80450a23acd5101f4a311265c0d70"
integrity sha512-nbun5lvVjlKnxLQlahzZ55nELVEduqoEXT03KCHnsEYJnFpI/3BaIzpMyq/v8C7UGU2NfxQmjq6ldZ310rsDqA==
dependencies:
nprogress "^0.2.0"
prop-types "^15.8.1"
no-case@^3.0.4: no-case@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz"
@ -4739,6 +4752,11 @@ npm-run-path@^5.1.0:
dependencies: dependencies:
path-key "^4.0.0" path-key "^4.0.0"
nprogress@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==
nypm@^0.3.8: nypm@^0.3.8:
version "0.3.9" version "0.3.9"
resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.3.9.tgz#ab74c55075737466847611aa33c3c67741c01d8f" resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.3.9.tgz#ab74c55075737466847611aa33c3c67741c01d8f"