diff --git a/Frontend/bun.lockb b/Frontend/bun.lockb index 61d9b92..9ffaf77 100644 Binary files a/Frontend/bun.lockb and b/Frontend/bun.lockb differ diff --git a/Frontend/config.json b/Frontend/config.json index 3c9c2a9..9010df0 100644 --- a/Frontend/config.json +++ b/Frontend/config.json @@ -1,53 +1,53 @@ { - "siteName": "RESTfulMC", - "siteUrl": "http://localhost:3000", - "apiEndpoint": "https://api.restfulmc.cc", - "analyticsDomain": "restfulmc.cc", - "metadata": { - "title": { - "default": "RESTfulMC", - "template": "%s 🞄 RESTfulMC" - }, - "description": "A simple, yet useful RESTful API for Minecraft utilizing Springboot.", - "keywords": [ - "java", - "minecraft", - "json", - "rest-api", - "restful", - "bedrock", - "springboot" - ], - "icons": [ - { - "rel": "icon", - "type": "image/png", - "sizes": "32x32", - "url": "/media/logo.webp" - } - ] - }, - "viewport": { - "themeColor": "#3C8627" - }, - "navbarLinks": { - "Player": "/player", - "Server": "/server", - "Mojang": "/mojang", - "Docs": "/docs" - }, - "featuredItems": [ - { - "name": "Player Lookup", - "description": "Wanna find a player? You can locate them here by their username or UUID.", - "image": "/media/featured/server.png", - "href": "/player" - }, - { - "name": "Server Lookup", - "description": "Fugiat culpa est consequat nostrud exercitation ut id ex in.", - "image": "/media/featured/server.png", - "href": "/player" - } - ] + "siteName": "RESTfulMC", + "siteUrl": "http://localhost:3000", + "apiEndpoint": "https://api.restfulmc.cc", + "analyticsDomain": "restfulmc.cc", + "metadata": { + "title": { + "default": "RESTfulMC", + "template": "%s 🞄 RESTfulMC" + }, + "description": "A simple, yet useful RESTful API for Minecraft utilizing Springboot.", + "keywords": [ + "java", + "minecraft", + "json", + "rest-api", + "restful", + "bedrock", + "springboot" + ], + "icons": [ + { + "rel": "icon", + "type": "image/png", + "sizes": "32x32", + "url": "/media/logo.webp" + } + ] + }, + "viewport": { + "themeColor": "#3C8627" + }, + "navbarLinks": { + "Player": "/player", + "Server": "/server", + "Mojang": "/mojang", + "Docs": "/docs" + }, + "featuredItems": [ + { + "name": "Player Lookup", + "description": "Wanna find a player? You can locate them here by their username or UUID.", + "image": "/media/featured/server.jpg", + "href": "/player" + }, + { + "name": "Server Lookup", + "description": "Fugiat culpa est consequat nostrud exercitation ut id ex in.", + "image": "/media/featured/server.jpg", + "href": "/player" + } + ] } diff --git a/Frontend/package.json b/Frontend/package.json index a2f1f17..83432d3 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -15,6 +15,7 @@ "@hookform/resolvers": "^3.3.4", "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", diff --git a/Frontend/public/media/featured/server.jpg b/Frontend/public/media/featured/server.jpg new file mode 100644 index 0000000..4b49100 Binary files /dev/null and b/Frontend/public/media/featured/server.jpg differ diff --git a/Frontend/public/media/featured/server.png b/Frontend/public/media/featured/server.png deleted file mode 100644 index e2dbb75..0000000 Binary files a/Frontend/public/media/featured/server.png and /dev/null differ diff --git a/Frontend/public/media/full-ping.png b/Frontend/public/media/full-ping.png new file mode 100644 index 0000000..2b79892 Binary files /dev/null and b/Frontend/public/media/full-ping.png differ diff --git a/Frontend/public/media/players.webp b/Frontend/public/media/players.webp deleted file mode 100644 index 765543c..0000000 Binary files a/Frontend/public/media/players.webp and /dev/null differ diff --git a/Frontend/public/media/server-background.png b/Frontend/public/media/server-background.png new file mode 100644 index 0000000..4237611 Binary files /dev/null and b/Frontend/public/media/server-background.png differ diff --git a/Frontend/src/app/(pages)/player/[[...slug]]/page.tsx b/Frontend/src/app/(pages)/player/[[...slug]]/page.tsx index 138fe4c..2533ab9 100644 --- a/Frontend/src/app/(pages)/player/[[...slug]]/page.tsx +++ b/Frontend/src/app/(pages)/player/[[...slug]]/page.tsx @@ -1,13 +1,14 @@ import Embed from "@/components/embed"; import PlayerResult from "@/components/player/player-result"; import PlayerSearch from "@/components/player/player-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 Image from "next/image"; -import { CachedPlayer, getPlayer, type RestfulMCAPIError } from "restfulmc-lib"; import { ReactElement } from "react"; +import { CachedPlayer, getPlayer, type RestfulMCAPIError } from "restfulmc-lib"; /** * The page to lookup a player. @@ -29,38 +30,32 @@ const PlayerPage = async ({ params }: PageProps): Promise => { // Render the page return (
-
- {/* Banner */} - Minecraft Players +
+

+ Player Lookup +

- {/* Search */} -
-

- Player Lookup -

+
+ {/* Error */} + {error && ( + + + Error + {error} + + )} -
- {/* Error */} - {error &&

{error}

} - - {/* Search */} - -
- - {/* Player Result */} - {result && } + {/* Search */} +
+ + {/* Player Result */} + {result && }
); @@ -93,13 +88,13 @@ export const generateMetadata = async ({ return Embed({ title: "Invalid Player", color: "#EB4034", - description: "The player you searched for is invalid.", + description: `The player ${query} is invalid.`, }); } else if (code === 404) { return Embed({ title: "Player Not Found", color: "#EB4034", - description: "The player you searched for was not found.", + description: `The player ${query} was not found.`, }); } } diff --git a/Frontend/src/app/(pages)/server/[[...slug]]/page.tsx b/Frontend/src/app/(pages)/server/[[...slug]]/page.tsx new file mode 100644 index 0000000..6a71944 --- /dev/null +++ b/Frontend/src/app/(pages)/server/[[...slug]]/page.tsx @@ -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 => { + 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 ( +
+
+

+ Server Lookup +

+ +
+ {/* Error */} + {error && ( + + + Error + {error} + + )} + + {/* Search */} + +
+ + {/* Server Result */} + {result && ( +
+ +
+ )} +
+
+ ); +}; + +/** + * 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 => { + return {}; +}; + +export default ServerPage; diff --git a/Frontend/src/app/components/player/player-search.tsx b/Frontend/src/app/components/player/player-search.tsx index a576fd8..c9fa6c0 100644 --- a/Frontend/src/app/components/player/player-search.tsx +++ b/Frontend/src/app/components/player/player-search.tsx @@ -5,6 +5,16 @@ import { Label } from "@/components/ui/label"; import { redirect } from "next/navigation"; 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. * @@ -13,16 +23,14 @@ import { ReactElement } from "react"; */ const PlayerSearch = ({ query, -}: { - query: string | undefined; -}): ReactElement => { +}: PlayerSearchProps): ReactElement => { const handleRedirect = async (form: FormData): Promise => { "use server"; redirect(`/player/${form.get("query")}`); }; return (
@@ -32,6 +40,7 @@ const PlayerSearch = ({ name="query" placeholder="Query..." defaultValue={query} + required maxLength={36} />
diff --git a/Frontend/src/app/components/server/server-result.tsx b/Frontend/src/app/components/server/server-result.tsx new file mode 100644 index 0000000..47cddf0 --- /dev/null +++ b/Frontend/src/app/components/server/server-result.tsx @@ -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 ( +
+ {/* Favicon */} + {`${server.hostname}'s + + {/* Name & MOTD */} +
+

{server.hostname}

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

+ ); + })} +
+ + {/* Ping */} +
+

+ {server.players.online}/{server.players.max} +

+ Ping! +
+
+ ); +}; +export default ServerResult; diff --git a/Frontend/src/app/components/server/server-search.tsx b/Frontend/src/app/components/server/server-search.tsx new file mode 100644 index 0000000..8457e28 --- /dev/null +++ b/Frontend/src/app/components/server/server-search.tsx @@ -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 => { + "use server"; + redirect(`/server/${form.get("platform")}/${form.get("hostname")}`); + }; + return ( + +
+ {/* Platform Selection */} +
+ + +
+ + {/* Hostname Query */} +
+ + +
+
+ Search + + ); +}; +export default ServerSearch; diff --git a/Frontend/src/app/components/ui/alert.tsx b/Frontend/src/app/components/ui/alert.tsx new file mode 100644 index 0000000..b4eeba6 --- /dev/null +++ b/Frontend/src/app/components/ui/alert.tsx @@ -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 & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertDescription, AlertTitle }; diff --git a/Frontend/src/app/components/ui/input.tsx b/Frontend/src/app/components/ui/input.tsx index aaec5fa..bc0e2df 100644 --- a/Frontend/src/app/components/ui/input.tsx +++ b/Frontend/src/app/components/ui/input.tsx @@ -1,25 +1,25 @@ -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 - extends React.InputHTMLAttributes {} + extends React.InputHTMLAttributes {} const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { - return ( - - ) - } -) -Input.displayName = "Input" + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; -export { Input } +export { Input }; diff --git a/Frontend/src/app/components/ui/select.tsx b/Frontend/src/app/components/ui/select.tsx new file mode 100644 index 0000000..abdae28 --- /dev/null +++ b/Frontend/src/app/components/ui/select.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1 transition-all transform-gpu", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}