feat: migrate to Radix UI and implement dark mode with next-themes

Replace @headlessui/react and @heroicons/react with @radix-ui/themes and
@radix-ui/react-icons for a more comprehensive component library. Add
next-themes for dark mode support with a new ThemeToggle component. Update
all components to use Radix UI primitives and theming system, including
AbstractCard, DynamicDate, ErrorCard, Property, PropertyList, LookupInput,
and all card components (AutnumCard, DomainCard, EntityCard, IPCard,
NameserverCard). Update global styles and app configuration to support theme
switching.
This commit is contained in:
2025-10-22 11:59:52 -05:00
parent 611074b546
commit 7073936e6c
19 changed files with 2161 additions and 550 deletions

View File

@@ -22,12 +22,13 @@
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.0.16",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/themes": "^3.2.1",
"@swc/helpers": "^0.5.11",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"next": "^15.5.6",
"next-themes": "^0.4.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.42.1",

1788
pnpm-lock.yaml generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,8 @@
import type { FunctionComponent, ReactNode } from "react";
import React from "react";
import { useBoolean } from "usehooks-ts";
import {
LinkIcon,
CodeBracketIcon,
DocumentArrowDownIcon,
ClipboardDocumentIcon,
} from "@heroicons/react/24/outline";
import { Link2Icon, CodeIcon, DownloadIcon, ClipboardCopyIcon } from "@radix-ui/react-icons";
import { Card, Flex, Box, IconButton, Code, ScrollArea } from "@radix-ui/themes";
type AbstractCardProps = {
children?: ReactNode;
@@ -26,23 +22,39 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
const { value: showRaw, toggle: toggleRaw } = useBoolean(false);
return (
<div className="mb-4 overflow-clip rounded bg-zinc-800 shadow">
{header != undefined || data != undefined ? (
<div className="flex bg-zinc-700 p-2 pl-3 md:pl-5">
<div className="flex grow gap-2">{header}</div>
{url != undefined ? (
<div className="pr-2">
<a href={url} target="_blank" rel="noreferrer">
<LinkIcon className="mt-1 h-5 w-5 cursor-pointer" />
<Box mb="4">
<Card size="2">
{(header != undefined || data != undefined) && (
<Flex
justify="between"
align="center"
p="3"
style={{
borderBottom: "1px solid var(--gray-a5)",
}}
>
<Flex gap="2" style={{ flex: 1 }}>
{header}
</Flex>
<Flex gap="2" align="center">
{url != undefined && (
<IconButton variant="ghost" size="2" asChild>
<a
href={url}
target="_blank"
rel="noreferrer"
aria-label="Open RDAP URL"
>
<Link2Icon width="18" height="18" />
</a>
</div>
) : null}
{data != undefined ? (
</IconButton>
)}
{data != undefined && (
<>
<div className="pr-2">
<ClipboardDocumentIcon
<IconButton
variant="ghost"
size="2"
onClick={() => {
// stringify the JSON object, then begin the async clipboard write
navigator.clipboard
.writeText(JSON.stringify(data, null, 4))
.then(
@@ -61,11 +73,13 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
}
);
}}
className="h-6 w-6 cursor-pointer"
/>
</div>
<div className="pr-2">
<DocumentArrowDownIcon
aria-label="Copy JSON to clipboard"
>
<ClipboardCopyIcon width="18" height="18" />
</IconButton>
<IconButton
variant="ghost"
size="2"
onClick={() => {
const file = new Blob([JSON.stringify(data, null, 4)], {
type: "application/json",
@@ -76,32 +90,57 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
anchor.download = "response.json";
anchor.click();
}}
className="h-6 w-6 cursor-pointer"
/>
</div>
<div className="pr-1">
<CodeBracketIcon
aria-label="Download JSON"
>
<DownloadIcon width="18" height="18" />
</IconButton>
<IconButton
variant="ghost"
size="2"
onClick={toggleRaw}
className="h-6 w-6 cursor-pointer"
/>
</div>
aria-label={
showRaw ? "Show formatted view" : "Show raw JSON"
}
>
<CodeIcon width="18" height="18" />
</IconButton>
</>
) : null}
</div>
) : null}
<div className="max-w-full overflow-x-auto p-2 px-4">
)}
</Flex>
</Flex>
)}
<Box p="4">
{showRaw ? (
<pre className="scrollbar-thin m-2 max-h-[40rem] max-w-full overflow-y-auto rounded whitespace-pre-wrap">
<ScrollArea type="auto" scrollbars="both" style={{ maxHeight: "40rem" }}>
<Code
variant="ghost"
size="2"
style={{
display: "block",
whiteSpace: "pre-wrap",
fontFamily: "var(--font-mono)",
}}
>
{JSON.stringify(data, null, 4)}
</pre>
</Code>
</ScrollArea>
) : (
children
)}
</div>
{footer != null ? (
<div className="flex gap-2 bg-zinc-700 p-2 pl-5">{footer}</div>
) : null}
</div>
</Box>
{footer != null && (
<Flex
gap="2"
p="3"
style={{
borderTop: "1px solid var(--gray-a5)",
}}
>
{footer}
</Flex>
)}
</Card>
</Box>
);
};

View File

@@ -2,6 +2,7 @@ import type { FunctionComponent } from "react";
import { useBoolean } from "usehooks-ts";
import { format } from "date-fns";
import TimeAgo from "react-timeago";
import { Button, Text } from "@radix-ui/themes";
type DynamicDateProps = {
value: Date | number;
@@ -18,15 +19,20 @@ const DynamicDate: FunctionComponent<DynamicDateProps> = ({ value, absoluteForma
const date = new Date(value);
return (
<button onClick={toggleFormat}>
<span className="dashed" title={date.toISOString()}>
<Button
variant="ghost"
size="1"
onClick={toggleFormat}
style={{ padding: 0, height: "auto" }}
>
<Text className="dashed" title={date.toISOString()} size="2">
{showAbsolute ? (
format(date, absoluteFormat ?? "LLL do, y HH:mm:ss xxx")
) : (
<TimeAgo date={date} />
)}
</span>
</button>
</Text>
</Button>
);
};

View File

@@ -1,6 +1,6 @@
import type { FunctionComponent, ReactNode } from "react";
import { XCircleIcon } from "@heroicons/react/20/solid";
import { cn } from "@/lib/utils";
import { CrossCircledIcon } from "@radix-ui/react-icons";
import { Callout, Box, Flex } from "@radix-ui/themes";
export type ErrorCardProps = {
title: ReactNode;
@@ -16,35 +16,46 @@ const ErrorCard: FunctionComponent<ErrorCardProps> = ({
className,
}) => {
return (
<div
className={cn(
className,
"rounded-md border border-red-700/30 bg-zinc-800 px-3 pt-3 pb-1"
)}
>
<div className="flex">
<div className="shrink-0">
<XCircleIcon className="h-5 w-5 text-red-300" aria-hidden="true" />
</div>
<div className="ml-3 w-full text-sm text-red-300">
<h3 className="font-medium text-red-200">{title}</h3>
<Box className={className}>
<Callout.Root color="red" role="alert">
<Callout.Icon>
<CrossCircledIcon />
</Callout.Icon>
<Flex direction="column" gap="2">
<Callout.Text weight="medium" size="3">
{title}
</Callout.Text>
{description != undefined ? (
<div className="mt-2 max-h-24 w-full overflow-y-auto whitespace-pre-wrap">
{description}
</div>
<Box
style={{
maxHeight: "6rem",
overflowY: "auto",
whiteSpace: "pre-wrap",
}}
>
<Callout.Text size="2">{description}</Callout.Text>
</Box>
) : null}
<div className="mt-2">
{issues != undefined ? (
<ul role="list" className="flex list-disc flex-col gap-1 pl-5">
{issues != undefined && issues.length > 0 ? (
<Box asChild>
<ul
role="list"
style={{
listStyleType: "disc",
paddingLeft: "1.25rem",
}}
>
{issues.map((issueText, index) => (
<li key={index}>{issueText}</li>
<li key={index}>
<Callout.Text size="2">{issueText}</Callout.Text>
</li>
))}
</ul>
</Box>
) : null}
</div>
</div>
</div>
</div>
</Flex>
</Callout.Root>
</Box>
);
};

View File

@@ -1,6 +1,6 @@
import type { FunctionComponent, ReactNode } from "react";
import React from "react";
import { cn } from "@/lib/utils";
import { DataList } from "@radix-ui/themes";
type PropertyProps = {
title: string | ReactNode;
@@ -9,6 +9,11 @@ type PropertyProps = {
valueClass?: string;
};
/**
* A simple wrapper around Radix DataList for displaying key-value pairs.
* This component uses DataList.Item, DataList.Label, and DataList.Value
* to provide semantic HTML and consistent styling.
*/
const Property: FunctionComponent<PropertyProps> = ({
title,
children,
@@ -16,10 +21,10 @@ const Property: FunctionComponent<PropertyProps> = ({
valueClass,
}) => {
return (
<>
<dt className={cn("font-medium", titleClass)}>{title}:</dt>
<dd className={cn("mt-2 mb-2 ml-6", valueClass)}>{children}</dd>
</>
<DataList.Item>
<DataList.Label className={titleClass}>{title}</DataList.Label>
<DataList.Value className={valueClass}>{children}</DataList.Value>
</DataList.Item>
);
};

View File

@@ -1,17 +1,19 @@
import type { FunctionComponent, ReactNode } from "react";
import React from "react";
import Property from "@/components/common/Property";
import { Text, Box, DataList } from "@radix-ui/themes";
const PropertyListItem: FunctionComponent<{
title: string;
children: string;
}> = ({ title, children }) => {
return (
<Box asChild>
<li>
<span className="dashed" title={title}>
<Text className="dashed" title={title} size="2">
{children}
</span>
</Text>
</li>
</Box>
);
};
@@ -20,15 +22,30 @@ type PropertyListProps = {
children: ReactNode;
};
/**
* PropertyList displays a labeled list of items (not key-value pairs).
* Uses DataList.Item for the label, and renders children as a bulleted list.
*/
const PropertyList: FunctionComponent<PropertyListProps> & {
Item: typeof PropertyListItem;
} = ({ title, children }) => {
return (
<Property title={title}>
<ul key={2} className="list-disc">
<DataList.Item>
<DataList.Label>{title}</DataList.Label>
<DataList.Value>
<Box asChild>
<ul
style={{
listStyleType: "disc",
paddingLeft: "1.25rem",
margin: 0,
}}
>
{children}
</ul>
</Property>
</Box>
</DataList.Value>
</DataList.Item>
);
};

View File

@@ -0,0 +1,40 @@
"use client";
import { useTheme } from "next-themes";
import { IconButton } from "@radix-ui/themes";
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
import { useEffect, useState } from "react";
export const ThemeToggle = () => {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
// Avoid hydration mismatch by only rendering after mount
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<IconButton
size="3"
variant="ghost"
onClick={toggleTheme}
aria-label="Toggle theme"
title={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
>
{theme === "light" ? (
<MoonIcon width="18" height="18" />
) : (
<SunIcon width="18" height="18" />
)}
</IconButton>
);
};

View File

@@ -1,24 +1,11 @@
import { useForm } from "react-hook-form";
import { useForm, Controller } from "react-hook-form";
import type { FunctionComponent } from "react";
import { Fragment, useState } from "react";
import { useState } from "react";
import { onPromise, preventDefault } from "@/helpers";
import type { SimplifiedTargetType, SubmitProps, TargetType } from "@/types";
import { TargetTypeEnum } from "@/schema";
import {
CheckIcon,
ChevronUpDownIcon,
LockClosedIcon,
MagnifyingGlassIcon,
ArrowPathIcon,
} from "@heroicons/react/20/solid";
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
Transition,
} from "@headlessui/react";
import { cn } from "@/lib/utils";
import { MagnifyingGlassIcon, ReloadIcon, LockClosedIcon } from "@radix-ui/react-icons";
import { TextField, Select, Flex, Checkbox, Text, IconButton } from "@radix-ui/themes";
import type { Maybe } from "true-myth";
import { placeholders } from "@/constants";
@@ -54,7 +41,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
onChange,
detectedType,
}: LookupInputProps) => {
const { register, handleSubmit, getValues } = useForm<SubmitProps>({
const { register, handleSubmit, getValues, control } = useForm<SubmitProps>({
defaultValues: {
target: "",
// Not used at this time.
@@ -115,200 +102,143 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
return result.success ? result.data : null;
}
const searchIcon = (
<>
<button
type="submit"
className={cn({
"absolute inset-y-0 left-0 flex items-center pl-3": true,
"pointer-events-none": isLoading,
})}
return (
<form
className="pb-2.5"
onSubmit={onSubmit != undefined ? onPromise(handleSubmit(onSubmit)) : preventDefault}
>
{isLoading ? (
<ArrowPathIcon
className="h-5 w-5 animate-spin text-zinc-400"
aria-hidden="true"
/>
) : (
<MagnifyingGlassIcon className="h-5 w-5 text-zinc-400" aria-hidden="true" />
)}
</button>
</>
);
const searchInput = (
<input
className={cn(
"block w-full rounded-l-md border border-transparent lg:py-4.5",
"bg-zinc-700 py-2 pr-1.5 pl-10 text-sm placeholder-zinc-400 placeholder:translate-y-2 focus:text-zinc-200",
"focus:outline-hidden sm:text-sm md:py-3 md:text-base lg:text-lg"
)}
disabled={isLoading}
<Flex direction="column" gap="3">
<label htmlFor="search" className="sr-only">
Search
</label>
<Flex gap="0" style={{ position: "relative" }}>
<TextField.Root
size="3"
placeholder={placeholders[selected]}
type="search"
disabled={isLoading}
{...register("target", {
required: true,
onChange: () => {
if (onChange != undefined)
void onChange({
target: getValues("target"),
// dropdown target will be pulled from state anyways, so no need to provide it here
targetType: retrieveTargetType(null),
});
},
})}
/>
);
style={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
border: "1px solid var(--gray-7)",
borderRight: "none",
boxShadow: "none",
flex: 1,
}}
>
<TextField.Slot side="left">
<IconButton
size="1"
variant="ghost"
type="submit"
disabled={isLoading}
tabIndex={-1}
style={{ cursor: isLoading ? "not-allowed" : "pointer" }}
>
{isLoading ? (
<ReloadIcon className="animate-spin" width="16" height="16" />
) : (
<MagnifyingGlassIcon width="16" height="16" />
)}
</IconButton>
</TextField.Slot>
</TextField.Root>
const dropdown = (
<Listbox
<Select.Root
value={selected}
onChange={(value) => {
setSelected(value);
onValueChange={(value) => {
setSelected(value as SimplifiedTargetType | "auto");
if (onChange != undefined)
void onChange({
target: getValues("target"),
// we provide the value as the state will not have updated yet for this context
targetType: retrieveTargetType(value),
});
}}
disabled={isLoading}
size="3"
>
<div className="relative">
<ListboxButton
className={cn(
"relative h-full w-full cursor-default rounded-r-lg bg-zinc-700 py-2 pr-10 pl-1 text-right whitespace-nowrap",
"text-xs focus:outline-hidden focus-visible:border-indigo-500 sm:text-sm md:text-base lg:text-lg",
"focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300"
)}
<Select.Trigger
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
minWidth: "150px",
}}
>
{/* Fetch special text for 'auto' mode, otherwise just use the options. */}
<span className="block">
{selected == "auto" ? (
// If the detected type was provided, then notate which in parentheses. Compact object naming might be better in the future.
detectedType.isJust ? (
<>
Auto (
<span className="animate-pulse">
{targetShortNames[detectedType.value]}
</span>
)
</>
<Text>Auto ({targetShortNames[detectedType.value]})</Text>
) : (
objectNames["auto"]
)
) : (
<>
<LockClosedIcon
className="mr-2.5 mb-1 inline h-4 w-4 animate-pulse text-zinc-500"
aria-hidden
/>
<Flex align="center" gap="2">
<LockClosedIcon width="14" height="14" />
{objectNames[selected]}
</>
</Flex>
)}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-zinc-200" aria-hidden="true" />
</span>
</ListboxButton>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<ListboxOptions
className={cn(
"scrollbar-thin absolute right-0 mt-1 max-h-60 min-w-full overflow-auto rounded-md bg-zinc-700 py-1",
"text-zinc-200 shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm"
)}
>
</Select.Trigger>
<Select.Content position="popper">
{Object.entries(objectNames).map(([key, value]) => (
<ListboxOption
key={key}
className={({ focus }) =>
cn(
"relative cursor-default py-2 pr-4 pl-10 select-none",
focus ? "bg-zinc-800 text-zinc-300" : null
)
}
value={key}
>
{({ selected }) => (
<>
<span
className={cn(
"block text-right text-xs whitespace-nowrap md:text-sm lg:text-base",
selected ? "font-medium" : null
)}
<Select.Item key={key} value={key}>
<Flex
align="center"
justify="between"
gap="2"
style={{ width: "100%" }}
>
{value}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-500">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : (
<button
onClick={(e) => {
e.preventDefault();
// TODO: Show Help Explanation
}}
className="absolute inset-y-0 left-0 flex items-center pl-4 text-lg font-bold opacity-20 hover:animate-pulse"
>
?
</button>
)}
</>
)}
</ListboxOption>
</Flex>
</Select.Item>
))}
</ListboxOptions>
</Transition>
</div>
</Listbox>
);
</Select.Content>
</Select.Root>
</Flex>
return (
<form
className="pb-3"
onSubmit={onSubmit != undefined ? onPromise(handleSubmit(onSubmit)) : preventDefault}
>
<div className="col">
<label htmlFor="search" className="sr-only">
Search
</label>
<div className="relative flex">
{searchIcon}
{searchInput}
{dropdown}
</div>
</div>
<div className="col">
<div className="flex flex-wrap pt-3 pb-1 text-sm">
<div className="whitespace-nowrap">
<input
className="mr-1 ml-2 whitespace-nowrap text-zinc-800 accent-blue-700"
type="checkbox"
{...register("requestJSContact")}
<Flex pl="3" gapX="5" gapY="2" wrap="wrap">
<Flex asChild align="center" gap="2">
<Text as="label" size="2">
<Controller
name="requestJSContact"
control={control}
render={({ field }) => (
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
)}
/>
<label className="text-zinc-300" htmlFor="requestJSContact">
Request JSContact
</label>
</div>
<div className="whitespace-nowrap">
<input
className="mr-1 ml-2 bg-zinc-500 text-inherit accent-blue-700"
type="checkbox"
{...register("followReferral")}
</Text>
</Flex>
<Flex asChild align="center" gap="2">
<Text as="label" size="2">
<Controller
name="followReferral"
control={control}
render={({ field }) => (
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
)}
/>
<label className="text-zinc-300" htmlFor="followReferral">
Follow referral to registrar&apos;s RDAP record
</label>
</div>
</div>
</div>
</Text>
</Flex>
</Flex>
</Flex>
</form>
);
};

View File

@@ -5,6 +5,7 @@ import Events from "@/components/lookup/Events";
import Property from "@/components/common/Property";
import PropertyList from "@/components/common/PropertyList";
import AbstractCard from "@/components/common/AbstractCard";
import { Flex, Text, DataList, Badge } from "@radix-ui/themes";
export type AutnumCardProps = {
data: AutonomousNumber;
@@ -22,14 +23,13 @@ const AutnumCard: FunctionComponent<AutnumCardProps> = ({ data, url }: AutnumCar
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">AUTONOMOUS SYSTEM</span>
<span className="font-mono tracking-wide">{asnRange}</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
<Flex gap="2" align="center" wrap="wrap">
<Text size="5">{asnRange}</Text>
<Badge color="gray">AUTONOMOUS SYSTEM</Badge>
</Flex>
}
>
<dl>
<DataList.Root orientation={{ initial: "vertical", sm: "horizontal" }} size="2">
<Property title="Name">{data.name}</Property>
<Property title="Handle">{data.handle}</Property>
<Property title="ASN Range">
@@ -49,7 +49,7 @@ const AutnumCard: FunctionComponent<AutnumCardProps> = ({ data, url }: AutnumCar
</PropertyList.Item>
))}
</PropertyList>
</dl>
</DataList.Root>
</AbstractCard>
);
};

View File

@@ -6,6 +6,7 @@ import Events from "@/components/lookup/Events";
import Property from "@/components/common/Property";
import PropertyList from "@/components/common/PropertyList";
import AbstractCard from "@/components/common/AbstractCard";
import { Flex, Text, DataList, Badge } from "@radix-ui/themes";
export type DomainProps = {
data: Domain;
@@ -18,16 +19,13 @@ const DomainCard: FunctionComponent<DomainProps> = ({ data, url }: DomainProps)
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">DOMAIN</span>
<span className="font-mono tracking-wide">
{data.ldhName ?? data.unicodeName}
</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
<Flex gap="2" align="center" wrap="wrap">
<Text size="5">{data.ldhName ?? data.unicodeName}</Text>
<Badge color="gray">DOMAIN</Badge>
</Flex>
}
>
<dl>
<DataList.Root orientation={{ initial: "vertical", sm: "horizontal" }} size="2">
{data.unicodeName != undefined ? (
<Property title="Unicode Name">{data.unicodeName}</Property>
) : null}
@@ -45,7 +43,7 @@ const DomainCard: FunctionComponent<DomainProps> = ({ data, url }: DomainProps)
</PropertyList.Item>
))}
</PropertyList>
</dl>
</DataList.Root>
</AbstractCard>
);
};

View File

@@ -4,6 +4,7 @@ import type { Entity } from "@/types";
import Property from "@/components/common/Property";
import PropertyList from "@/components/common/PropertyList";
import AbstractCard from "@/components/common/AbstractCard";
import { Flex, DataList, Badge, Text } from "@radix-ui/themes";
export type EntityCardProps = {
data: Entity;
@@ -16,15 +17,13 @@ const EntityCard: FunctionComponent<EntityCardProps> = ({ data, url }: EntityCar
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">ENTITY</span>
<span className="font-mono tracking-wide">
{data.handle || data.roles.join(", ")}
</span>
</>
<Flex gap="2" align="center" wrap="wrap">
<Text size="5">{data.handle || data.roles.join(", ")}</Text>
<Badge color="gray">ENTITY</Badge>
</Flex>
}
>
<dl>
<DataList.Root orientation={{ initial: "vertical", sm: "horizontal" }} size="2">
{data.handle && <Property title="Handle">{data.handle}</Property>}
<PropertyList title="Roles">
{data.roles.map((role, index) => (
@@ -42,7 +41,7 @@ const EntityCard: FunctionComponent<EntityCardProps> = ({ data, url }: EntityCar
))}
</PropertyList>
)}
</dl>
</DataList.Root>
</AbstractCard>
);
};

View File

@@ -1,26 +1,42 @@
import type { FunctionComponent } from "react";
import type { Event } from "@/types";
import { Fragment } from "react";
import DynamicDate from "@/components/common/DynamicDate";
import { Table, Text } from "@radix-ui/themes";
export type EventsProps = {
data: Event[];
};
const Events: FunctionComponent<EventsProps> = ({ data }) => {
return (
<dl>
{data.map(({ eventAction, eventDate, eventActor }, index) => {
return (
<Fragment key={index}>
<dt className="font-weight-bolder">{eventAction}:</dt>
<dd>
<Table.Root size="1" variant="surface">
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell>Event</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>Date</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>Actor</Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{data.map(({ eventAction, eventDate, eventActor }, index) => (
<Table.Row key={index}>
<Table.Cell>
<Text size="2" weight="medium">
{eventAction}
</Text>
</Table.Cell>
<Table.Cell>
<DynamicDate value={new Date(eventDate)} />
{eventActor != null ? ` (by ${eventActor})` : null}
</dd>
</Fragment>
);
})}
</dl>
</Table.Cell>
<Table.Cell>
<Text size="2" color="gray">
{eventActor ?? "—"}
</Text>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
);
};

View File

@@ -35,12 +35,6 @@ const Generic: FunctionComponent<ObjectProps> = ({ data, url }: ObjectProps) =>
</AbstractCard>
);
}
// const title: string = (data.unicodeName ?? data.ldhName ?? data.handle)?.toUpperCase() ?? "Response";
// return <div className="card">
// <div className="card-header">{title}</div>
// {objectFragment}
// </div>
};
export default Generic;

View File

@@ -5,6 +5,7 @@ import Events from "@/components/lookup/Events";
import Property from "@/components/common/Property";
import PropertyList from "@/components/common/PropertyList";
import AbstractCard from "@/components/common/AbstractCard";
import { Flex, Text, DataList, Badge } from "@radix-ui/themes";
export type IPCardProps = {
data: IpNetwork;
@@ -17,17 +18,15 @@ const IPCard: FunctionComponent<IPCardProps> = ({ data, url }: IPCardProps) => {
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">IP NETWORK</span>
<span className="font-mono tracking-wide">
{data.startAddress}
{data.startAddress !== data.endAddress && ` - ${data.endAddress}`}
</span>
<span className="whitespace-nowrap">({data.handle})</span>
</>
<Flex gap="2" align="center" wrap="wrap">
<Text size="5">
{data.startAddress} - {data.endAddress}
</Text>
<Badge color="gray">IP NETWORK</Badge>
</Flex>
}
>
<dl>
<DataList.Root orientation={{ initial: "vertical", sm: "horizontal" }} size="2">
<Property title="Name">{data.name}</Property>
<Property title="Handle">{data.handle}</Property>
<Property title="IP Version">{data.ipVersion.toUpperCase()}</Property>
@@ -48,7 +47,7 @@ const IPCard: FunctionComponent<IPCardProps> = ({ data, url }: IPCardProps) => {
</PropertyList.Item>
))}
</PropertyList>
</dl>
</DataList.Root>
</AbstractCard>
);
};

View File

@@ -3,6 +3,7 @@ import React from "react";
import type { Nameserver } from "@/types";
import Property from "@/components/common/Property";
import AbstractCard from "@/components/common/AbstractCard";
import { Flex, DataList, Badge, Text } from "@radix-ui/themes";
export type NameserverCardProps = {
data: Nameserver;
@@ -18,15 +19,15 @@ const NameserverCard: FunctionComponent<NameserverCardProps> = ({
data={data}
url={url}
header={
<>
<span className="font-mono tracking-tighter">NAMESERVER</span>
<span className="font-mono tracking-wide">{data.ldhName}</span>
</>
<Flex gap="2" align="center" wrap="wrap">
<Text size="5">{data.ldhName}</Text>
<Badge color="gray">NAMESERVER</Badge>
</Flex>
}
>
<dl>
<DataList.Root orientation={{ initial: "vertical", sm: "horizontal" }} size="2">
<Property title="LDH Name">{data.ldhName}</Property>
</dl>
</DataList.Root>
</AbstractCard>
);
};

View File

@@ -1,12 +1,21 @@
import { type AppType } from "next/dist/shared/lib/utils";
import { ThemeProvider } from "next-themes";
import { Theme } from "@radix-ui/themes";
import "@fontsource-variable/inter";
import "@fontsource/ibm-plex-mono/400.css";
import "@radix-ui/themes/styles.css";
import "../styles/globals.css";
const MyApp: AppType = ({ Component, pageProps }) => {
return <Component {...pageProps} />;
return (
<ThemeProvider attribute="class" defaultTheme="system">
<Theme accentColor="indigo" grayColor="slate" radius="medium" scaling="100%">
<Component {...pageProps} />
</Theme>
</ThemeProvider>
);
};
export default MyApp;

View File

@@ -6,8 +6,10 @@ import type { MetaParsedGeneric } from "@/hooks/useLookup";
import useLookup from "@/hooks/useLookup";
import LookupInput from "@/components/form/LookupInput";
import ErrorCard from "@/components/common/ErrorCard";
import { ThemeToggle } from "@/components/common/ThemeToggle";
import { Maybe } from "true-myth";
import type { TargetType } from "@/types";
import { Flex, Container, Section, Text, Link } from "@radix-ui/themes";
const Index: NextPage = () => {
const { error, setTarget, setTargetType, submit, getType } = useLookup();
@@ -37,16 +39,30 @@ const Index: NextPage = () => {
content="xevion, rdap, whois, rdap, domain name, dns, ip address"
/>
</Head>
<nav className="bg-zinc-850 px-5 py-4 shadow-xs">
<span className="text-xl font-medium text-white" style={{ fontSize: "larger" }}>
<a href="https://github.com/Xevion/rdap">rdap</a>
<a href={"https://xevion.dev"} className="text-zinc-400 hover:animate-pulse">
<Flex
asChild
justify="between"
align="center"
px="5"
py="4"
style={{
borderBottom: "1px solid var(--gray-a5)",
}}
>
<nav>
<Text size="5" weight="medium">
<Link href="https://github.com/Xevion/rdap" color="gray" highContrast>
rdap
</Link>
<Link href="https://xevion.dev" color="gray">
.xevion.dev
</a>
</span>
</Link>
</Text>
<ThemeToggle />
</nav>
<div className="mx-auto max-w-screen-sm px-5 lg:max-w-screen-md xl:max-w-screen-lg">
<div className="dark container mx-auto w-full py-6 md:py-12">
</Flex>
<Container size="3" px="5">
<Section size="2">
<LookupInput
isLoading={isLoading}
detectedType={detectedType}
@@ -83,8 +99,8 @@ const Index: NextPage = () => {
{response.isJust ? (
<Generic url={response.value.url} data={response.value.data} />
) : null}
</div>
</div>
</Section>
</Container>
</>
);
};

View File

@@ -7,31 +7,27 @@
--font-mono:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--color-zinc-850: #1d1d20;
}
dd {
margin: 0.5em 0 1em 2em;
}
/* Utility classes */
.dashed {
border-bottom: 1px dashed silver;
}
body {
color-scheme: dark;
@apply bg-zinc-900 font-sans text-white;
}
dd,
dl {
white-space: nowrap;
}
dl {
margin: 0;
border-bottom: 1px dashed var(--gray-a6);
}
.scrollbar-thin {
scrollbar-width: thin;
}
/* Keep animate-spin for loading states */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}