Updates
Some checks failed
Deploy Frontend / docker (17, 3.8.5) (push) Failing after 1m55s

This commit is contained in:
Braydon 2024-04-17 20:37:49 -04:00
parent 9905673b1e
commit a1dfa6b6fa
10 changed files with 418 additions and 147 deletions

Binary file not shown.

@ -13,10 +13,13 @@
"dependencies": { "dependencies": {
"@heroicons/react": "^2.1.3", "@heroicons/react": "^2.1.3",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@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-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clipboard-copy": "^4.0.1",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"lucide-react": "^0.370.0", "lucide-react": "^0.370.0",
"next": "14.2.1", "next": "14.2.1",

@ -59,7 +59,7 @@ const PlayerPage = async ({ params }: PageProps): Promise<ReactElement> => {
</div> </div>
{/* Player Result */} {/* Player Result */}
{result && <PlayerResult player={result} />} {result && <PlayerResult query={query} player={result} />}
</div> </div>
</div> </div>
</main> </main>
@ -92,11 +92,13 @@ export const generateMetadata = async ({
if (code === 400) { if (code === 400) {
return Embed({ return Embed({
title: "Invalid Player", title: "Invalid Player",
color: "#EB4034",
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",
color: "#EB4034",
description: "The player you searched for was not found.", description: "The player you searched for was not found.",
}); });
} }

@ -0,0 +1,28 @@
"use client";
import { ReactElement, ReactNode } from "react";
import copy from "clipboard-copy";
/**
* Props for the copy button.
*/
type CopyButtonProps = {
/**
* The content to copy when
* this component is clicked.
*/
content: string;
/**
* The children to render in this button.
*/
children: ReactNode;
};
/**
* A component that copies
*/
const CopyButton = ({ content, children }: CopyButtonProps): ReactElement => (
<button onClick={async () => await copy(content)}>{children}</button>
);
export default CopyButton;

@ -9,6 +9,12 @@ type EmbedProps = {
*/ */
title: string; title: string;
/**
* The color of this embed, undefined
* for no custom color.
*/
color?: string | undefined;
/** /**
* The description of the embed. * The description of the embed.
*/ */
@ -28,6 +34,7 @@ type EmbedProps = {
*/ */
const Embed = ({ const Embed = ({
title, title,
color,
description, description,
thumbnail = "", thumbnail = "",
}: EmbedProps): Metadata => { }: EmbedProps): Metadata => {
@ -45,6 +52,7 @@ const Embed = ({
twitter: { twitter: {
card: "summary", card: "summary",
}, },
themeColor: color,
}; };
}; };
export default Embed; export default Embed;

@ -8,7 +8,7 @@ import { ReactElement } from "react";
*/ */
const StatisticCounters = (): ReactElement => ( 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 pointer-events-none">
<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} />

@ -4,6 +4,14 @@ import { CachedPlayer, SkinPart } from "restfulmc-lib";
import { ReactElement } from "react"; import { ReactElement } from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import config from "@/config"; import config from "@/config";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import CopyButton from "@/components/copy-button";
import * as querystring from "node:querystring";
/** /**
* The result of a player search. * The result of a player search.
@ -12,6 +20,7 @@ import config from "@/config";
* @returns the player result jsx * @returns the player result jsx
*/ */
const PlayerResult = ({ const PlayerResult = ({
query,
player: { player: {
uniqueId, uniqueId,
username, username,
@ -19,77 +28,99 @@ const PlayerResult = ({
legacy, legacy,
}, },
}: { }: {
query: string;
player: CachedPlayer; player: CachedPlayer;
}): ReactElement => ( }): ReactElement => (
<div className="relative px-2 py-3 flex flex-col items-center bg-muted rounded-xl"> <ContextMenu>
{/* Raw Json */} <ContextMenuTrigger>
<div className="absolute top-[7.25rem] right-3"> <div className="relative px-2 py-3 flex flex-col items-center bg-muted rounded-xl">
<Link {/* Raw Json */}
href={`${config.apiEndpoint}/player/${username}`} <div className="absolute top-[7.25rem] right-5">
target="_blank" <Link
> href={`${config.apiEndpoint}/player/${username}`}
<Badge className="bg-minecraft-green-2 hover:bg-minecraft-green-2 hover:opacity-85 text-white font-semibold uppercase transition-all transform-gpu"> target="_blank"
Raw Json >
</Badge> <Badge className="bg-minecraft-green-2 hover:bg-minecraft-green-2 hover:opacity-85 text-white font-semibold uppercase transition-all transform-gpu">
</Link> Raw Json
</div> </Badge>
</Link>
</div>
<div className="w-full flex flex-col gap-3 justify-center items-center divide-y divide-zinc-700"> <div className="w-full flex flex-col gap-3 justify-center items-center 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"> <h1 className="text-xl font-bold text-minecraft-green-3">
{username} {username}
</h1> </h1>
<code className="text-xs xs:text-sm text-zinc-300"> <code className="text-xs xs:text-sm text-zinc-300">
{uniqueId} {uniqueId}
</code> </code>
{/* Legacy Badge */} {/* Legacy Badge */}
{legacy && ( {legacy && (
<p className="text-sm font-semibold uppercase"> <p className="text-sm font-semibold uppercase">
Legacy Legacy
</p> </p>
)} )}
</div>
</div>
{/* Skin Parts */}
<div className="pt-3 w-[90%] flex flex-col gap-3">
{/* Header */}
<h1 className="font-semibold uppercase">Skin Parts</h1>
{/* Skin Parts */}
<div className="flex gap-5">
{Object.entries(parts)
.filter(
([part]) =>
part === SkinPart.HEAD ||
part === SkinPart.FACE ||
part === SkinPart.BODY_FLAT
)
.map(([part, url], index) => (
<Link
key={index}
href={url}
target="_blank"
>
<img
className="h-20 sm:h-24 md:h-28 hover:scale-[1.02] transition-all transform-gpu"
src={url}
alt={`${username}'s ${part}`}
/>
</Link>
))}
</div>
</div>
</div> </div>
</div> </div>
</ContextMenuTrigger>
{/* Skin Parts */} <ContextMenuContent>
<div className="pt-3 w-[90%] flex flex-col gap-3"> <ContextMenuItem>
{/* Header */} <CopyButton content={username}>Copy Player Username</CopyButton>
<h1 className="font-semibold uppercase">Skin Parts</h1> </ContextMenuItem>
<ContextMenuItem>
{/* Skin Parts */} <CopyButton content={uniqueId}>Copy Player UUID</CopyButton>
<div className="flex gap-5"> </ContextMenuItem>
{Object.entries(parts) <ContextMenuItem>
.filter( <CopyButton content={`${config.siteUrl}/player/${query}`}>
([part]) => Copy Share URL
part === SkinPart.HEAD || </CopyButton>
part === SkinPart.FACE || </ContextMenuItem>
part === SkinPart.BODY_FLAT </ContextMenuContent>
) </ContextMenu>
.map(([part, url], index) => (
<Link key={index} href={url} target="_blank">
<img
className="h-20 sm:h-24 md:h-28 hover:scale-[1.02] transition-all transform-gpu"
src={url}
alt={`${username}'s ${part}`}
/>
</Link>
))}
</div>
</div>
</div>
</div>
); );
export default PlayerResult; export default PlayerResult;

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/app/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 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",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex 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
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex 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">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

@ -1,4 +1,3 @@
import Footer from "@/components/footer";
import Navbar from "@/components/navbar"; import Navbar from "@/components/navbar";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import config from "@/config"; import config from "@/config";

@ -1,86 +1,86 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
const { fontFamily, screens } = require("tailwindcss/defaultTheme"); const { screens } = require("tailwindcss/defaultTheme");
const config = { const config = {
darkMode: ["class"], darkMode: ["class"],
content: ["./src/app/**/*.{ts,tsx}"], content: ["./src/app/**/*.{ts,tsx}"],
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem", padding: "2rem",
screens: { screens: {
"2xl": "1400px", "2xl": "1400px",
}, },
}, },
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border))", border: "hsl(var(--border))",
input: "hsl(var(--input))", input: "hsl(var(--input))",
ring: "hsl(var(--ring))", ring: "hsl(var(--ring))",
background: "hsl(var(--background))", background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))", foreground: "hsl(var(--foreground))",
primary: { primary: {
DEFAULT: "hsl(var(--primary))", DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))", foreground: "hsl(var(--primary-foreground))",
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))", foreground: "hsl(var(--secondary-foreground))",
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive))", DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))", foreground: "hsl(var(--destructive-foreground))",
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))", foreground: "hsl(var(--muted-foreground))",
}, },
accent: { accent: {
DEFAULT: "hsl(var(--accent))", DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))", foreground: "hsl(var(--accent-foreground))",
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover))", DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))", foreground: "hsl(var(--popover-foreground))",
}, },
card: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))", foreground: "hsl(var(--card-foreground))",
}, },
// Custom Colors // Custom Colors
"navbar-background": "hsl(var(--navbar-background))", "navbar-background": "hsl(var(--navbar-background))",
"minecraft-green-1": "hsl(var(--minecraft-green-1))", "minecraft-green-1": "hsl(var(--minecraft-green-1))",
"minecraft-green-2": "hsl(var(--minecraft-green-2))", "minecraft-green-2": "hsl(var(--minecraft-green-2))",
"minecraft-green-3": "hsl(var(--minecraft-green-3))", "minecraft-green-3": "hsl(var(--minecraft-green-3))",
"minecraft-green-4": "hsl(var(--minecraft-green-4))", "minecraft-green-4": "hsl(var(--minecraft-green-4))",
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: "var(--radius)",
md: "calc(var(--radius) - 2px)", md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)", sm: "calc(var(--radius) - 4px)",
}, },
keyframes: { keyframes: {
"accordion-down": { "accordion-down": {
from: { height: "0" }, from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" }, to: { height: "var(--radix-accordion-content-height)" },
}, },
"accordion-up": { "accordion-up": {
from: { height: "var(--radix-accordion-content-height)" }, from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" }, to: { height: "0" },
}, },
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
}, },
}, },
screens: { screens: {
xs: "475px", xs: "475px",
...screens, ...screens,
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [require("tailwindcss-animate")],
} satisfies Config; } satisfies Config;
export default config; export default config;