From 335bc6aee8f1b864a5accaf01ec47dd998e28d34 Mon Sep 17 00:00:00 2001 From: Xevion Date: Wed, 22 Oct 2025 12:56:30 -0500 Subject: [PATCH] feat: add CopyButton and StatusBadge components with enhanced RDAP card UX This commit introduces two new reusable components and significantly improves the user experience across all RDAP cards: New Components: - CopyButton: Provides one-click copying functionality for handles, addresses, and other identifiers - StatusBadge: Displays color-coded status badges with proper type safety RDAP Card Enhancements: - Replace deprecated ClipboardCopyIcon with ClipboardIcon - Add copy buttons next to all handles, addresses, and identifiers - Migrate status displays from PropertyList to StatusBadge components with color coding - Replace PropertyList with proper DataList components for roles and public IDs - Improve Events table layout and styling - Wrap all copyable values in Code components for better visual distinction Type Safety Improvements: - Add rdapStatusColors mapping with proper Radix UI badge color types - Update IpNetwork and AutonomousNumber schemas to use typed StatusEnum arrays --- src/components/AbstractCard.tsx | 4 +- src/components/CopyButton.tsx | 40 +++++++++++++++++ src/components/StatusBadge.tsx | 36 +++++++++++++++ src/rdap/components/AutnumCard.tsx | 46 ++++++++++++------- src/rdap/components/DomainCard.tsx | 57 ++++++++++++++++------- src/rdap/components/EntityCard.tsx | 58 ++++++++++++++++-------- src/rdap/components/Events.tsx | 26 ++++++----- src/rdap/components/IPCard.tsx | 62 ++++++++++++++++++++------ src/rdap/components/NameserverCard.tsx | 14 ++++-- src/rdap/constants.ts | 40 +++++++++++++++++ src/rdap/schemas.ts | 4 +- 11 files changed, 308 insertions(+), 79 deletions(-) create mode 100644 src/components/CopyButton.tsx create mode 100644 src/components/StatusBadge.tsx diff --git a/src/components/AbstractCard.tsx b/src/components/AbstractCard.tsx index de05cec..dbc9e2b 100644 --- a/src/components/AbstractCard.tsx +++ b/src/components/AbstractCard.tsx @@ -1,7 +1,7 @@ import type { FunctionComponent, ReactNode } from "react"; import React from "react"; import { useBoolean } from "usehooks-ts"; -import { Link2Icon, CodeIcon, DownloadIcon, ClipboardCopyIcon } from "@radix-ui/react-icons"; +import { Link2Icon, CodeIcon, DownloadIcon, ClipboardIcon } from "@radix-ui/react-icons"; import { Card, Flex, Box, IconButton, Code, ScrollArea } from "@radix-ui/themes"; type AbstractCardProps = { @@ -75,7 +75,7 @@ const AbstractCard: FunctionComponent = ({ }} aria-label="Copy JSON to clipboard" > - + = ({ value, size = "1" }) => { + const handleCopy = () => { + navigator.clipboard.writeText(value).then( + () => { + // Successfully copied to clipboard + }, + (err) => { + if (err instanceof Error) { + console.error(`Failed to copy to clipboard (${err.toString()}).`); + } else { + console.error("Failed to copy to clipboard."); + } + } + ); + }; + + return ( + + + + ); +}; + +export default CopyButton; diff --git a/src/components/StatusBadge.tsx b/src/components/StatusBadge.tsx new file mode 100644 index 0000000..915c53e --- /dev/null +++ b/src/components/StatusBadge.tsx @@ -0,0 +1,36 @@ +import type { FunctionComponent } from "react"; +import React from "react"; +import type { RdapStatusType } from "@/rdap/schemas"; +import { rdapStatusColors, rdapStatusInfo } from "@/rdap/constants"; +import { QuestionMarkIcon } from "@radix-ui/react-icons"; +import { Badge, HoverCard, Text, Flex } from "@radix-ui/themes"; + +export type StatusBadgeProps = { + status: RdapStatusType; +}; + +const StatusBadge: FunctionComponent = ({ status }) => { + return ( + + + + + {status} + + + + + + {rdapStatusInfo[status]} + + + ); +}; + +export default StatusBadge; diff --git a/src/rdap/components/AutnumCard.tsx b/src/rdap/components/AutnumCard.tsx index 40282dc..9cb1577 100644 --- a/src/rdap/components/AutnumCard.tsx +++ b/src/rdap/components/AutnumCard.tsx @@ -3,9 +3,10 @@ import React from "react"; import type { AutonomousNumber } from "@/rdap/schemas"; import Events from "@/rdap/components/Events"; import Property from "@/components/Property"; -import PropertyList from "@/components/PropertyList"; +import CopyButton from "@/components/CopyButton"; +import StatusBadge from "@/components/StatusBadge"; import AbstractCard from "@/components/AbstractCard"; -import { Flex, Text, DataList, Badge } from "@radix-ui/themes"; +import { Flex, Text, DataList, Badge, Code } from "@radix-ui/themes"; export type AutnumCardProps = { data: AutonomousNumber; @@ -31,24 +32,39 @@ const AutnumCard: FunctionComponent = ({ data, url }: AutnumCar > {data.name} - {data.handle} - - {data.startAutnum === data.endAutnum - ? `AS${data.startAutnum}` - : `AS${data.startAutnum} - AS${data.endAutnum}`} - + + Handle + + + {data.handle} + + + + + + ASN Range + + + {asnRange} + + + + {data.type} {data.country.toUpperCase()} - - {data.status.map((status, index) => ( - - {status} - - ))} - + + Status + + + {data.status.map((status, index) => ( + + ))} + + + ); diff --git a/src/rdap/components/DomainCard.tsx b/src/rdap/components/DomainCard.tsx index d015b83..874c9a0 100644 --- a/src/rdap/components/DomainCard.tsx +++ b/src/rdap/components/DomainCard.tsx @@ -1,12 +1,12 @@ import type { FunctionComponent } from "react"; import React from "react"; -import { rdapStatusInfo } from "@/rdap/constants"; import type { Domain } from "@/rdap/schemas"; import Events from "@/rdap/components/Events"; import Property from "@/components/Property"; -import PropertyList from "@/components/PropertyList"; +import CopyButton from "@/components/CopyButton"; +import StatusBadge from "@/components/StatusBadge"; import AbstractCard from "@/components/AbstractCard"; -import { Flex, Text, DataList, Badge } from "@radix-ui/themes"; +import { Flex, Text, DataList, Badge, Code } from "@radix-ui/themes"; export type DomainProps = { data: Domain; @@ -27,22 +27,49 @@ const DomainCard: FunctionComponent = ({ data, url }: DomainProps) > {data.unicodeName != undefined ? ( - {data.unicodeName} + + Unicode Name + + + {data.unicodeName} + + + + ) : null} - - {data.ldhName} - - {data.handle} + + + {data.unicodeName != undefined ? "LDH Name" : "Name"} + + + + {data.ldhName} + + + + + + Handle + + + {data.handle} + + + + - - {data.status.map((statusKey, index) => ( - - {statusKey} - - ))} - + + Status + + + {data.status.map((statusKey, index) => ( + + ))} + + + ); diff --git a/src/rdap/components/EntityCard.tsx b/src/rdap/components/EntityCard.tsx index e28710f..cebf49c 100644 --- a/src/rdap/components/EntityCard.tsx +++ b/src/rdap/components/EntityCard.tsx @@ -1,10 +1,9 @@ import type { FunctionComponent } from "react"; import React from "react"; import type { Entity } from "@/rdap/schemas"; -import Property from "@/components/Property"; -import PropertyList from "@/components/PropertyList"; +import CopyButton from "@/components/CopyButton"; import AbstractCard from "@/components/AbstractCard"; -import { Flex, DataList, Badge, Text } from "@radix-ui/themes"; +import { Flex, DataList, Badge, Text, Code } from "@radix-ui/themes"; export type EntityCardProps = { data: Entity; @@ -24,22 +23,45 @@ const EntityCard: FunctionComponent = ({ data, url }: EntityCar } > - {data.handle && {data.handle}} - - {data.roles.map((role, index) => ( - - {role} - - ))} - + {data.handle && ( + + Handle + + + {data.handle} + + + + + )} + + Roles + + + {data.roles.map((role, index) => ( + + {role} + + ))} + + + {data.publicIds && data.publicIds.length > 0 && ( - - {data.publicIds.map((publicId, index) => ( - - {`${publicId.identifier} (${publicId.type})`} - - ))} - + + Public IDs + + + {data.publicIds.map((publicId, index) => ( + + + {publicId.identifier} ({publicId.type}) + + + + ))} + + + )} diff --git a/src/rdap/components/Events.tsx b/src/rdap/components/Events.tsx index b881082..c659665 100644 --- a/src/rdap/components/Events.tsx +++ b/src/rdap/components/Events.tsx @@ -9,29 +9,33 @@ export type EventsProps = { const Events: FunctionComponent = ({ data }) => { return ( - + Event Date - Actor + Actor {data.map(({ eventAction, eventDate, eventActor }, index) => ( - - - {eventAction} - + + {eventAction} - + - - - {eventActor ?? "—"} - + + {eventActor ? ( + + {eventActor} + + ) : ( + + — + + )} ))} diff --git a/src/rdap/components/IPCard.tsx b/src/rdap/components/IPCard.tsx index 5c17847..4d11320 100644 --- a/src/rdap/components/IPCard.tsx +++ b/src/rdap/components/IPCard.tsx @@ -3,9 +3,10 @@ import React from "react"; import type { IpNetwork } from "@/rdap/schemas"; import Events from "@/rdap/components/Events"; import Property from "@/components/Property"; -import PropertyList from "@/components/PropertyList"; +import CopyButton from "@/components/CopyButton"; +import StatusBadge from "@/components/StatusBadge"; import AbstractCard from "@/components/AbstractCard"; -import { Flex, Text, DataList, Badge } from "@radix-ui/themes"; +import { Flex, Text, DataList, Badge, Code } from "@radix-ui/themes"; export type IPCardProps = { data: IpNetwork; @@ -28,25 +29,60 @@ const IPCard: FunctionComponent = ({ data, url }: IPCardProps) => { > {data.name} - {data.handle} + + Handle + + + {data.handle} + + + + {data.ipVersion.toUpperCase()} - {data.startAddress} - {data.endAddress} + + Start Address + + + {data.startAddress} + + + + + + End Address + + + {data.endAddress} + + + + {data.type} {data.country && {data.country}} {data.parentHandle && ( - {data.parentHandle} + + Parent Handle + + + {data.parentHandle} + + + + )} - - {data.status.map((status, index) => ( - - {status} - - ))} - + + Status + + + {data.status.map((status, index) => ( + + ))} + + + ); diff --git a/src/rdap/components/NameserverCard.tsx b/src/rdap/components/NameserverCard.tsx index 6119c0a..d653c63 100644 --- a/src/rdap/components/NameserverCard.tsx +++ b/src/rdap/components/NameserverCard.tsx @@ -1,9 +1,9 @@ import type { FunctionComponent } from "react"; import React from "react"; import type { Nameserver } from "@/rdap/schemas"; -import Property from "@/components/Property"; +import CopyButton from "@/components/CopyButton"; import AbstractCard from "@/components/AbstractCard"; -import { Flex, DataList, Badge, Text } from "@radix-ui/themes"; +import { Flex, DataList, Badge, Text, Code } from "@radix-ui/themes"; export type NameserverCardProps = { data: Nameserver; @@ -26,7 +26,15 @@ const NameserverCard: FunctionComponent = ({ } > - {data.ldhName} + + LDH Name + + + {data.ldhName} + + + + ); diff --git a/src/rdap/constants.ts b/src/rdap/constants.ts index 2bf7618..2b0de59 100644 --- a/src/rdap/constants.ts +++ b/src/rdap/constants.ts @@ -1,5 +1,45 @@ // see https://www.iana.org/assignments/rdap-json-values import type { RdapStatusType, RootRegistryType, SimplifiedTargetType } from "@/rdap/schemas"; +import type { badgePropDefs } from "@radix-ui/themes/src/components/badge.props"; + +type BadgeColor = (typeof badgePropDefs)["color"]["values"][number]; + +export const rdapStatusColors: Record = { + active: "jade", + inactive: "gray", + validated: "blue", + locked: "red", + "renew prohibited": "red", + "update prohibited": "red", + "transfer prohibited": "red", + "delete prohibited": "red", + "client delete prohibited": "red", + "client renew prohibited": "red", + "client transfer prohibited": "red", + "client update prohibited": "red", + "server delete prohibited": "red", + "server renew prohibited": "red", + "server transfer prohibited": "red", + "server update prohibited": "red", + "client hold": "orange", + "server hold": "orange", + "pending create": "amber", + "pending renew": "amber", + "pending transfer": "amber", + "pending update": "amber", + "pending delete": "amber", + "pending restore": "amber", + proxy: "violet", + private: "violet", + removed: "violet", + obscured: "violet", + associated: "blue", + "add period": "blue", + "auto renew period": "blue", + "redemption period": "orange", + "renew period": "blue", + "transfer period": "blue", +}; export const rdapStatusInfo: Record = { validated: diff --git a/src/rdap/schemas.ts b/src/rdap/schemas.ts index e7e35fa..e471f07 100644 --- a/src/rdap/schemas.ts +++ b/src/rdap/schemas.ts @@ -110,7 +110,7 @@ export const IpNetworkSchema = z.object({ type: z.string(), country: z.string().optional(), parentHandle: z.string().optional(), - status: z.string().array(), + status: z.array(StatusEnum), entities: z.array(EntitySchema).optional(), remarks: z.any().optional(), links: z.any().optional(), @@ -125,7 +125,7 @@ export const AutonomousNumberSchema = z.object({ endAutnum: z.number().positive(), // TODO: 32bit name: z.string(), type: z.string(), - status: z.array(z.string()), + status: z.array(StatusEnum), country: z.string().length(2), events: z.array(EventSchema), entities: z.array(EntitySchema),