Add code dialogs

This commit is contained in:
Braydon 2024-04-21 23:55:38 -04:00
parent 060e991741
commit fb0b82edd3
6 changed files with 197 additions and 81 deletions

@ -0,0 +1,75 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import CodeHighlighter from "@/components/code/code-highlighter";
import { ReactElement } from "react";
/**
* Props for the code dialog.
*/
type CodeDialogProps = {
/**
* The title of this dialog.
*/
title: string;
/**
* The description of this dialog.
*/
description: string;
/**
* The language of the code, if any
*/
language: string | undefined;
/**
* The trigger to open this dialog.
*/
trigger: ReactElement;
/**
* The code in the dialog.
*/
children: string;
};
/**
* A dialog to display code.
*
* @param title the title of this dialog
* @param description the description of this dialog
* @param trigger the trigger to open this dialog
* @param language the language of the code
* @param children the children (code) to display
* @return the dialog jsx
*/
const CodeDialog = ({
title,
description,
language,
trigger,
children,
}: CodeDialogProps) => (
<Dialog>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent className="max-w-none w-[90vw] lg:w-[85vw] xl:w-[80vw] 2xl:w-[70vw] max-h-[90vh]">
{/* Header */}
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
{/* Code */}
<CodeHighlighter className="max-h-[70vh]" language={language}>
{children}
</CodeHighlighter>
</DialogContent>
</Dialog>
);
export default CodeDialog;

@ -3,11 +3,17 @@ import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import Image from "next/image"; import Image from "next/image";
import { capitalize } from "@/lib/string-utils"; import { capitalize } from "@/lib/string-utils";
import { cn } from "@/lib";
/** /**
* Props for the code highlighter. * Props for the code highlighter.
*/ */
type CodeHighlighterProps = { type CodeHighlighterProps = {
/**
* The class name for the code block.
*/
className?: string | undefined;
/** /**
* The language of the code, if any * The language of the code, if any
*/ */
@ -22,26 +28,33 @@ type CodeHighlighterProps = {
/** /**
* A code highlighter component. * A code highlighter component.
* *
* @param className the class name for the code block
* @param language the language for syntax highlighting * @param language the language for syntax highlighting
* @param children the children (code) to highlight * @param children the children (code) to highlight
* @return the highlighter jsx * @return the highlighter jsx
*/ */
const CodeHighlighter = ({ const CodeHighlighter = ({
className,
language, language,
children, children,
}: CodeHighlighterProps): ReactElement => { }: CodeHighlighterProps): ReactElement => {
// Return a basic code block if no language is provided // Return a basic code block if no language is provided
if (!language) { if (!language) {
return ( return (
<code className="p-2 max-w-5xl block bg-muted/70 break-all rounded-lg"> <code
className={cn(
"p-2 block bg-muted/70 break-all rounded-lg",
className
)}
>
{children} {children}
</code> </code>
); );
} }
return ( return (
<div className="relative max-w-5xl"> <div className="relative">
<SyntaxHighlighter <SyntaxHighlighter
className="!bg-muted/70 break-all rounded-lg" className={cn("!bg-muted/70 break-all rounded-lg", className)}
language={language} language={language}
style={atomOneDark} style={atomOneDark}
wrapLongLines wrapLongLines

@ -10,10 +10,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import SyntaxHighlighter from "react-syntax-highlighter"; import CodeHighlighter from "@/components/code/code-highlighter";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import Image from "next/image";
import CodeHighlighter from "@/components/code-highlighter";
/** /**
* The MDX components to style. * The MDX components to style.
@ -73,7 +70,10 @@ const components: any = {
className: string; className: string;
children: any; children: any;
}): ReactElement => ( }): ReactElement => (
<CodeHighlighter language={className?.replace("language-", "")}> <CodeHighlighter
className="max-w-5xl"
language={className?.replace("language-", "")}
>
{children} {children}
</CodeHighlighter> </CodeHighlighter>
), ),

@ -1,5 +1,4 @@
import CopyButton from "@/components/copy-button"; import CopyButton from "@/components/copy-button";
import { Badge } from "@/components/ui/badge";
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
@ -11,6 +10,8 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { ReactElement } from "react"; import { ReactElement } from "react";
import { CachedPlayer, SkinPart } from "restfulmc-lib"; import { CachedPlayer, SkinPart } from "restfulmc-lib";
import CodeDialog from "@/components/code/code-dialog";
import RawJsonBadge from "@/components/raw-json-badge";
/** /**
* The props for a player result. * The props for a player result.
@ -30,41 +31,35 @@ type PlayerResultProps = {
/** /**
* The result of a player search. * The result of a player search.
* *
* @param query the original query to lookup this player
* @param player the player to display * @param player the player to display
* @returns the player result jsx * @returns the player result jsx
*/ */
const PlayerResult = ({ const PlayerResult = ({ query, player }: PlayerResultProps): ReactElement => (
query,
player: {
uniqueId,
username,
skin: { parts },
legacy,
},
}: PlayerResultProps): ReactElement => (
<ContextMenu> <ContextMenu>
<ContextMenuTrigger> <ContextMenuTrigger>
<div className="relative px-2 py-7 flex flex-col items-center bg-muted rounded-xl"> <div className="relative px-2 py-7 flex flex-col items-center bg-muted rounded-xl">
{/* Raw Json */} {/* Raw Json */}
<div className="absolute top-2 right-2"> <div className="absolute top-2 right-2">
<Link <CodeDialog
href={`${config.apiEndpoint}/player/${username}`} title="Raw Player Data"
target="_blank" description={`The raw JSON data for the player ${player.username}:`}
language="json"
trigger={<RawJsonBadge />}
> >
<Badge className="bg-minecraft-green-2 hover:bg-minecraft-green-2 hover:opacity-85 text-white font-semibold uppercase transition-all transform-gpu"> {JSON.stringify(player, undefined, 4)}
Raw Json </CodeDialog>
</Badge>
</Link>
</div> </div>
{/* Result */}
<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 pointer-events-none" className="w-24 h-24 sm:w-28 sm:h-28 md:w-32 md:h-32 pointer-events-none"
src={parts.HEAD} src={player.skin.parts.HEAD}
alt={`${username}'s Head`} alt={`${player.username}'s Head`}
width={128} width={128}
height={128} height={128}
/> />
@ -72,14 +67,14 @@ const PlayerResult = ({
{/* 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} {player.username}
</h1> </h1>
<code className="text-xs xs:text-sm text-zinc-300"> <code className="text-xs xs:text-sm text-zinc-300">
{uniqueId} {player.uniqueId}
</code> </code>
{/* Legacy Badge */} {/* Legacy Badge */}
{legacy && ( {player.legacy && (
<p className="text-sm font-semibold uppercase"> <p className="text-sm font-semibold uppercase">
Legacy Legacy
</p> </p>
@ -94,7 +89,7 @@ const PlayerResult = ({
{/* Skin Parts */} {/* Skin Parts */}
<div className="flex gap-5"> <div className="flex gap-5">
{Object.entries(parts) {Object.entries(player.skin.parts)
.filter( .filter(
([part]) => ([part]) =>
part === SkinPart.HEAD || part === SkinPart.HEAD ||
@ -114,7 +109,7 @@ const PlayerResult = ({
<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={`${player.username}'s ${part}`}
/> />
</Link> </Link>
) )
@ -125,11 +120,11 @@ const PlayerResult = ({
</div> </div>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent className="flex flex-col"> <ContextMenuContent className="flex flex-col">
<CopyButton content={username} showToast> <CopyButton content={player.username} showToast>
<ContextMenuItem>Copy Player Username</ContextMenuItem> <ContextMenuItem>Copy Player Username</ContextMenuItem>
</CopyButton> </CopyButton>
<CopyButton content={uniqueId} showToast> <CopyButton content={player.uniqueId} showToast>
<ContextMenuItem>Copy Player UUID</ContextMenuItem> <ContextMenuItem>Copy Player UUID</ContextMenuItem>
</CopyButton> </CopyButton>

@ -0,0 +1,14 @@
import { ReactElement } from "react";
import { Badge } from "@/components/ui/badge";
/**
* A badge for raw json.
*
* @returns the raw json badge jsx
*/
const RawJsonBadge = (): ReactElement => (
<Badge className="bg-minecraft-green-2 hover:bg-minecraft-green-2 hover:opacity-85 text-white font-semibold uppercase transition-all transform-gpu">
Raw Json
</Badge>
);
export default RawJsonBadge;

@ -7,6 +7,8 @@ import {
} from "restfulmc-lib"; } from "restfulmc-lib";
import config from "@/config"; import config from "@/config";
import { minecraft } from "@/font/fonts"; import { minecraft } from "@/font/fonts";
import CodeDialog from "@/components/code/code-dialog";
import RawJsonBadge from "@/components/raw-json-badge";
/** /**
* The props for a server result. * The props for a server result.
@ -28,58 +30,75 @@ const ServerResult = ({ server }: ServerResultProps): ReactElement => {
const favicon: string | undefined = (server as CachedJavaMinecraftServer) const favicon: string | undefined = (server as CachedJavaMinecraftServer)
.favicon?.url; // The favicon of the server .favicon?.url; // The favicon of the server
return ( return (
<div <div className="flex flex-col gap-3">
className={cn( {/* Result */}
"w-[37.5rem] relative p-1.5 flex gap-2 items-center pointer-events-none", <div
`bg-[url("/media/dirt-background.png")] bg-center bg-repeat ${minecraft.className}` className={cn(
)} "w-[37.5rem] relative p-1.5 flex gap-2 items-center pointer-events-none",
> `bg-[url("/media/dirt-background.png")] bg-center bg-repeat ${minecraft.className}`
{/* Favicon */} )}
<Image >
className="h-16 w-16" {/* Favicon */}
src={favicon || `${config.apiEndpoint}/server/icon/fallback`} <Image
alt={`${server.hostname}'s Favicon`} className="h-16 w-16"
width={64} src={
height={64} favicon || `${config.apiEndpoint}/server/icon/fallback`
/> }
alt={`${server.hostname}'s Favicon`}
width={64}
height={64}
/>
{/* Content */} {/* Content */}
<div className="-translate-y-1 w-full h-full flex flex-col gap-1"> <div className="-translate-y-1 w-full h-full flex flex-col gap-1">
{/* Name & Ping */} {/* Name & Ping */}
<div className="w-full flex justify-between"> <div className="w-full flex justify-between">
<h1>{server.hostname}</h1> <h1>{server.hostname}</h1>
{/* Players & Ping */} {/* Players & Ping */}
<div className="flex gap-1 items-center"> <div className="flex gap-1 items-center">
<p className="text-[#AAAAAA]"> <p className="text-[#AAAAAA]">
{server.players.online} {server.players.online}
<span className="text-[#555555]">/</span> <span className="text-[#555555]">/</span>
{server.players.max} {server.players.max}
</p> </p>
<Image <Image
src="/media/ping-full.png" src="/media/ping-full.png"
alt="Ping!" alt="Ping!"
width={20} width={20}
height={20} height={20}
/> />
</div>
</div>
{/* MOTD */}
<div className="w-full flex flex-col">
{server.motd.html.map(
(line: string, index: number): ReactElement => {
return (
<p
key={index}
className="leading-[1.15rem] pointer-events-auto"
dangerouslySetInnerHTML={{
__html: line,
}}
></p>
);
}
)}
</div> </div>
</div> </div>
{/* MOTD */}
<div className="w-full flex flex-col">
{server.motd.html.map(
(line: string, index: number): ReactElement => {
return (
<p
key={index}
className="leading-[1.15rem] pointer-events-auto"
dangerouslySetInnerHTML={{ __html: line }}
></p>
);
}
)}
</div>
</div> </div>
{/* Raw Json */}
<CodeDialog
title="Raw Server Data"
description={`The raw JSON data for the player ${server.hostname}:`}
language="json"
trigger={<RawJsonBadge />}
>
{JSON.stringify(server, undefined, 4)}
</CodeDialog>
</div> </div>
); );
}; };