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

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

@ -1,5 +1,4 @@
import CopyButton from "@/components/copy-button";
import { Badge } from "@/components/ui/badge";
import {
ContextMenu,
ContextMenuContent,
@ -11,6 +10,8 @@ import Image from "next/image";
import Link from "next/link";
import { ReactElement } from "react";
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.
@ -30,41 +31,35 @@ type PlayerResultProps = {
/**
* The result of a player search.
*
* @param query the original query to lookup this player
* @param player the player to display
* @returns the player result jsx
*/
const PlayerResult = ({
query,
player: {
uniqueId,
username,
skin: { parts },
legacy,
},
}: PlayerResultProps): ReactElement => (
const PlayerResult = ({ query, player }: PlayerResultProps): ReactElement => (
<ContextMenu>
<ContextMenuTrigger>
<div className="relative px-2 py-7 flex flex-col items-center bg-muted rounded-xl">
{/* Raw Json */}
<div className="absolute top-2 right-2">
<Link
href={`${config.apiEndpoint}/player/${username}`}
target="_blank"
<CodeDialog
title="Raw Player Data"
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">
Raw Json
</Badge>
</Link>
{JSON.stringify(player, undefined, 4)}
</CodeDialog>
</div>
{/* Result */}
<div className="w-full flex flex-col gap-3 justify-center items-center divide-y divide-zinc-700">
{/* Details */}
<div className="flex gap-5 items-center">
{/* Player Head */}
<Image
className="w-24 h-24 sm:w-28 sm:h-28 md:w-32 md:h-32 pointer-events-none"
src={parts.HEAD}
alt={`${username}'s Head`}
src={player.skin.parts.HEAD}
alt={`${player.username}'s Head`}
width={128}
height={128}
/>
@ -72,14 +67,14 @@ const PlayerResult = ({
{/* Name, Unique ID, and Badges */}
<div className="flex flex-col gap-1.5">
<h1 className="text-xl font-bold text-minecraft-green-3">
{username}
{player.username}
</h1>
<code className="text-xs xs:text-sm text-zinc-300">
{uniqueId}
{player.uniqueId}
</code>
{/* Legacy Badge */}
{legacy && (
{player.legacy && (
<p className="text-sm font-semibold uppercase">
Legacy
</p>
@ -94,7 +89,7 @@ const PlayerResult = ({
{/* Skin Parts */}
<div className="flex gap-5">
{Object.entries(parts)
{Object.entries(player.skin.parts)
.filter(
([part]) =>
part === SkinPart.HEAD ||
@ -114,7 +109,7 @@ const PlayerResult = ({
<img
className="h-20 sm:h-24 md:h-28 hover:scale-[1.02] transition-all transform-gpu"
src={url}
alt={`${username}'s ${part}`}
alt={`${player.username}'s ${part}`}
/>
</Link>
)
@ -125,11 +120,11 @@ const PlayerResult = ({
</div>
</ContextMenuTrigger>
<ContextMenuContent className="flex flex-col">
<CopyButton content={username} showToast>
<CopyButton content={player.username} showToast>
<ContextMenuItem>Copy Player Username</ContextMenuItem>
</CopyButton>
<CopyButton content={uniqueId} showToast>
<CopyButton content={player.uniqueId} showToast>
<ContextMenuItem>Copy Player UUID</ContextMenuItem>
</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";
import config from "@/config";
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.
@ -28,58 +30,75 @@ const ServerResult = ({ server }: ServerResultProps): ReactElement => {
const favicon: string | undefined = (server as CachedJavaMinecraftServer)
.favicon?.url; // The favicon of the server
return (
<div
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"
src={favicon || `${config.apiEndpoint}/server/icon/fallback`}
alt={`${server.hostname}'s Favicon`}
width={64}
height={64}
/>
<div className="flex flex-col gap-3">
{/* Result */}
<div
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"
src={
favicon || `${config.apiEndpoint}/server/icon/fallback`
}
alt={`${server.hostname}'s Favicon`}
width={64}
height={64}
/>
{/* Content */}
<div className="-translate-y-1 w-full h-full flex flex-col gap-1">
{/* Name & Ping */}
<div className="w-full flex justify-between">
<h1>{server.hostname}</h1>
{/* Content */}
<div className="-translate-y-1 w-full h-full flex flex-col gap-1">
{/* Name & Ping */}
<div className="w-full flex justify-between">
<h1>{server.hostname}</h1>
{/* Players & Ping */}
<div className="flex gap-1 items-center">
<p className="text-[#AAAAAA]">
{server.players.online}
<span className="text-[#555555]">/</span>
{server.players.max}
</p>
<Image
src="/media/ping-full.png"
alt="Ping!"
width={20}
height={20}
/>
{/* Players & Ping */}
<div className="flex gap-1 items-center">
<p className="text-[#AAAAAA]">
{server.players.online}
<span className="text-[#555555]">/</span>
{server.players.max}
</p>
<Image
src="/media/ping-full.png"
alt="Ping!"
width={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>
{/* 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>
{/* 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>
);
};