This commit is contained in:
Braydon 2024-04-17 19:04:15 -04:00
parent 6a9ec1fe9f
commit b560341068
16 changed files with 451 additions and 415 deletions

1
Frontend/.gitignore vendored

@ -1,5 +1,6 @@
node_modules node_modules
.next/ .next/
.fleet/
.vscode/ .vscode/
.env*.local .env*.local
next-env.d.ts next-env.d.ts

4
Frontend/.prettierrc Normal file

@ -0,0 +1,4 @@
{
"trailingComma": "es5",
"tabWidth": 4
}

@ -3,12 +3,13 @@ import { minecrafter } from "@/font/fonts";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Metadata } from "next"; import { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { ReactElement } from "react";
/** /**
* Page metadata. * Page metadata.
*/ */
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Docs", title: "Docs",
}; };
/** /**
@ -16,34 +17,34 @@ export const metadata: Metadata = {
* *
* @returns the page jsx * @returns the page jsx
*/ */
const DocsPage = (): JSX.Element => ( const DocsPage = (): ReactElement => (
<main className="h-[64vh] flex flex-col gap-3 justify-center items-center"> <main className="h-[64vh] flex flex-col gap-3 justify-center items-center">
{/* Creeper */} {/* Creeper */}
<div className="absolute left-28 bottom-16 pointer-events-none"> <div className="absolute left-28 bottom-16 pointer-events-none">
<Creeper /> <Creeper />
</div> </div>
{/* Header */} {/* Header */}
<h1 <h1
className={cn( className={cn(
"text-6xl text-minecraft-green-3 pointer-events-none", "text-6xl text-minecraft-green-3 pointer-events-none",
minecrafter.className minecrafter.className
)} )}
> >
Documentation Documentation
</h1> </h1>
{/* Content */} {/* Content */}
<h2 className="text-xl"> <h2 className="text-xl">
This page is still under construction, however we do have a{" "} This page is still under construction, however we do have a{" "}
<Link <Link
className="text-minecraft-green-4" className="text-minecraft-green-4"
href="https://git.rainnny.club/Rainnny/RESTfulMC/wiki" href="https://git.rainnny.club/Rainnny/RESTfulMC/wiki"
> >
Wiki Wiki
</Link> </Link>
! !
</h2> </h2>
</main> </main>
); );
export default DocsPage; export default DocsPage;

@ -7,11 +7,11 @@ import StatisticCounters from "@/components/landing/statistic-counters";
* *
* @returns the page jsx * @returns the page jsx
*/ */
const LandingPage = (): JSX.Element => ( const LandingPage = (): ReactElement => (
<main className="px-3"> <main className="px-3">
<Hero /> <Hero />
<FeaturedContent /> <FeaturedContent />
<StatisticCounters /> <StatisticCounters />
</main> </main>
); );
export default LandingPage; export default LandingPage;

@ -7,62 +7,63 @@ import { PageProps } from "@/types/page";
import { Metadata } from "next"; import { Metadata } from "next";
import Image from "next/image"; import Image from "next/image";
import { CachedPlayer, getPlayer, type RestfulMCAPIError } from "restfulmc-lib"; import { CachedPlayer, getPlayer, type RestfulMCAPIError } from "restfulmc-lib";
import { ReactElement } from "react";
/** /**
* The page to lookup a player. * The page to lookup a player.
* *
* @returns the page jsx * @returns the page jsx
*/ */
const PlayerPage = async ({ params }: PageProps): Promise<JSX.Element> => { const PlayerPage = async ({ params }: PageProps): Promise<ReactElement> => {
let error: string | undefined = undefined; // The error to display let error: string | undefined = undefined; // The error to display
let result: CachedPlayer | undefined = undefined; // The player to display let result: CachedPlayer | undefined = undefined; // The player to display
const query: string | undefined = trimQuery(params.slug?.[0]); // The query to search for const query: string | undefined = trimQuery(params.slug?.[0]); // The query to search for
// Try and get the player to display // Try and get the player to display
try { try {
result = query ? await getPlayer(query) : undefined; result = query ? await getPlayer(query) : undefined;
} catch (err) { } catch (err) {
error = (err as RestfulMCAPIError).message; // Set the error message error = (err as RestfulMCAPIError).message; // Set the error message
} }
// 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="mt-0 sm:mt-[45rem] xl:mt-0 flex flex-col xl:flex-row xl:gap-24 2xl:gap-48 transition-all transform-gpu">
{/* Banner */} {/* Banner */}
<Image <Image
className="hidden sm:flex xl:my-auto h-[28rem] pointer-events-none" className="hidden sm:flex xl:my-auto h-[28rem] pointer-events-none"
src="/media/players.webp" src="/media/players.webp"
alt="Minecraft Players" alt="Minecraft Players"
width={632} width={632}
height={632} height={632}
/> />
{/* Search */} {/* Search */}
<div className="pb-16 xl:pb-0 flex flex-col gap-7"> <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",
minecrafter.className minecrafter.className
)} )}
> >
Player Lookup Player Lookup
</h1> </h1>
<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 && <p className="text-red-500">{error}</p>}
{/* Search */} {/* Search */}
<PlayerSearch query={query} /> <PlayerSearch query={query} />
</div> </div>
{/* Player Result */} {/* Player Result */}
{result && <PlayerResult player={result} />} {result && <PlayerResult player={result} />}
</div> </div>
</div> </div>
</main> </main>
); );
}; };
/** /**
@ -73,35 +74,35 @@ const PlayerPage = async ({ params }: PageProps): Promise<JSX.Element> => {
* @returns the generated metadata * @returns the generated metadata
*/ */
export const generateMetadata = async ({ export const generateMetadata = async ({
params, params,
}: PageProps): Promise<Metadata> => { }: PageProps): Promise<Metadata> => {
const query: string | undefined = trimQuery(params.slug?.[0]); // The query to embed for const query: string | undefined = trimQuery(params.slug?.[0]); // The query to embed for
// Try and get the player to display // Try and get the player to display
if (query) { if (query) {
try { try {
const player: CachedPlayer = await getPlayer(query); // Get the player to embed const player: CachedPlayer = await getPlayer(query); // Get the player to embed
return Embed({ return Embed({
title: `${player.username}'s Profile`, title: `${player.username}'s Profile`,
description: `UUID: ${player.uniqueId}\n\nClick to view data about this player.`, description: `UUID: ${player.uniqueId}\n\nClick to view data about this player.`,
thumbnail: player.skin.parts.HEAD, thumbnail: player.skin.parts.HEAD,
}); });
} catch (err) { } catch (err) {
const code: number = (err as RestfulMCAPIError).code; // Get the error status code const code: number = (err as RestfulMCAPIError).code; // Get the error status code
if (code === 400) { if (code === 400) {
return Embed({ return Embed({
title: "Invalid Player", title: "Invalid Player",
description: "The player you searched for is invalid.", description: "The player you searched for is invalid.",
}); });
} else if (code === 404) { } else if (code === 404) {
return Embed({ return Embed({
title: "Player Not Found", title: "Player Not Found",
description: "The player you searched for was not found.", description: "The player you searched for was not found.",
}); });
} }
} }
} }
return {}; return {};
}; };
/** /**
@ -111,11 +112,11 @@ export const generateMetadata = async ({
* @returns the trimmed query * @returns the trimmed query
*/ */
const trimQuery = (query: string | undefined): string | undefined => { const trimQuery = (query: string | undefined): string | undefined => {
// Limit the query to 36 chars // Limit the query to 36 chars
if (query && query.length > 36) { if (query && query.length > 36) {
query = query.substr(0, 36); query = query.substr(0, 36);
} }
return query; return query;
}; };
export default PlayerPage; export default PlayerPage;

@ -3,28 +3,29 @@
import { minecrafter } from "@/font/fonts"; import { minecrafter } from "@/font/fonts";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import CountUp from "react-countup"; import CountUp from "react-countup";
import { ReactElement } from "react";
/** /**
* Props for the counter. * Props for the counter.
*/ */
type CounterProps = { type CounterProps = {
/** /**
* The name of the counter. * The name of the counter.
*/ */
name: string; name: string;
/** /**
* The amount to count up to. * The amount to count up to.
*/ */
amount: number; amount: number;
/** /**
* The optional duration of the count up. * The optional duration of the count up.
* <p> * <p>
* Uses the default duration if not provided. * Uses the default duration if not provided.
* </p> * </p>
*/ */
duration?: number | undefined; duration?: number | undefined;
}; };
/** /**
@ -34,16 +35,19 @@ type CounterProps = {
* @param duration the optional duration of the count up * @param duration the optional duration of the count up
* @returns the counter jsx * @returns the counter jsx
*/ */
const Counter = ({ name, amount, duration }: CounterProps): JSX.Element => ( const Counter = ({ name, amount, duration }: CounterProps): ReactElement => (
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<h1 <h1
className={cn("text-6xl text-minecraft-green-3", minecrafter.className)} className={cn(
> "text-6xl text-minecraft-green-3",
{name} minecrafter.className
</h1> )}
<h2 className="text-4xl font-semibold uppercase"> >
<CountUp start={0} end={amount} duration={duration} /> {name}
</h2> </h1>
</div> <h2 className="text-4xl font-semibold uppercase">
<CountUp start={0} end={amount} duration={duration} />
</h2>
</div>
); );
export default Counter; export default Counter;

@ -1,16 +1,17 @@
import Image from "next/image"; import Image from "next/image";
import { ReactElement } from "react";
/** /**
* A creeper image. * A creeper image.
* *
* @returns the creeper jsx * @returns the creeper jsx
*/ */
const Creeper = (): JSX.Element => ( const Creeper = (): ReactElement => (
<Image <Image
src="/media/creeper.png" src="/media/creeper.png"
alt="A Minecraft Creeper" alt="A Minecraft Creeper"
width={216} width={216}
height={216} height={216}
/> />
); );
export default Creeper; export default Creeper;

@ -1,2 +1,4 @@
const Footer = (): JSX.Element => <footer>FOOTER</footer>; import { ReactElement } from "react";
const Footer = (): ReactElement => <footer>FOOTER</footer>;
export default Footer; export default Footer;

@ -4,28 +4,30 @@ import { StarIcon } from "@heroicons/react/24/outline";
import Link from "next/link"; import Link from "next/link";
import { Suspense } from "react"; import { Suspense } from "react";
const GitHubStarButton = async (): Promise<JSX.Element> => { const GitHubStarButton = async (): Promise<ReactElement> => {
return ( return (
<Link <Link
href="https://github.com/Rainnny7/RESTfulMC" href="https://github.com/Rainnny7/RESTfulMC"
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
<MinecraftButton className="flex gap-1.5 items-center group/star"> <MinecraftButton className="flex gap-1.5 items-center group/star">
{/* Star Count */} {/* Star Count */}
<Suspense fallback={<Skeleton className="w-4 h-5 rounded-md" />}> <Suspense
<GitHubStarCount /> fallback={<Skeleton className="w-4 h-5 rounded-md" />}
</Suspense> >
<GitHubStarCount />
</Suspense>
<StarIcon <StarIcon
className="group-hover/star:text-orange-400 delay-0 transition-all transform-gpu" className="group-hover/star:text-orange-400 delay-0 transition-all transform-gpu"
width={22} width={22}
height={22} height={22}
/> />
<span>Star on GitHub</span> <span>Star on GitHub</span>
</MinecraftButton> </MinecraftButton>
</Link> </Link>
); );
}; };
/** /**
@ -34,10 +36,10 @@ const GitHubStarButton = async (): Promise<JSX.Element> => {
* @returns the star count jsx * @returns the star count jsx
*/ */
const GitHubStarCount = async (): Promise<JSX.Element> => { const GitHubStarCount = async (): Promise<JSX.Element> => {
const stars: number = await getStarCount(); // Get the repo star count const stars: number = await getStarCount(); // Get the repo star count
return ( return (
<code className="px-1 rounded-md bg-minecraft-green-3/80">{stars}</code> <code className="px-1 rounded-md bg-minecraft-green-3/80">{stars}</code>
); );
}; };
/** /**
@ -47,12 +49,12 @@ const GitHubStarCount = async (): Promise<JSX.Element> => {
* @returns the star count * @returns the star count
*/ */
const getStarCount = async (): Promise<number> => { const getStarCount = async (): Promise<number> => {
const response: Response = await fetch( const response: Response = await fetch(
"https://api.github.com/repos/Rainnny7/RESTfulMC", "https://api.github.com/repos/Rainnny7/RESTfulMC",
{ next: { revalidate: 300 } } // Revalidate every 5 minutes { next: { revalidate: 300 } } // Revalidate every 5 minutes
); );
const json: any = await response.json(); // Get the JSON response const json: any = await response.json(); // Get the JSON response
return json.stargazers_count; // Return the stars return json.stargazers_count; // Return the stars
}; };
export default GitHubStarButton; export default GitHubStarButton;

@ -3,20 +3,23 @@ import { minecrafter } from "@/font/fonts";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { FeaturedItemProps } from "@/types/config"; import { FeaturedItemProps } from "@/types/config";
import Link from "next/link"; import Link from "next/link";
import { ReactElement } from "react";
/** /**
* The featured content component. * The featured content component.
* *
* @returns the featured content jsx * @returns the featured content jsx
*/ */
const FeaturedContent = (): JSX.Element => ( const FeaturedContent = (): ReactElement => (
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
<div className="max-w-2xl flex flex-wrap justify-center gap-5"> <div className="max-w-2xl flex flex-wrap justify-center gap-5">
{config.featuredItems.map((item, index) => ( {config.featuredItems.map(
<FeaturedItem key={index} {...item} /> (item: FeaturedItemProps, index: number) => (
))} <FeaturedItem key={index} {...item} />
</div> )
</div> )}
</div>
</div>
); );
/** /**
@ -26,25 +29,28 @@ const FeaturedContent = (): JSX.Element => (
* @returns the item jsx * @returns the item jsx
*/ */
const FeaturedItem = ({ const FeaturedItem = ({
name, name,
description, description,
image, image,
href, href,
}: FeaturedItemProps): JSX.Element => ( }: FeaturedItemProps): ReactElement => (
<Link <Link
className="pt-28 w-[19rem] h-80 flex flex-col gap-1 items-center bg-center bg-cover bg-no-repeat rounded-3xl text-center backdrop-blur-md hover:scale-[1.01] transition-all transform-gpu" className="pt-28 w-[19rem] h-80 flex flex-col gap-1 items-center bg-center bg-cover bg-no-repeat rounded-3xl text-center backdrop-blur-md hover:scale-[1.01] transition-all transform-gpu"
href={href} href={href}
style={{ style={{
backgroundImage: `url(${image})`, backgroundImage: `url(${image})`,
}} }}
> >
<h1 <h1
className={cn("text-3xl font-semibold text-white", minecrafter.className)} className={cn(
> "text-3xl font-semibold text-white",
{name} minecrafter.className
</h1> )}
<h2 className="text-md max-w-[15rem]">{description}</h2> >
</Link> {name}
</h1>
<h2 className="text-md max-w-[15rem]">{description}</h2>
</Link>
); );
export default FeaturedContent; export default FeaturedContent;

@ -4,40 +4,43 @@ import config from "@/config";
import { minecrafter } from "@/font/fonts"; import { minecrafter } from "@/font/fonts";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Link from "next/link"; import Link from "next/link";
import { ReactElement } from "react";
/** /**
* The hero content. * The hero content.
* *
* @returns the hero jsx * @returns the hero jsx
*/ */
const Hero = (): JSX.Element => ( const Hero = (): ReactElement => (
<div className="pt-56 pb-40 flex flex-col gap-8 justify-center items-center"> <div className="pt-56 pb-40 flex flex-col gap-8 justify-center items-center">
<div className="flex flex-col gap-4 items-center text-center"> <div className="flex flex-col gap-4 items-center text-center">
{/* Title */} {/* Title */}
<h1 <h1
className={cn( className={cn(
"text-5xl sm:text-6xl text-minecraft-green-3", "text-5xl sm:text-6xl text-minecraft-green-3",
minecrafter.className minecrafter.className
)} )}
> >
{config.siteName} {config.siteName}
</h1> </h1>
{/* Subtitle */} {/* Subtitle */}
<h2 className="text-xl">{config.metadata.description}</h2> <h2 className="text-xl">{config.metadata.description}</h2>
</div> </div>
{/* Links */} {/* Links */}
<div className="flex gap-5 xs:gap-10"> <div className="flex gap-5 xs:gap-10">
<Link href="/docs"> <Link href="/docs">
<MinecraftButton className="w-44 h-12">Get Started</MinecraftButton> <MinecraftButton className="w-44 h-12">
</Link> Get Started
</MinecraftButton>
</Link>
{/* Star on Github <3 */} {/* Star on Github <3 */}
<div className="md:hidden"> <div className="md:hidden">
<GitHubStarButton /> <GitHubStarButton />
</div> </div>
</div> </div>
</div> </div>
); );
export default Hero; export default Hero;

@ -1,18 +1,19 @@
import Counter from "@/components/counter"; import Counter from "@/components/counter";
import { ReactElement } from "react";
/** /**
* The statistic counters component. * The statistic counters component.
* *
* @returns the counters jsx * @returns the counters jsx
*/ */
const StatisticCounters = (): JSX.Element => ( const StatisticCounters = (): ReactElement => (
<div className="py-56 flex justify-center items-center"> <div className="py-56 flex justify-center items-center">
<div className="grid grid-flow-row grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-24"> <div className="grid grid-flow-row grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-24">
<Counter name="Testing" amount={1_000_000} /> <Counter name="Testing" amount={1_000_000} />
<Counter name="Testing" amount={1_000_000} /> <Counter name="Testing" amount={1_000_000} />
<Counter name="Testing" amount={1_000_000} /> <Counter name="Testing" amount={1_000_000} />
<Counter name="Testing" amount={1_000_000} /> <Counter name="Testing" amount={1_000_000} />
</div> </div>
</div> </div>
); );
export default StatisticCounters; export default StatisticCounters;

@ -7,66 +7,69 @@ import { cn } from "@/lib/utils";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { ReactElement } from "react";
/** /**
* The navbar for the site. * The navbar for the site.
* *
* @returns the navbar jsx * @returns the navbar jsx
*/ */
const Navbar = (): JSX.Element => { const Navbar = (): ReactElement => {
const path: string = usePathname(); // Get the current path const path: string = usePathname(); // Get the current path
return ( return (
<nav className="fixed inset-x-0 flex h-16 sm:px-12 justify-center sm:justify-between items-center bg-navbar-background z-50"> <nav className="fixed inset-x-0 flex h-16 sm:px-12 justify-center sm:justify-between items-center bg-navbar-background z-50">
{/* Left */} {/* Left */}
<div className="flex gap-3 xs:gap-7 lg:gap-12 items-center transition-all transform-gpu"> <div className="flex gap-3 xs:gap-7 lg:gap-12 items-center transition-all transform-gpu">
{/* App Branding */} {/* App Branding */}
<Link <Link
className={cn( className={cn(
"text-3xl text-minecraft-green-3 hover:opacity-85 transition-all transform-gpu", "text-3xl text-minecraft-green-3 hover:opacity-85 transition-all transform-gpu",
minecrafter.className minecrafter.className
)} )}
href="/" href="/"
> >
{/* Small Screens */} {/* Small Screens */}
<Image <Image
className="lg:hidden" className="lg:hidden"
src="/media/logo.webp" src="/media/logo.webp"
alt="Site Logo" alt="Site Logo"
width={42} width={42}
height={42} height={42}
/> />
{/* Large Screens */} {/* Large Screens */}
<span className="hidden lg:flex">{config.siteName}</span> <span className="hidden lg:flex">{config.siteName}</span>
</Link> </Link>
{/* Links */} {/* Links */}
<div className="flex gap-7"> <div className="flex gap-7">
{Object.entries(config.navbarLinks).map((link, index) => { {Object.entries(config.navbarLinks).map(
const url: string = link[1]; // The href of the link (link: [string, string], index: number) => {
let active: boolean = path.startsWith(url); // Is this the active link? const url: string = link[1]; // The href of the link
return ( let active: boolean = path.startsWith(url); // Is this the active link?
<Link return (
key={index} <Link
className={cn( key={index}
"font-semibold uppercase hover:text-minecraft-green-4 transition-all transform-gpu", className={cn(
active && "text-minecraft-green-4" "font-semibold uppercase hover:text-minecraft-green-4 transition-all transform-gpu",
)} active && "text-minecraft-green-4"
href={url} )}
> href={url}
{link[0]} >
</Link> {link[0]}
); </Link>
})} );
</div> }
</div> )}
</div>
</div>
{/* Social Buttons - Right */} {/* Social Buttons - Right */}
<div className="hidden md:flex"> <div className="hidden md:flex">
{/* Star on Github <3 */} {/* Star on Github <3 */}
<GitHubStarButton /> <GitHubStarButton />
</div> </div>
</nav> </nav>
); );
}; };
export default Navbar; export default Navbar;

@ -2,6 +2,7 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { CachedPlayer, SkinPart } from "restfulmc-lib"; import { CachedPlayer, SkinPart } from "restfulmc-lib";
import { ReactElement } from "react";
/** /**
* The result of a player search. * The result of a player search.
@ -10,62 +11,68 @@ import { CachedPlayer, SkinPart } from "restfulmc-lib";
* @returns the player result jsx * @returns the player result jsx
*/ */
const PlayerResult = ({ const PlayerResult = ({
player: { player: {
uniqueId, uniqueId,
username, username,
skin: { parts }, skin: { parts },
legacy, legacy,
}, },
}: { }: {
player: CachedPlayer; player: CachedPlayer;
}): JSX.Element => ( }): ReactElement => (
<div className="px-2 py-3 flex flex-col gap-3 items-center bg-muted rounded-xl divide-y divide-zinc-700"> <div className="px-2 py-3 flex flex-col gap-3 items-center bg-muted rounded-xl divide-y divide-zinc-700">
{/* Details */} {/* Details */}
<div className="flex gap-5 items-center"> <div className="flex gap-5 items-center">
{/* Player Head */} {/* Player Head */}
<Image <Image
className="w-24 h-24 sm:w-28 sm:h-28 md:w-32 md:h-32" className="w-24 h-24 sm:w-28 sm:h-28 md:w-32 md:h-32"
src={parts.HEAD} src={parts.HEAD}
alt={`${username}'s Head`} alt={`${username}'s Head`}
width={128} width={128}
height={128} height={128}
/> />
{/* Name, Unique ID, and Badges */} {/* Name, Unique ID, and Badges */}
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<h1 className="text-xl font-bold text-minecraft-green-3">{username}</h1> <h1 className="text-xl font-bold text-minecraft-green-3">
<code className="text-xs xs:text-sm text-zinc-300">{uniqueId}</code> {username}
</h1>
<code className="text-xs xs:text-sm text-zinc-300">
{uniqueId}
</code>
{/* Legacy Badge */} {/* Legacy Badge */}
{legacy && <p className="text-sm font-semibold uppercase">Legacy</p>} {legacy && (
</div> <p className="text-sm font-semibold uppercase">Legacy</p>
</div> )}
</div>
</div>
{/* Skin Parts */} {/* Skin Parts */}
<div className="pt-3 w-[90%] flex flex-col gap-3"> <div className="pt-3 w-[90%] flex flex-col gap-3">
{/* Header */} {/* Header */}
<h1 className="font-semibold uppercase">Skin Parts</h1> <h1 className="font-semibold uppercase">Skin Parts</h1>
{/* Skin Parts */} {/* Skin Parts */}
<div className="flex gap-5"> <div className="flex gap-5">
{Object.entries(parts) {Object.entries(parts)
.filter( .filter(
([part]) => ([part]) =>
part === SkinPart.HEAD || part === SkinPart.HEAD ||
part === SkinPart.FACE || part === SkinPart.FACE ||
part === SkinPart.BODY_FLAT part === SkinPart.BODY_FLAT
) )
.map(([part, url], index) => ( .map(([part, url], index) => (
<Link key={index} href={url} target="_blank"> <Link key={index} href={url} target="_blank">
<img <img
className="h-20 sm:h-24 md:h-28 hover:scale-[1.02] transition-all transform-gpu" className="h-20 sm:h-24 md:h-28 hover:scale-[1.02] transition-all transform-gpu"
src={url} src={url}
alt={`${username}'s ${part}`} alt={`${username}'s ${part}`}
/> />
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
</div> </div>
); );
export default PlayerResult; export default PlayerResult;

@ -11,31 +11,31 @@ import { redirect } from "next/navigation";
* @returns the search component jsx * @returns the search component jsx
*/ */
const PlayerSearch = ({ const PlayerSearch = ({
query, query,
}: { }: {
query: string | undefined; query: string | undefined;
}): JSX.Element => { }): ReactElement => {
const handleRedirect = async (form: FormData) => { const handleRedirect = async (form: FormData) => {
"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 justify-center items-center"
action={handleRedirect} action={handleRedirect}
> >
<div className="w-full flex flex-col gap-3"> <div className="w-full flex flex-col gap-3">
<Label htmlFor="query">Username or UUID</Label> <Label htmlFor="query">Username or UUID</Label>
<Input <Input
type="search" type="search"
name="query" name="query"
placeholder="Query..." placeholder="Query..."
defaultValue={query} defaultValue={query}
maxLength={36} maxLength={36}
/> />
</div> </div>
<MinecraftButton type="submit">Search</MinecraftButton> <MinecraftButton type="submit">Search</MinecraftButton>
</form> </form>
); );
}; };
export default PlayerSearch; export default PlayerSearch;

@ -3,76 +3,76 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 30 5% 9%; --background: 30 5% 9%;
--foreground: 0 0% 98%; --foreground: 0 0% 98%;
--card: 240 10% 3.9%; --card: 240 10% 3.9%;
--card-foreground: 0 0% 98%; --card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%; --popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%; --popover-foreground: 0 0% 98%;
--primary: 0 0% 98%; --primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%; --primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%; --secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 0 0% 98%;
--muted: 20 4% 14%; --muted: 20 4% 14%;
--muted-foreground: 240 5% 64.9%; --muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%; --accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%; --border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%; --input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%; --ring: 240 4.9% 83.9%;
--radius: 0.5rem; --radius: 0.5rem;
/* Navbar */ /* Navbar */
--navbar-background: 0 0% 7%; --navbar-background: 0 0% 7%;
/* Minecraft Colors (Dark -> Light) */ /* Minecraft Colors (Dark -> Light) */
--minecraft-green-1: 108 56% 25%; --minecraft-green-1: 108 56% 25%;
--minecraft-green-2: 107 55% 34%; --minecraft-green-2: 107 55% 34%;
--minecraft-green-3: 104 51% 43%; --minecraft-green-3: 104 51% 43%;
--minecraft-green-4: 103 50% 53%; --minecraft-green-4: 103 50% 53%;
} }
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
/* Scrollbar (Firefox) */ /* Scrollbar (Firefox) */
scrollbar-color: hsl(var(--minecraft-green-2)) hsl(var(--background)); scrollbar-color: hsl(var(--minecraft-green-2)) hsl(var(--background));
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
/* Scrollbar (Chrome & Safari) */ /* Scrollbar (Chrome & Safari) */
@layer base { @layer base {
::-webkit-scrollbar { ::-webkit-scrollbar {
@apply w-1.5; @apply w-1.5;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@apply bg-inherit; @apply bg-inherit;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
@apply bg-minecraft-green-2 rounded-3xl; @apply bg-minecraft-green-2 rounded-3xl;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
@apply bg-opacity-80; @apply bg-opacity-80;
} }
} }