server route!
All checks were successful
Deploy Frontend / docker (17, 3.8.5) (push) Successful in 2m59s

This commit is contained in:
Braydon 2024-04-17 23:34:13 -04:00
parent 367c974cb3
commit 56563802be
16 changed files with 591 additions and 107 deletions

Binary file not shown.

@ -40,13 +40,13 @@
{ {
"name": "Player Lookup", "name": "Player Lookup",
"description": "Wanna find a player? You can locate them here by their username or UUID.", "description": "Wanna find a player? You can locate them here by their username or UUID.",
"image": "/media/featured/server.png", "image": "/media/featured/server.jpg",
"href": "/player" "href": "/player"
}, },
{ {
"name": "Server Lookup", "name": "Server Lookup",
"description": "Fugiat culpa est consequat nostrud exercitation ut id ex in.", "description": "Fugiat culpa est consequat nostrud exercitation ut id ex in.",
"image": "/media/featured/server.png", "image": "/media/featured/server.jpg",
"href": "/player" "href": "/player"
} }
] ]

@ -15,6 +15,7 @@
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

@ -1,13 +1,14 @@
import Embed from "@/components/embed"; import Embed from "@/components/embed";
import PlayerResult from "@/components/player/player-result"; import PlayerResult from "@/components/player/player-result";
import PlayerSearch from "@/components/player/player-search"; import PlayerSearch from "@/components/player/player-search";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { minecrafter } from "@/font/fonts"; import { minecrafter } from "@/font/fonts";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { PageProps } from "@/types/page"; import { PageProps } from "@/types/page";
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
import { Metadata } from "next"; import { Metadata } from "next";
import Image from "next/image";
import { CachedPlayer, getPlayer, type RestfulMCAPIError } from "restfulmc-lib";
import { ReactElement } from "react"; import { ReactElement } from "react";
import { CachedPlayer, getPlayer, type RestfulMCAPIError } from "restfulmc-lib";
/** /**
* The page to lookup a player. * The page to lookup a player.
@ -29,18 +30,7 @@ const PlayerPage = async ({ params }: PageProps): Promise<ReactElement> => {
// Render the page // Render the page
return ( return (
<main className="px-3 h-screen flex justify-center items-center"> <main className="px-3 h-screen flex justify-center items-center">
<div className="mt-0 sm:mt-[45rem] xl:mt-0 flex flex-col xl:flex-row xl:gap-24 2xl:gap-48 transition-all transform-gpu"> <div className="flex flex-col gap-7">
{/* Banner */}
<Image
className="hidden sm:flex xl:my-auto h-[28rem] pointer-events-none"
src="/media/players.webp"
alt="Minecraft Players"
width={632}
height={632}
/>
{/* Search */}
<div className="pb-16 xl:pb-0 flex flex-col gap-7">
<h1 <h1
className={cn( className={cn(
"mt-20 text-6xl text-minecraft-green-3 text-center pointer-events-none", "mt-20 text-6xl text-minecraft-green-3 text-center pointer-events-none",
@ -52,7 +42,13 @@ const PlayerPage = async ({ params }: PageProps): Promise<ReactElement> => {
<div className="flex flex-col gap-5 px-10 xs:px-0"> <div className="flex flex-col gap-5 px-10 xs:px-0">
{/* Error */} {/* Error */}
{error && <p className="text-red-500">{error}</p>} {error && (
<Alert variant="destructive">
<ExclamationCircleIcon width={20} height={20} />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Search */} {/* Search */}
<PlayerSearch query={query} /> <PlayerSearch query={query} />
@ -61,7 +57,6 @@ const PlayerPage = async ({ params }: PageProps): Promise<ReactElement> => {
{/* Player Result */} {/* Player Result */}
{result && <PlayerResult query={query} player={result} />} {result && <PlayerResult query={query} player={result} />}
</div> </div>
</div>
</main> </main>
); );
}; };
@ -93,13 +88,13 @@ export const generateMetadata = async ({
return Embed({ return Embed({
title: "Invalid Player", title: "Invalid Player",
color: "#EB4034", color: "#EB4034",
description: "The player you searched for is invalid.", description: `The player ${query} is invalid.`,
}); });
} else if (code === 404) { } else if (code === 404) {
return Embed({ return Embed({
title: "Player Not Found", title: "Player Not Found",
color: "#EB4034", color: "#EB4034",
description: "The player you searched for was not found.", description: `The player ${query} was not found.`,
}); });
} }
} }

@ -0,0 +1,93 @@
import ServerResult from "@/components/server/server-result";
import ServerSearch from "@/components/server/server-search";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { minecrafter } from "@/font/fonts";
import { cn } from "@/lib/utils";
import { PageProps } from "@/types/page";
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
import { Metadata } from "next";
import { ReactElement } from "react";
import {
CachedBedrockMinecraftServer,
CachedJavaMinecraftServer,
ServerPlatform,
getMinecraftServer,
type RestfulMCAPIError,
} from "restfulmc-lib";
/**
* The page to lookup a server.
*
* @returns the page jsx
*/
const ServerPage = async ({ params }: PageProps): Promise<ReactElement> => {
let error: string | undefined = undefined; // The error to display
let result:
| CachedJavaMinecraftServer
| CachedBedrockMinecraftServer
| undefined = undefined; // The server to display
const platform: string | undefined = params.slug?.[0]; // The platform to search for
const hostname: string | undefined = params.slug?.[1]; // The hostname of the server to search for
// Try and get the server to display
try {
result =
platform && hostname
? await getMinecraftServer(platform as ServerPlatform, hostname)
: undefined;
} catch (err) {
error = (err as RestfulMCAPIError).message; // Set the error message
}
// Render the page
return (
<main className="px-3 h-screen flex justify-center items-center">
<div className="flex flex-col gap-7">
<h1
className={cn(
"mt-20 text-6xl text-minecraft-green-3 text-center pointer-events-none",
minecrafter.className
)}
>
Server Lookup
</h1>
<div className="flex flex-col gap-5 px-10 xs:px-16 transition-all transform-gpu">
{/* Error */}
{error && (
<Alert variant="destructive">
<ExclamationCircleIcon width={20} height={20} />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Search */}
<ServerSearch platform={platform} hostname={hostname} />
</div>
{/* Server Result */}
{result && (
<div className="flex justify-center scale-90 xs:scale-100 transition-all transform-gpu">
<ServerResult server={result} />
</div>
)}
</div>
</main>
);
};
/**
* Generate metadata for this page.
*
* @param params the route params
* @param searchParams the search params
* @returns the generated metadata
*/
export const generateMetadata = async ({
params,
}: PageProps): Promise<Metadata> => {
return {};
};
export default ServerPage;

@ -5,6 +5,16 @@ import { Label } from "@/components/ui/label";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { ReactElement } from "react"; import { ReactElement } from "react";
/**
* Props for a player search.
*/
type PlayerSearchProps = {
/**
* The original query to search for.
*/
query: string | undefined;
};
/** /**
* A component for searching for a player. * A component for searching for a player.
* *
@ -13,16 +23,14 @@ import { ReactElement } from "react";
*/ */
const PlayerSearch = ({ const PlayerSearch = ({
query, query,
}: { }: PlayerSearchProps): ReactElement => {
query: string | undefined;
}): ReactElement => {
const handleRedirect = async (form: FormData): Promise<void> => { const handleRedirect = async (form: FormData): Promise<void> => {
"use server"; "use server";
redirect(`/player/${form.get("query")}`); redirect(`/player/${form.get("query")}`);
}; };
return ( return (
<form <form
className="flex flex-col gap-7 justify-center items-center" className="flex flex-col gap-7 items-center"
action={handleRedirect} action={handleRedirect}
> >
<div className="w-full flex flex-col gap-3"> <div className="w-full flex flex-col gap-3">
@ -32,6 +40,7 @@ const PlayerSearch = ({
name="query" name="query"
placeholder="Query..." placeholder="Query..."
defaultValue={query} defaultValue={query}
required
maxLength={36} maxLength={36}
/> />
</div> </div>

@ -0,0 +1,72 @@
import { cn } from "@/lib/utils";
import Image from "next/image";
import { ReactNode } from "react";
import {
CachedBedrockMinecraftServer,
CachedJavaMinecraftServer,
} from "restfulmc-lib";
import config from "../../config";
/**
* The props for a server result.
*/
type ServerResultProps = {
/**
* The result of a search.
*/
server: CachedJavaMinecraftServer | CachedBedrockMinecraftServer;
};
/**
* The result of a server search.
*
* @param server the server to display
* @returns the server result jsx
*/
const ServerResult = ({ server }: ServerResultProps): ReactNode => {
const favicon: string | undefined = (server as CachedJavaMinecraftServer)
.favicon?.url; // The favicon of the server (TODO: bedrock)
return (
<div
className={cn(
"w-[29rem] relative p-2 flex gap-2 rounded-lg",
'bg-[url("/media/server-background.png")]'
)}
>
{/* Favicon */}
<Image
src={favicon || `${config.apiEndpoint}/server/icon/fallback`}
alt={`${server.hostname}'s Favicon`}
width={64}
height={64}
/>
{/* Name & MOTD */}
<div className="flex flex-col">
<h1>{server.hostname}</h1>
{server.motd.html.map((line, index) => {
return (
<p
key={index}
dangerouslySetInnerHTML={{ __html: line }}
></p>
);
})}
</div>
{/* Ping */}
<div className="absolute top-0.5 right-0.5 flex gap-1 items-center">
<p>
{server.players.online}/{server.players.max}
</p>
<Image
src="/media/full-ping.png"
alt="Ping!"
width={28}
height={28}
/>
</div>
</div>
);
};
export default ServerResult;

@ -0,0 +1,92 @@
import MinecraftButton from "@/components/minecraft-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { redirect } from "next/navigation";
import { ReactElement } from "react";
import { ServerPlatform } from "restfulmc-lib";
/**
* Props for a server search.
*/
type ServerSearchProps = {
/**
* The original platform to query for.
*/
platform: string | undefined;
/**
* The original hostname to query for.
*/
hostname: string | undefined;
};
/**
* A component for searching for a server.
*
* @param query the query to search for
* @returns the search component jsx
*/
const ServerSearch = ({
platform,
hostname,
}: ServerSearchProps): ReactElement => {
const handleRedirect = async (form: FormData): Promise<void> => {
"use server";
redirect(`/server/${form.get("platform")}/${form.get("hostname")}`);
};
return (
<form
className="flex flex-col gap-7 items-center"
action={handleRedirect}
>
<div className="w-full flex gap-2">
{/* Platform Selection */}
<div className="flex flex-col gap-3">
<Label htmlFor="platform">Platform</Label>
<Select
name="platform"
defaultValue={platform || "java"}
required
>
<SelectTrigger className="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.values(ServerPlatform).map(
(platform, index) => (
<SelectItem key={index} value={platform}>
{platform.charAt(0).toUpperCase() +
platform.substring(1)}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
{/* Hostname Query */}
<div className="w-full flex flex-col gap-3">
<Label htmlFor="hostname">Hostname</Label>
<Input
type="search"
name="hostname"
placeholder="Query..."
defaultValue={hostname}
required
maxLength={36}
/>
</div>
</div>
<MinecraftButton type="submit">Search</MinecraftButton>
</form>
);
};
export default ServerSearch;

@ -0,0 +1,62 @@
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/app/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-red-600/50 text-red-600 dark:border-red-600 [&>svg]:text-red-600",
},
},
defaultVariants: {
variant: "default",
},
}
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn(
"mb-1 font-medium leading-none tracking-tight",
className
)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertDescription, AlertTitle };

@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react";
import { cn } from "@/app/lib/utils" import { cn } from "@/app/lib/utils";
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {} extends React.InputHTMLAttributes<HTMLInputElement> {}
@ -11,15 +11,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-zinc-700 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all transform-gpu",
className className
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} }
) );
Input.displayName = "Input" Input.displayName = "Input";
export { Input } export { Input };

@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/app/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-zinc-700 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 transition-all transform-gpu",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}