build: upgrade Tailwind CSS from v3 to v4

- Migrate from Tailwind CSS 3.x to 4.1.15
- Replace autoprefixer with @tailwindcss/postcss 4.1.15
- Update PostCSS configuration for Tailwind v4 compatibility
- Convert globals.scss to globals.css for CSS-first configuration
- Remove legacy tailwind.config.cjs in favor of CSS-based config
- Update all component imports to use new globals.css
- Remove old.tsx.disabled file
This commit is contained in:
2025-10-22 02:11:09 -05:00
parent 66bf588647
commit 771a27da29
12 changed files with 420 additions and 1078 deletions

View File

@@ -32,6 +32,7 @@
"zod": "^4.1.12"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.15",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@types/node": "^24.9.1",
@@ -40,14 +41,13 @@
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"@vitest/ui": "^3.2.4",
"autoprefixer": "^10.4.7",
"eslint": "^9.38.0",
"eslint-config-next": "15.5.6",
"happy-dom": "^20.0.8",
"postcss": "^8.4.14",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1",
"tailwindcss": "^3.2.0",
"tailwindcss": "^4.1.15",
"type-fest": "^5.1.0",
"typescript": "^5.9.3",
"vitest": "^3.2.4"

958
pnpm-lock.yaml generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
'@tailwindcss/postcss': {},
},
};

View File

@@ -29,7 +29,7 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
<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="grow space-x-2">{header}</div>
<div className="flex grow gap-2">{header}</div>
{url != undefined ? (
<div className="pr-2">
<a href={url} target="_blank" rel="noreferrer">
@@ -96,7 +96,7 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
)}
</div>
{footer != null ? (
<div className="space-x-2 bg-zinc-700 p-2 pl-5">{footer}</div>
<div className="flex gap-2 bg-zinc-700 p-2 pl-5">{footer}</div>
) : null}
</div>
);

View File

@@ -23,7 +23,7 @@ const ErrorCard: FunctionComponent<ErrorCardProps> = ({
)}
>
<div className="flex">
<div className="flex-shrink-0">
<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">
@@ -35,7 +35,7 @@ const ErrorCard: FunctionComponent<ErrorCardProps> = ({
) : null}
<div className="mt-2">
{issues != undefined ? (
<ul role="list" className="list-disc space-y-1 pl-5">
<ul role="list" className="flex list-disc flex-col gap-1 pl-5">
{issues.map((issueText, index) => (
<li key={index}>{issueText}</li>
))}

View File

@@ -146,7 +146,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
className={clsx(
"lg:py-4.5 block w-full rounded-l-md border border-transparent",
"bg-zinc-700 py-2 pl-10 pr-1.5 text-sm placeholder-zinc-400 placeholder:translate-y-2 focus:text-zinc-200",
" focus:outline-none sm:text-sm md:py-3 md:text-base lg:text-lg"
" focus:outline-hidden sm:text-sm md:py-3 md:text-base lg:text-lg"
)}
disabled={isLoading}
placeholder={placeholders[selected]}
@@ -184,8 +184,8 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
<ListboxButton
className={clsx(
"relative h-full w-full cursor-default whitespace-nowrap rounded-r-lg bg-zinc-700 py-2 pl-1 pr-10 text-right",
"text-xs focus:outline-none focus-visible:border-indigo-500 sm:text-sm md:text-base lg:text-lg",
"focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 "
"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 "
)}
>
{/* Fetch special text for 'auto' mode, otherwise just use the options. */}
@@ -229,7 +229,7 @@ const LookupInput: FunctionComponent<LookupInputProps> = ({
<ListboxOptions
className={clsx(
"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 ring-opacity-5 focus:outline-none sm:text-sm"
"text-zinc-200 shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm"
)}
>
{Object.entries(objectNames).map(([key, value]) => (

View File

@@ -1,6 +1,6 @@
import { type AppType } from "next/dist/shared/lib/utils";
import "../styles/globals.scss";
import "../styles/globals.css";
const MyApp: AppType = ({ Component, pageProps }) => {
return <Component {...pageProps} />;

View File

@@ -41,7 +41,7 @@ const Index: NextPage = () => {
content="xevion, rdap, whois, rdap, domain name, dns, ip address"
/>
</Head>
<nav className="bg-zinc-850 px-5 py-4 shadow-sm">
<nav className="bg-zinc-850 px-5 py-4 shadow-xs">
<span
className="text-xl font-medium text-white"
style={{ fontSize: "larger" }}

View File

@@ -1,425 +0,0 @@
import { type NextPage } from "next";
import Head from "next/head";
import type { Register, TargetType } from "@/types";
import { placeholders, registryURLs } from "@/constants";
import { domainMatch, getBestURL, getType } from "@/rdap";
import type { FormEvent } from "react";
import { useEffect, useMemo, useState } from "react";
import { truthy } from "@/helpers";
import Generic, { type ParsedGeneric } from "@/components/lookup/Generic";
import type { ZodSchema } from "zod";
import { DomainSchema, RegisterSchema } from "@/schema";
const Old: NextPage = () => {
const [requestJSContact, setRequestJSContact] = useState(false);
const [followReferral, setFollowReferral] = useState(false);
const [object, setObject] = useState<string>("");
const [loading, setLoading] = useState(false);
const [response, setResponse] = useState<ParsedGeneric | null>(null);
const [error, setError] = useState<string | null>(null);
const [registryData, setRegistryData] = useState<Record<
string,
Register
> | null>(null);
// Change the selected type automatically
const uriType = useMemo<TargetType>(
function () {
return getType(object) ?? "domain";
},
[object]
);
async function loadRegistryData() {
setLoading(true);
let registersLoaded = 0;
const totalRegisters = Object.keys(registryURLs).length;
const responses = await Promise.all(
Object.entries(registryURLs).map(async ([registryType, url]) => {
const response = await fetch(url);
registersLoaded++;
return {
registryType,
response: RegisterSchema.parse(await response.json()),
};
})
);
setRegistryData(() => {
return Object.fromEntries(
responses.map(({ registryType, response }) => [registryType, response])
) as Record<string, Register>;
});
setLoading(false);
}
// construct an RDAP URL for the given object
function getRDAPURL(object: string): string | null {
let urls: string[] = [];
if (registryData == null) {
console.log("Registry data not loaded.");
return null;
}
const service = registryData[uriType]?.services;
if (service == undefined) return null;
services: for (const serviceItem of service) {
// special case for object tags, since the registrant email address is in the 0th position
const [rangeIndex, urlIndex] = uriType == "entity" ? [1, 2] : [0, 1];
for (const tld of serviceItem[rangeIndex]!) {
let match = false;
switch (uriType) {
case "domain":
match = domainMatch(tld, object);
break;
// case "autnum":
// match = asnMatch(range, object);
// break;
// case "entity":
// match = entityMatch(range, object);
// break;
// case "ip":
// match = ipMatch(range, object);
// break;
}
if (match) {
urls = serviceItem[urlIndex]!;
break services;
}
}
}
// no match
if (urls.length == 0) return null;
let url = getBestURL(urls);
// some bootstrap entries have a trailing slash, some don't
if (!url.endsWith("/")) url += "/";
return `${url + uriType}/${object}`;
}
async function submit(e?: FormEvent) {
e?.preventDefault();
console.log(`Submit invoked. ${uriType}/${JSON.stringify(object)}`);
const queryParams = requestJSContact ? "?jscard=1" : "";
const [url, schema]: [string, ZodSchema<ParsedGeneric>] | [null, null] =
(function () {
switch (uriType) {
// case 'url':
// return [object];
// case 'tld':
// return `https://root.rdap.org/domain/${object}${queryParams}`;
// case 'registrar':
// return `https://registrars.rdap.org/entity/${object}-IANA${queryParams}`;
// case 'json':
// return `json://${object}`
case "domain":
const temp = getRDAPURL(object);
if (temp) return [`${temp}${queryParams}`, DomainSchema];
return [null, null];
default:
setError(`No RDAP URL available for ${uriType} ${object}.`);
return [null, null];
}
})();
console.log(`URL: ${url ?? "null"}`);
if (url != null) await sendQuery(url, schema, followReferral);
}
async function sendQuery(
url: string,
schema: ZodSchema<ParsedGeneric>,
followReferral = false
) {
setLoading(true);
let data: ParsedGeneric | null = null;
if (url.startsWith("json://")) {
console.log("Mock JSON query detected.");
// run the callback with a mock XHR
data = schema.parse(JSON.parse(url.substring(7)));
} else {
try {
const response = await fetch(url);
if (response.status == 404) setError("This object does not exist.");
else if (response.status != 200)
setError(`Error ${response.status}: ${response.statusText}`);
data = schema.parse(await response.json());
} catch (e) {
console.log(e);
setLoading(false);
if (e instanceof Error) setError(e.toString());
return;
}
}
// if (followReferral && data.hasOwnProperty('links') != undefined) {
// console.log('Using followReferral.')
// for (const link of data.links) {
// if ('related' == link.rel && 'application/rdap+json' == link.type && link.href.match(/^(https?:|)\/\//i)) {
// await sendQuery(link.href, false)
// return;
// }
// }
// }
setLoading(false);
console.log(data);
try {
setResponse(data);
const url = `${window.location.origin}?type=${encodeURIComponent(
uriType
)}&object=${object}&request-jscontact=${
requestJSContact ? 1 : 0
}&follow-referral=${followReferral ? 1 : 0}`;
window.history.pushState(null, document.title, url);
} catch (e) {
if (e instanceof Error) setError(`Exception: ${e.message}`);
else setError("Unknown error.");
}
}
useEffect(() => {
// Load parameters from URL query string on page load
const params = new URLSearchParams(window.location.search);
// if (params.has('type'))
// setUriType(params.get('type') as ObjectType);
if (params.has("object")) setObject(params.get("object")!);
if (
params.has("request-jscontact") &&
truthy(params.get("request-jscontact"))
)
setRequestJSContact(true);
if (params.has("follow-referral") && truthy(params.get("follow-referral")))
setFollowReferral(true);
loadRegistryData().catch(console.error);
if (params.has("object") && (params.get("object")?.length ?? 0) > 0) {
setObject(params.get("object")!);
// submit().catch(console.error);
}
}, []);
return (
<>
<Head>
<title>rdap.xevion.dev</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="/shortcut-icon.svg" />
<meta name="description" content="" />
<meta
name="keywords"
content="xevion, rdap, whois, rdap, domain name, dns, ip address"
/>
</Head>
<>
<style jsx>{`
dd {
margin: 0.5em 0 1em 2em;
}
.card {
margin-bottom: 1em;
}
dl {
margin: 0;
}
.rdap-status-code,
.rdap-event-time {
border-bottom: 1px dashed silver;
}
#object {
text-transform: lowercase;
}
#spinner-msg {
height: 2em;
display: inline-block;
margin: -0.25em 0 0 0;
padding: 0.25em 0 0 0;
}
`}</style>
<nav className="navbar navbar-expand-lg navbar-dark shadow-sm">
<span className="text-white" style={{ fontSize: "larger" }}>
<a className="navbar-brand" href="#">
rdap.xevion.dev
</a>
</span>
</nav>
<div className="container mx-auto max-w-screen-lg py-12">
<form
onSubmit={(e) => {
void submit(e);
}}
className="form-inline"
>
<div className="col p-0">
<div className="input-group">
<div className="input-group-prepend">
<select
onChange={() => {
return false;
}}
className="custom-select border-zinc-700 bg-zinc-800 text-zinc-200"
id="type"
name="type"
value={uriType}
>
<option value="domain">Domain</option>
<option value="tld">TLD</option>
<option value="ip">IP/CIDR</option>
<option value="autnum">AS Number</option>
<option value="entity">Entity</option>
<option value="registrar">Registrar</option>
<option value="url">URL</option>
<option value="json">JSON</option>
</select>
</div>
<input
className="form-control border-zinc-700 bg-zinc-800 text-zinc-200 focus:border-zinc-600 focus:bg-zinc-700"
type="text"
placeholder={placeholders[uriType]}
disabled={loading}
onChange={(e) => {
setObject(e.target.value);
}}
required
/>
<div className="input-group-append">
<input
id="button"
type="button"
value="Submit"
onClick={(event) => {
void submit(event);
}}
className="btn btn-primary"
disabled={loading}
/>
</div>
</div>
</div>
</form>
<div
className="container p-0 italic text-[#aaa]"
style={{ fontSize: "small" }}
>
<div className="col pt-3 pb-1">
Options:&nbsp;
<label htmlFor="request-jscontact">
<input
name="request-jscontact"
id="request-jscontact"
type="checkbox"
/>
Request JSContact
</label>
&nbsp;
<label htmlFor="follow-referral">
<input
name="follow-referral"
id="follow-referral"
type="checkbox"
/>
Follow referral to registrar&apos;s RDAP record
</label>
</div>
</div>
<div id="output-div">
{response != null ? <Generic data={response} /> : null}
</div>
<p>
This page implements a <em>completely private lookup tool</em> for
domain names, IP addresses and Autonymous System Numbers (ASNs).
Only the relevant registry sees your query: your browser will
directly connect to the registry&apos;s RDAP server using an
encrypted HTTPS connection to protect the confidentiality of your
query. If you click the &quot;Follow referral to registrar&apos;s
RDAP record&quot; checkbox, then the sponsoring registrar may also
see your query.
</p>
<ul>
<li>
<a href="https://about.rdap.org" target="_new">
Click here
</a>{" "}
for more information about what RDAP is and how it differs from
traditional Whois.
</li>
<li>
Most generic TLDs now support RDAP, but only a few ccTLDs have
deployed RDAP so far. To see which TLDs support RDAP,{" "}
<a href="https://deployment.rdap.org" target="_new">
click here
</a>
.
</li>
<li>
There is no bootstrap registry for top-level domains or
ICANN-accredited registrars; instead these queries are sent to the
<a href="https://about.rdap.org/#additional" target="_new">
{"{"}root,registrars{"}"}.rdap.org servers
</a>
.
</li>
<li>
To submit feedback,{" "}
<a href="mailto:feedback@rdap.org">click here</a>. Please contact
the relevant registry or registrar if you have an issue with the
content of an RDAP response.
</li>
<li>
This tool is Free Software; for the license,{" "}
<a href="LICENSE">click here</a>. To fork a copy of the git
repository,{" "}
<a
rel="noopener"
target="_new"
href="https://gitlab.centralnic.com/centralnic/rdap-web-client"
>
click here
</a>
.
</li>
<li>
This page uses{" "}
<a
rel="noopener"
target="_new"
href="https://github.com/whitequark/ipaddr.js/"
>
ipaddr.js
</a>{" "}
by{" "}
<a rel="noopener" target="_new" href="https://whitequark.org/">
whitequark
</a>
.
</li>
</ul>
</div>
</>
</>
);
};
export default Old;

36
src/styles/globals.css Normal file
View File

@@ -0,0 +1,36 @@
@import "tailwindcss";
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Inter:wght@100..900&display=swap");
@theme {
--font-sans: "Inter var", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--color-zinc-850: #1D1D20;
}
@source "../**/*.{js,ts,jsx,tsx}";
dd {
margin: 0.5em 0 1em 2em;
}
.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;
}
.scrollbar-thin {
scrollbar-width: thin;
}

View File

@@ -1,31 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700\&family=Inter:wght@100..900&display=swap");
dd {
margin: 0.5em 0 1em 2em;
}
.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;
}
.scrollbar-thin {
scrollbar-width: thin;
}

View File

@@ -1,21 +0,0 @@
/** @type {import('tailwindcss').Config} */
const defaultTheme = require("tailwindcss/defaultTheme");
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
darkMode: "class",
theme: {
extend: {
fontFamily: {
mono: ["IBM Plex Mono", ...defaultTheme.fontFamily.mono],
sans: ["Inter var", ...defaultTheme.fontFamily.sans],
},
colors: {
zinc: {
850: "#1D1D20",
},
},
},
},
plugins: [],
};