feat: server list

This commit is contained in:
dvelo 2025-02-15 20:06:35 -06:00
parent f438d03399
commit 0f254e09f6
12 changed files with 9170 additions and 41 deletions

@ -39,6 +39,7 @@ import { Button } from "@/components/ui/button";
import { ClerkProvider } from "@clerk/nextjs"; import { ClerkProvider } from "@clerk/nextjs";
import Link from "next/link"; import Link from "next/link";
import { NavBar } from "@/components/feat/navbar/navbar"; import { NavBar } from "@/components/feat/navbar/navbar";
import { TooltipProvider } from "@/components/ui/tooltip";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@ -54,6 +55,7 @@ export default function RootLayout({
<ClerkProvider> <ClerkProvider>
<html lang="en"> <html lang="en">
<body className={inter.className}> <body className={inter.className}>
<TooltipProvider>
<noscript> <noscript>
<main className="flex justify-center items-center text-center min-h-screen h-max"> <main className="flex justify-center items-center text-center min-h-screen h-max">
<Placeholder <Placeholder
@ -71,6 +73,7 @@ export default function RootLayout({
<NavBar /> <NavBar />
<div className="pt-16">{children}</div> <div className="pt-16">{children}</div>
</IsScript> </IsScript>
</TooltipProvider>
</body> </body>
</html> </html>
</ClerkProvider> </ClerkProvider>

File diff suppressed because it is too large Load Diff

@ -20,8 +20,7 @@ export default function IconDisplay(props: {
<i <i
className={cn( className={cn(
props.server.icon != null props.server.icon != null
? `icon-minecraft icon-minecraft- ? `icon-minecraft icon-minecraft-${props.server.icon.replaceAll("_", "-").toLowerCase()}`
${props.server.icon.replaceAll("_", "-").toLowerCase()}`
: "icon-minecraft icon-minecraft-oak-sign", : "icon-minecraft icon-minecraft-oak-sign",
props.className props.className
)} )}
@ -62,8 +61,7 @@ export function IconDisplayClient(props: { server: string }) {
<i <i
className={cn( className={cn(
icon != null icon != null
? `icon-minecraft icon-minecraft- ? `icon-minecraft icon-minecraft-${icon.replaceAll("_", "-").toLowerCase()}`
${icon.replaceAll("_", "-").toLowerCase()}`
: "icon-minecraft icon-minecraft-oak-sign", : "icon-minecraft icon-minecraft-oak-sign",
"w-[16px] h-[16px]" "w-[16px] h-[16px]"
)} )}

@ -1,24 +1,30 @@
"use client"; "use client";
import { BrandingGenericIcon } from "@/components/icon";
import { BrandingColorfulIcon } from "@/components/Icon";
import { version } from "@/config/version"; import { version } from "@/config/version";
import { useScroll } from "@/lib/hooks/use-scroll";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Link from "next/link";
export function NavBar() { export function NavBar() {
const showBorder = useScroll(40);
return ( return (
<div <div
className={cn( className={cn(
"w-screen h-[3rem] border-b grid-cols-3 fixed backdrop-blur z-10", "w-screen h-[3rem] grid-cols-3 fixed backdrop-blur-xl z-10 flex",
"items-center justify-self-start me-auto mt-2 pl-4 max-sm:mt-3 flex-1" "items-center justify-self-start me-auto pl-4 flex-1 transition-all",
showBorder ? "border-b" : ""
)} )}
> >
<div className="gap-3 flex items-center"> <Link className="gap-5 flex items-center " href="/">
<BrandingColorfulIcon className="max-w-[32px] max-h-[32px] mt-0.5" /> <BrandingGenericIcon className="max-w-[32px] max-h-[32px] mt-0.5" />
<span className="gap-2 flex"> <span className="gap-2 flex group hover:text-blue-500 hover:underline transition-all">
<strong>MHSF</strong> <strong className="">MHSF</strong>
<span className="text-muted-foreground">v{version}</span> <span className="text-muted-foreground group-hover:text-blue-500 transition-all">
v{version}
</span> </span>
</div> </span>
</Link>
</div> </div>
); );
} }

@ -0,0 +1,14 @@
import type { OnlineServer } from "@/lib/types/mh-server";
import IconDisplay from "../icons/minecraft-icon-display";
import { Material } from "@/components/ui/material";
export default function ServerCard({ server }: { server: OnlineServer }) {
return (
<Material key={server.name}>
<span className="flex gap-2 items-center">
<IconDisplay server={server} />
{server.name}
</span>
</Material>
);
}

@ -1,4 +1,39 @@
"use client"; "use client";
import { Spinner } from "@/components/ui/spinner";
import { useServers } from "@/lib/hooks/use-servers";
import ServerCard from "./server-card";
import { Separator } from "@/components/ui/separator";
import { Statistics } from "./statistics";
export function ServerList() { export function ServerList() {
return <main>Server List </main>; const { servers, loading, serverCount, playerCount } = useServers();
if (loading)
return (
<div className="absolute top-[50%] left-[50%]">
<Spinner />
</div>
);
return (
<main className="px-3 lg:px-16">
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl mb-3">
Statistics
</h1>
<Statistics
totalServers={serverCount}
totalPlayers={playerCount}
topServer={servers[0]}
/>
<Separator className="my-3" />
<h1 className="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-4xl">
Servers
</h1>
<div className="grid grid-cols-4 gap-2 mt-3">
{servers.map((c) => (
<ServerCard server={c} key={c.name} />
))}
</div>
</main>
);
} }

@ -0,0 +1,29 @@
import { Material } from "@/components/ui/material";
import type { OnlineServer } from "@/lib/types/mh-server";
export function Statistics({
totalPlayers,
totalServers,
topServer,
}: {
totalPlayers: number;
totalServers: number;
topServer: OnlineServer;
}) {
return (
<div className="grid grid-cols-3 gap-2">
<Material className="gap-2">
<strong>Total Players</strong> <br />
<span className="text-lg">{totalPlayers}</span>
</Material>
<Material className="gap-2">
<strong>Total Servers</strong> <br />
<span className="text-lg">{totalServers}</span>
</Material>
<Material className="gap-2">
<strong>Top Server</strong> <br />
<span className="text-lg">{topServer.name}</span>
</Material>
</div>
);
}

@ -189,7 +189,7 @@ export function BrandingPrideIcon(props: SVGProps<SVGSVGElement>) {
export function BrandingGenericIcon(props: SVGProps<SVGSVGElement>) { export function BrandingGenericIcon(props: SVGProps<SVGSVGElement>) {
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
if (resolvedTheme == "dark") { if (resolvedTheme === "dark") {
return ( return (
<svg <svg
width="265" width="265"

@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ElementRef<typeof SeparatorPrimitive.Root>,
@ -18,14 +18,14 @@ const Separator = React.forwardRef<
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"shrink-0 bg-border", "shrink-0 bg-slate-200",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className className
)} )}
{...props} {...props}
/> />
) )
) );
Separator.displayName = SeparatorPrimitive.Root.displayName Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator } export { Separator };

@ -0,0 +1,7 @@
import { OnlineServer } from "../types/mh-server";
export function useInfiniteScrolling({
servers,
}: {
servers: OnlineServer[];
}) {}

@ -0,0 +1,21 @@
import { useCallback, useEffect, useState } from "react";
export function useScroll(threshold: number) {
const [scrolled, setScrolled] = useState(false);
const onScroll = useCallback(() => {
setScrolled(window.scrollY > threshold);
}, [threshold]);
useEffect(() => {
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, [onScroll]);
// also check on first load
useEffect(() => {
onScroll();
}, [onScroll]);
return scrolled;
}

@ -0,0 +1,43 @@
import { useEffect, useState } from "react";
import type { OnlineServer } from "../types/mh-server";
export function useServers() {
const [servers, setServers] = useState<OnlineServer[]>([]);
const [serverCount, setServerCount] = useState<number>(0);
const [playerCount, setPlayerCount] = useState<number>(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
try {
(async () => {
const serversFetch = await fetch("https://api.minehut.com/servers");
const serversJson: ServersAPIResponse = await serversFetch.json();
setPlayerCount(serversJson.total_players);
setServerCount(serversJson.total_servers);
setServers(serversJson.servers);
setLoading(false);
})();
} catch (e) {
console.error(e);
setError(true);
}
}, []);
return {
servers,
playerCount,
serverCount,
loading,
error,
};
}
export type ServersAPIResponse = {
servers: OnlineServer[];
// stats
total_players: number;
total_search_results: number;
total_servers: number;
};