diff --git a/src/app/(pages)/page.tsx b/src/app/(pages)/page.tsx index 53ead15..896c4e4 100644 --- a/src/app/(pages)/page.tsx +++ b/src/app/(pages)/page.tsx @@ -6,9 +6,25 @@ import { Separator } from "../components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "../components/ui/tooltip"; type Button = { + /** + * The title of the button. + */ title: string; + + /** + * The tooltip to display for this statistic. + */ tooltip: string; + + /** + * The URL to go to. + */ url: string; + + /** + * Whether clicking the button will + * open the link in a new tab. + */ openInNewTab?: boolean; }; diff --git a/src/app/(pages)/player/[[...id]]/page.tsx b/src/app/(pages)/player/[[...id]]/page.tsx index 7dedfc9..128115d 100644 --- a/src/app/(pages)/player/[[...id]]/page.tsx +++ b/src/app/(pages)/player/[[...id]]/page.tsx @@ -1,18 +1,14 @@ /* eslint-disable @next/next/no-img-element */ -import { Card } from "@/app/components/card"; -import { CodeDialog } from "@/app/components/code-dialog"; import { CopyButton } from "@/app/components/copy-button"; import { ErrorCard } from "@/app/components/error-card"; import { LookupPlayer } from "@/app/components/player/lookup-player"; +import { PlayerView } from "@/app/components/player/player-view"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/app/components/ui/context-menu"; -import { Separator } from "@/app/components/ui/separator"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/app/components/ui/tooltip"; import { Colors } from "@/common/colors"; import { generateEmbed } from "@/common/embed"; -import { CachedPlayer, McUtilsAPIError, SkinPart, getPlayer } from "mcutils-library"; +import { isValidPlayer } from "@/common/player"; +import { CachedPlayer, McUtilsAPIError, getPlayer } from "mcutils-library"; import { Metadata, Viewport } from "next"; -import Image from "next/image"; -import Link from "next/link"; import { ReactElement } from "react"; import config from "../../../../../config.json"; @@ -23,43 +19,28 @@ type Params = { }; export async function generateViewport({ params: { id } }: Params): Promise { - try { - if (!id || id.length === 0) { - return { - themeColor: Colors.red, - }; - } - await getPlayer(id); // Ensure the player is valid. - return { - themeColor: Colors.green, - }; - } catch (err) { - // An error occurred - return { - themeColor: Colors.red, - }; - } + const validPlayer = await isValidPlayer(id); + return { + themeColor: validPlayer ? Colors.green : Colors.red, + }; } export async function generateMetadata({ params: { id } }: Params): Promise { + // No id provided + if (!id || id.length === 0) { + return generateEmbed({ + title: "Player Lookup", + description: "Click to lookup a player.", + }); + } + try { - // No id provided - if (!id || id.length === 0) { - return generateEmbed({ - title: "Player Lookup", - description: "Click to lookup a player.", - }); - } - const player = await getPlayer(id); - - const { username, uniqueId, skin } = player; + const { username, uniqueId, skin } = await getPlayer(id); const headPartUrl = skin.parts.head; - const description = `UUID: ${uniqueId}\n\nClick to view more information about the player.`; - return generateEmbed({ title: `${username}`, - description: description, + description: `UUID: ${uniqueId}\n\nClick to view more information about the player.`, image: headPartUrl, }); } catch (err) { @@ -95,65 +76,7 @@ export default async function Page({ params: { id } }: Params): Promise - -
-
- - - -
- -
- The player's skin -
- -
-
-

{player.username}

-

{player.uniqueId}

-
- - - -
-

Skin Parts

-
- {Object.entries(player.skin.parts) - .filter(([part]) => part !== SkinPart.HEAD) // Don't show the head part again - .map(([part, url]) => { - return ( - - - - {`The - - - -

- Click to view {player.username}'s {part} -

-
-
- ); - })} -
-
-
-
-
+
diff --git a/src/app/(pages)/server/[platform]/[[...hostname]]/page.tsx b/src/app/(pages)/server/[platform]/[[...hostname]]/page.tsx index 8bdc818..dbe8ffc 100644 --- a/src/app/(pages)/server/[platform]/[[...hostname]]/page.tsx +++ b/src/app/(pages)/server/[platform]/[[...hostname]]/page.tsx @@ -1,11 +1,11 @@ -import { Card } from "@/app/components/card"; import { CopyButton } from "@/app/components/copy-button"; import { ErrorCard } from "@/app/components/error-card"; import { LookupServer } from "@/app/components/server/lookup-server"; +import { ServerView } from "@/app/components/server/server-view"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/app/components/ui/context-menu"; import { Colors } from "@/common/colors"; import { generateEmbed } from "@/common/embed"; -import { formatNumber } from "@/common/number-utils"; +import { isValidServer } from "@/common/server"; import { capitalizeFirstLetter } from "@/common/string-utils"; import { CachedBedrockMinecraftServer, @@ -15,7 +15,6 @@ import { getServer, } from "mcutils-library"; import { Metadata, Viewport } from "next"; -import Image from "next/image"; import { ReactElement } from "react"; import config from "../../../../../../config.json"; @@ -55,40 +54,29 @@ function checkPlatform(platform: ServerPlatform): boolean { } export async function generateViewport({ params: { platform, hostname } }: Params): Promise { - try { - if (!checkPlatform(platform) || !hostname || hostname.length === 0) { - return { - themeColor: Colors.red, - }; - } - await getServer(platform, hostname); // Ensure the server is valid. - return { - themeColor: Colors.green, - }; - } catch (err) { - // An error occurred - return { - themeColor: Colors.red, - }; - } + const validPlayer = await isValidServer(platform, hostname); + return { + themeColor: validPlayer ? Colors.green : Colors.red, + }; } export async function generateMetadata({ params: { platform, hostname } }: Params): Promise { + if (!checkPlatform(platform)) { + // Invalid platform + return generateEmbed({ + title: "Server Not Found", + description: "Invalid platform", + }); + } + if (!hostname || hostname.length === 0) { + // No hostname + return generateEmbed({ + title: "Server Lookup", + description: `Click to lookup a ${capitalizeFirstLetter(platform)} server.`, + }); + } + try { - if (!checkPlatform(platform)) { - // Invalid platform - return generateEmbed({ - title: "Server Not Found", - description: "Invalid platform", - }); - } - if (!hostname || hostname.length === 0) { - // No hostname - return generateEmbed({ - title: "Server Lookup", - description: `Click to lookup a ${capitalizeFirstLetter(platform)} server.`, - }); - } const server = await getServer(platform, hostname); const { hostname: serverHostname, players } = server as CachedJavaMinecraftServer | CachedBedrockMinecraftServer; @@ -115,21 +103,21 @@ export async function generateMetadata({ params: { platform, hostname } }: Param export default async function Page({ params: { platform, hostname } }: Params): Promise { let error: string | undefined = undefined; // The error to display let server: CachedJavaMinecraftServer | CachedBedrockMinecraftServer | undefined = undefined; // The server to display - let invalidPlatform = !checkPlatform(platform); // Whether the platform is invalid + let invalidPlatform: boolean = !checkPlatform(platform); // Whether the platform is invalid + let favicon: string | undefined; // The server's favicon - // Try and get the player to display - try { - if (invalidPlatform) { - error = "Invalid platform"; // Set the error message - } else { + if (invalidPlatform) { + error = "Invalid platform"; // Set the error message + } else { + // Try and get the player to display + try { server = platform && hostname ? await getServer(platform, hostname) : undefined; + favicon = getFavicon(platform, server); + } catch (err) { + error = (err as McUtilsAPIError).message; // Set the error message } - } catch (err) { - error = (err as McUtilsAPIError).message; // Set the error message } - const favicon = getFavicon(platform, server); - return (
@@ -143,39 +131,7 @@ export default async function Page({ params: { platform, hostname } }: Params): {server != null && ( - -
-
- {favicon && ( -
- The server's favicon -
- )} - -
-

{server.hostname}

-
-

- Players online: {formatNumber(server.players.online)}/{formatNumber(server.players.max)} -

-
-
-
- -
- {server.motd.html.map((line, index) => { - return

; - })} -
-
-
+
diff --git a/src/app/components/card.tsx b/src/app/components/card.tsx index b2452a7..223c44a 100644 --- a/src/app/components/card.tsx +++ b/src/app/components/card.tsx @@ -1,12 +1,18 @@ import { cn } from "@/common/utils"; import { ReactElement } from "react"; -export function Card({ - children, - className, -}: Readonly<{ +type CardProps = { + /** + * The children for this element. + */ children: React.ReactNode; + + /** + * The class names to append. + */ className?: string; -}>): ReactElement { +}; + +export function Card({ children, className }: CardProps): ReactElement { return
{children}
; } diff --git a/src/app/components/code-dialog.tsx b/src/app/components/code-dialog.tsx index f8b2171..941f5d1 100644 --- a/src/app/components/code-dialog.tsx +++ b/src/app/components/code-dialog.tsx @@ -4,9 +4,24 @@ import { atelierSeasideDark } from "react-syntax-highlighter/dist/esm/styles/hlj import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; type CodeDialogProps = { + /** + * The title of the dialog. + */ title: string; + + /** + * The description of the dialog. + */ description: string; + + /** + * The code to show in the dialog. + */ code: string; + + /** + * The children for this element. + */ children: React.ReactNode; }; diff --git a/src/app/components/container.tsx b/src/app/components/container.tsx index 7c427c5..edb26a2 100644 --- a/src/app/components/container.tsx +++ b/src/app/components/container.tsx @@ -1,11 +1,14 @@ import { ReactElement } from "react"; import NavBar from "./navbar"; -export default function Container({ - children, -}: Readonly<{ +type ContainerProps = { + /** + * The children for this element. + */ children: React.ReactNode; -}>): ReactElement { +}; + +export default function Container({ children }: ContainerProps): ReactElement { return (
diff --git a/src/app/components/copy-button.tsx b/src/app/components/copy-button.tsx index 09651b4..e131413 100644 --- a/src/app/components/copy-button.tsx +++ b/src/app/components/copy-button.tsx @@ -5,7 +5,14 @@ import copy from "clipboard-copy"; import { ReactElement } from "react"; type CopyButtonProps = { + /** + * The content to copy to the clipboard. + */ content: string; + + /** + * The children for this element. + */ children: React.ReactNode; }; @@ -26,7 +33,7 @@ export function CopyButton({ content, children }: CopyButtonProps): ReactElement title: "Copied!", description: (

- Copied {content} to your clipboard + Copied {content} to your clipboard.

), duration: 5000, diff --git a/src/app/components/error-card.tsx b/src/app/components/error-card.tsx index 14800d1..a85b93d 100644 --- a/src/app/components/error-card.tsx +++ b/src/app/components/error-card.tsx @@ -2,6 +2,9 @@ import { ReactElement } from "react"; import { Card } from "./card"; type ErrorProps = { + /** + * The message to show. + */ message: string; }; diff --git a/src/app/components/logo.tsx b/src/app/components/logo.tsx index b0c2e35..f5faf78 100644 --- a/src/app/components/logo.tsx +++ b/src/app/components/logo.tsx @@ -1,7 +1,14 @@ import Image from "next/image"; import { ReactElement } from "react"; -export default function Logo({ size = 30 }: Readonly<{ size?: number }>): ReactElement { +type LogoProps = { + /** + * The size the logo will be. + */ + size?: number; +}; + +export default function Logo({ size = 30 }: LogoProps): ReactElement { return ( Minecraft Utilities

-
+
{pages.map((page, index) => { - return ; + return ; })}
-
+
diff --git a/src/app/components/player/player-view.tsx b/src/app/components/player/player-view.tsx new file mode 100644 index 0000000..1f7a48a --- /dev/null +++ b/src/app/components/player/player-view.tsx @@ -0,0 +1,80 @@ +/* eslint-disable @next/next/no-img-element */ +import { CachedPlayer, SkinPart } from "mcutils-library"; +import Image from "next/image"; +import Link from "next/link"; +import { ReactElement } from "react"; +import { Card } from "../card"; +import { CodeDialog } from "../code-dialog"; +import { Separator } from "../ui/separator"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; + +type PlayerViewProps = { + /** + * The player to display. + */ + player: CachedPlayer; +}; + +export function PlayerView({ player }: PlayerViewProps): ReactElement { + return ( + +
+
+ + + +
+ +
+ The player's skin +
+ +
+
+

{player.username}

+

{player.uniqueId}

+
+ + + +
+

Skin Parts

+
+ {Object.entries(player.skin.parts) + .filter(([part]) => part !== SkinPart.HEAD) // Don't show the head part again + .map(([part, url]) => { + return ( + + + + {`The + + + +

+ Click to view {player.username}'s {part} +

+
+
+ ); + })} +
+
+
+
+
+ ); +} diff --git a/src/app/components/rediect-button.tsx b/src/app/components/rediect-button.tsx index 52fa4eb..5c47f5b 100644 --- a/src/app/components/rediect-button.tsx +++ b/src/app/components/rediect-button.tsx @@ -2,8 +2,20 @@ import Link from "next/link"; import { ReactElement } from "react"; type ButtonProps = { + /** + * The title of the button. + */ title: string; + + /** + * The URL to go to. + */ url: string; + + /** + * Whether clicking the button will + * open the link in a new tab. + */ openInNewTab?: boolean; }; diff --git a/src/app/components/server/lookup-server.tsx b/src/app/components/server/lookup-server.tsx index 75786ae..faac24f 100644 --- a/src/app/components/server/lookup-server.tsx +++ b/src/app/components/server/lookup-server.tsx @@ -11,6 +11,9 @@ import { Label } from "../ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; type LookupServerProps = { + /** + * The current platform. + */ currentPlatform: string; }; diff --git a/src/app/components/server/server-view.tsx b/src/app/components/server/server-view.tsx new file mode 100644 index 0000000..62a8d4a --- /dev/null +++ b/src/app/components/server/server-view.tsx @@ -0,0 +1,55 @@ +import { formatNumber } from "@/common/number-utils"; +import { CachedBedrockMinecraftServer, CachedJavaMinecraftServer } from "mcutils-library"; +import Image from "next/image"; +import { ReactElement } from "react"; +import { Card } from "../card"; + +type ServerViewProps = { + /** + * The server to display. + */ + server: CachedJavaMinecraftServer | CachedBedrockMinecraftServer; + + /** + * The favicon for the server. + */ + favicon: string | undefined; +}; + +export function ServerView({ server, favicon }: ServerViewProps): ReactElement { + return ( + +
+
+ {favicon && ( +
+ The server's favicon +
+ )} + +
+

{server.hostname}

+
+

+ Players online: {formatNumber(server.players.online)}/{formatNumber(server.players.max)} +

+
+
+
+ +
+ {server.motd.html.map((line, index) => { + return

; + })} +
+
+
+ ); +} diff --git a/src/app/components/stat.tsx b/src/app/components/stat.tsx index b9e86eb..b68652d 100644 --- a/src/app/components/stat.tsx +++ b/src/app/components/stat.tsx @@ -2,8 +2,19 @@ import { ReactElement } from "react"; import CountUp from "react-countup"; type StatProps = { + /** + * The title of this statistic. + */ title: string; + + /** + * The value to display for this statistic. + */ value: number; + + /** + * The icon to display. + */ icon: ReactElement; }; diff --git a/src/app/components/stats.tsx b/src/app/components/stats.tsx index f8dd59d..0a8249b 100644 --- a/src/app/components/stats.tsx +++ b/src/app/components/stats.tsx @@ -7,9 +7,24 @@ import useWebSocket, { ReadyState } from "react-use-websocket"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; type Stat = { + /** + * The metric ID from the websocket. + */ id: string; + + /** + * The display name for this statistic. + */ displayName: string; + + /** + * The tooltip to display for this statistic. + */ tooltip: string; + + /** + * The icon to use for this statistic. + */ icon: ReactElement; }; diff --git a/src/common/player.ts b/src/common/player.ts new file mode 100644 index 0000000..351fa4f --- /dev/null +++ b/src/common/player.ts @@ -0,0 +1,16 @@ +import { getPlayer } from "mcutils-library"; + +/** + * Checks if the player is valid. + * + * @param id the player's id + * @returns true if valid, false otherwise + */ +export async function isValidPlayer(id: string): Promise { + try { + await getPlayer(id); + return true; + } catch { + return true; + } +} diff --git a/src/common/server.ts b/src/common/server.ts new file mode 100644 index 0000000..8137296 --- /dev/null +++ b/src/common/server.ts @@ -0,0 +1,17 @@ +import { ServerPlatform, getServer } from "mcutils-library"; + +/** + * Checks if the server is valid. + * + * @param platform the server's platform + * @param query the hostname for the server + * @returns true if valid, false otherwise + */ +export async function isValidServer(platform: ServerPlatform, query: string): Promise { + try { + await getServer(platform, query); + return true; + } catch { + return true; + } +}