diff --git a/.changeset/dry-spoons-invite.md b/.changeset/dry-spoons-invite.md new file mode 100644 index 000000000..a4fe46a11 --- /dev/null +++ b/.changeset/dry-spoons-invite.md @@ -0,0 +1,5 @@ +--- +"@namehash/nameguard-react": minor +--- + +Add custom onOpenReport handlers for Report components diff --git a/.changeset/fair-pumas-chew.md b/.changeset/fair-pumas-chew.md new file mode 100644 index 000000000..63c8757a1 --- /dev/null +++ b/.changeset/fair-pumas-chew.md @@ -0,0 +1,6 @@ +--- +"@namehash/nameguard-react": minor +"@namehash/nameguard": minor +--- + +Add custom getReportURL handlers for Report components diff --git a/.changeset/famous-jobs-wait.md b/.changeset/famous-jobs-wait.md new file mode 100644 index 000000000..f2d03e894 --- /dev/null +++ b/.changeset/famous-jobs-wait.md @@ -0,0 +1,5 @@ +--- +"@namehash/nameguard-react": minor +--- + +Move NameGuard URL related logic to NameGuard React url.ts diff --git a/apps/examples.nameguard.io/index.d.ts b/apps/examples.nameguard.io/index.d.ts new file mode 100644 index 000000000..a08b613da --- /dev/null +++ b/apps/examples.nameguard.io/index.d.ts @@ -0,0 +1 @@ +declare module "@namehash/nameguard-react"; diff --git a/apps/examples.nameguard.io/package.json b/apps/examples.nameguard.io/package.json index e5e5acba3..331fda466 100644 --- a/apps/examples.nameguard.io/package.json +++ b/apps/examples.nameguard.io/package.json @@ -12,6 +12,7 @@ "@headlessui/react": "1.7.17", "@namehash/nameguard": "workspace:*", "@namehash/nameguard-react": "workspace:*", + "@namehash/ens-utils": "workspace:*", "@namehash/nameguard-js": "workspace:*", "viem": "^2.9.3", "classcat": "5.0.4", diff --git a/apps/examples.nameguard.io/src/app/components/ImpersonationReport.tsx b/apps/examples.nameguard.io/src/app/components/ImpersonationReport.tsx index 6a1a947de..8b1af4297 100644 --- a/apps/examples.nameguard.io/src/app/components/ImpersonationReport.tsx +++ b/apps/examples.nameguard.io/src/app/components/ImpersonationReport.tsx @@ -1,15 +1,17 @@ "use client"; -import { Rating, type SecurePrimaryNameResult } from "@namehash/nameguard"; +import { type SecurePrimaryNameResult } from "@namehash/nameguard"; import { RatingIcon, RatingIconSize, - RatingLoadingIcon, + getReportURL, ratingTextColor, + RatingLoadingIcon, } from "@namehash/nameguard-react"; import cc from "classcat"; import { Tooltip } from "./Tooltip"; +import { buildENSName } from "@namehash/ens-utils"; type ImpersonationReportProps = { data?: SecurePrimaryNameResult; @@ -66,9 +68,9 @@ export function ImpersonationReport({ data }: ImpersonationReportProps) {
{ + alert(`Example of custom logic to handle a request to open a NameGuard report for name "${name.displayName}".`); +}; + export default function ReportDocsPage() { return (
@@ -36,35 +40,35 @@ export default function ReportDocsPage() {
@@ -76,35 +80,35 @@ export default function ReportDocsPage() {
@@ -116,35 +120,35 @@ export default function ReportDocsPage() {
@@ -156,76 +160,76 @@ export default function ReportDocsPage() {
-
{""}
+
{""}
alert(ensName.name)} + onOpenReport={customOpenReportHandler} />
alert(ensName.name)} + onOpenReport={customOpenReportHandler} />
alert(ensName.name)} + onOpenReport={customOpenReportHandler} />
alert(ensName.name)} + onOpenReport={customOpenReportHandler} />
alert(ensName.name)} + name={getExampleReportName()} + onOpenReport={customOpenReportHandler} />
@@ -255,72 +259,72 @@ export default function ReportDocsPage() {
- +
-
{""}
+
{""}
alert(ensName.name)} + onOpenReport={customOpenReportHandler} />
alert(ensName.name)} + onOpenReport={customOpenReportHandler} />
alert(ensName.name)} + onOpenReport={customOpenReportHandler} />
alert(ensName.name)} + onOpenReport={customOpenReportHandler} />
alert(ensName.name)} + name={getExampleReportName()} + onOpenReport={customOpenReportHandler} />
@@ -333,38 +337,31 @@ export default function ReportDocsPage() {
alert(ensName.name)} />
alert(ensName.name)} />
alert(ensName.name)} + displayUnnormalizedNames={true} />
alert(ensName.name)} />
- alert(ensName.name)} - /> +
diff --git a/apps/nameguard.io/src/components/organisms/HeroCarousel.tsx b/apps/nameguard.io/src/components/organisms/HeroCarousel.tsx index eeaff873f..58519a4f6 100644 --- a/apps/nameguard.io/src/components/organisms/HeroCarousel.tsx +++ b/apps/nameguard.io/src/components/organisms/HeroCarousel.tsx @@ -50,9 +50,10 @@ export async function HeroCarousel() { */} {data?.results?.map((report, index) => ( ))} {/* @@ -62,9 +63,10 @@ export async function HeroCarousel() { */} {data?.results?.map((report, index) => ( ))} diff --git a/apps/namehashlabs.org/components/2 - molecules/avatar-with-tooltip.tsx b/apps/namehashlabs.org/components/2 - molecules/avatar-with-tooltip.tsx index 2c36cc775..0c0fb08f4 100644 --- a/apps/namehashlabs.org/components/2 - molecules/avatar-with-tooltip.tsx +++ b/apps/namehashlabs.org/components/2 - molecules/avatar-with-tooltip.tsx @@ -9,7 +9,6 @@ import { EnsSolidIcon, TwitterIcon } from "../1 - atoms/"; import { Profile } from "@/data/ensProfiles"; import { useId } from "react"; import cc from "classcat"; -import { URL } from "url"; interface AvatarWithTooltipProps { className?: string; diff --git a/packages/nameguard-react/.gitignore b/packages/nameguard-react/.gitignore new file mode 100644 index 000000000..c20a41dca --- /dev/null +++ b/packages/nameguard-react/.gitignore @@ -0,0 +1,3 @@ + +# dependencies +/node_modules diff --git a/packages/nameguard-react/package.json b/packages/nameguard-react/package.json index a5418d4bf..6e948a3ba 100644 --- a/packages/nameguard-react/package.json +++ b/packages/nameguard-react/package.json @@ -17,11 +17,12 @@ ], "scripts": { "build": "tsup", - "dev": "tsup --watch --clean=false" + "dev": "tsup --watch --clean=false", + "test": "vitest" }, "exports": { ".": { - "types": "./dist/index.d.ts", + "types": "./dist/index.d.mts", "import": "./dist/index.mjs", "default": "./dist/index.mjs" }, @@ -32,7 +33,7 @@ }, "main": "./dist/index.mjs", "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", + "types": "./dist/index.d.mts", "styles": "./dist/index.css", "dependencies": { "@headlessui-float/react": "0.11.4", @@ -40,8 +41,8 @@ "@heroicons/react": "2.0.18", "@namehash/ens-utils": "workspace:*", "@namehash/ens-webfont": "workspace:*", - "@namehash/namekit-react": "workspace:*", "@namehash/nameguard": "workspace:*", + "@namehash/namekit-react": "workspace:*", "classcat": "5.0.4", "sonner": "1.2.3", "swr": "2.2.4", @@ -58,7 +59,8 @@ "react-dom": "18.3.1", "tailwindcss": "3.4.3", "tsup": "^7.2.0", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "vitest": "^1.6.0" }, "peerDependencies": { "react": "^18.2.0", diff --git a/packages/nameguard-react/src/components/Report/CheckResultCodeIcon.tsx b/packages/nameguard-react/src/components/Report/CheckResultCodeIcon.tsx index 2f30e97c8..68339987d 100644 --- a/packages/nameguard-react/src/components/Report/CheckResultCodeIcon.tsx +++ b/packages/nameguard-react/src/components/Report/CheckResultCodeIcon.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { Tooltip } from "@namehash/namekit-react/client"; import { CheckResultCode } from "@namehash/nameguard"; +import { Tooltip } from "@namehash/namekit-react/client"; import { CheckResultCodePassIcon } from "../icons/CheckResultCodePassIcon"; import { CheckResultCodeWarnIcon } from "../icons/CheckResultCodeWarnIcon"; import { CheckResultCodeAlertIcon } from "../icons/CheckResultCodeAlertIcon"; diff --git a/packages/nameguard-react/src/components/Report/LoadingSkeleton.tsx b/packages/nameguard-react/src/components/Report/LoadingSkeleton.tsx index 1c37446b9..fb80b26d1 100644 --- a/packages/nameguard-react/src/components/Report/LoadingSkeleton.tsx +++ b/packages/nameguard-react/src/components/Report/LoadingSkeleton.tsx @@ -1,11 +1,11 @@ import React from "react"; -import { RatingIcon, RatingIconSize } from "./RatingIcon"; -import { ReportFormattedDisplayName } from "./ReportFormattedDisplayName"; +import { RatingIconSize } from "./RatingIcon"; import { ReportChangesApplied } from "./ReportChangesApplied"; -import { ParsedName } from "@namehash/ens-utils"; import { RatingLoadingIcon } from "../icons/RatingLoadingIcon"; import { DisplayedName } from "../DisplayedName/DisplayedName"; +import { ReportFormattedDisplayName } from "./ReportFormattedDisplayName"; +import { ParsedName } from "@namehash/ens-utils"; type LoadingSkeletonProps = { parsedName: ParsedName; diff --git a/packages/nameguard-react/src/components/Report/Report.tsx b/packages/nameguard-react/src/components/Report/Report.tsx index 538159f1b..39c76d11d 100644 --- a/packages/nameguard-react/src/components/Report/Report.tsx +++ b/packages/nameguard-react/src/components/Report/Report.tsx @@ -1,7 +1,7 @@ import React, { Fragment, useMemo } from "react"; import useSWR from "swr"; import { type NameGuardReport, nameguard, Rating } from "@namehash/nameguard"; -import { parseName, Normalization } from "@namehash/ens-utils"; +import { parseName, Normalization, buildENSName } from "@namehash/ens-utils"; import { Toaster } from "sonner"; import { type Settings } from "../../stores/settings"; @@ -116,10 +116,12 @@ export const Report = ({
-
- - -
+ {data && externalLinks && ( +
+ + +
+ )}
{isLoading && !hadLoadingError && normalizationUnknown && ( diff --git a/packages/nameguard-react/src/components/Report/ReportModalNameBadge.tsx b/packages/nameguard-react/src/components/Report/ReportModalNameBadge.tsx index 5d8880296..9a068bbb5 100644 --- a/packages/nameguard-react/src/components/Report/ReportModalNameBadge.tsx +++ b/packages/nameguard-react/src/components/Report/ReportModalNameBadge.tsx @@ -1,30 +1,36 @@ import React from "react"; import { ENSName } from "@namehash/ens-utils"; -import { ConsolidatedNameGuardReport } from "@namehash/nameguard"; import { useSearchStore } from "../../stores/search"; -import { ReportBadge } from "../ReportBadge"; +import { ReportBadge } from "../.."; +import { ReportBadgeProps } from "../ReportBadge"; -interface ReportModalNameBadgeProps { - data?: ConsolidatedNameGuardReport; - hadLoadingError?: boolean; - ensName: ENSName; -} +/** + * Same as `ReportBadgeProps` but without `onOpenReport` + * since `ReportModalNameBadge` provides its own implementation of + * `onOpenReport`. + */ +type ReportModalNameBadgeProps = Omit; export function ReportModalNameBadge({ + name, data, - ensName, - hadLoadingError = false, + hadLoadingError, + displayUnnormalizedNames, + maxNameDisplayWidth, ...props }: ReportModalNameBadgeProps) { const { openModal } = useSearchStore(); return ( openModal(ensName.name)} + displayUnnormalizedNames={displayUnnormalizedNames} + maxNameDisplayWidth={maxNameDisplayWidth} + onOpenReport={(name: ENSName) => { + openModal(name.name); + }} /> ); } diff --git a/packages/nameguard-react/src/components/ReportBadge/index.tsx b/packages/nameguard-react/src/components/ReportBadge/index.tsx index 394d773d6..c5bda30dc 100644 --- a/packages/nameguard-react/src/components/ReportBadge/index.tsx +++ b/packages/nameguard-react/src/components/ReportBadge/index.tsx @@ -1,114 +1,114 @@ "use client"; -import { type ConsolidatedNameGuardReport } from "@namehash/nameguard"; -import React, { useEffect } from "react"; -import cc from "classcat"; - -import { Link } from "@namehash/namekit-react"; +import { type ConsolidatedNameGuardReport } from "@namehash/nameguard"; import { ENSName } from "@namehash/ens-utils"; +import React from "react"; + import { ReportIcon } from "../ReportIcon/index"; -import { RatingLoadingIcon, RatingIconSize, DisplayedName } from "../.."; -import { UnknownReportIcon } from "../UnknownReportIcon/UnknownReportIcon"; +import { RatingIconSize, DisplayedName } from "../.."; +import { OpenReportHandler, openReport } from "../../utils/openreport"; -interface ReportBadgeProps { - /* - The data prop is the consolidated report for the ensName. - The ensName prop is the ENSName object. +export interface ReportBadgeProps { + /** + * The `ENSName` that this `ReportBadge` is related to. + * + * Used to provide functionality even when the `data` prop is `undefined`. + * (such as during data loading). + */ + name: ENSName; - The data prop should always be relative to the ensName prop. - This means that the data prop should always be the report for - the ensName provided in the ensName prop. - */ - ensName: ENSName; + /** + * - If `undefined` and `hasLoadingError` is `false`: + * - The component will display a loading state. + * - If `undefined` and `hasLoadingError` is `true`: + * - The component will display an unknown state. + * - If `defined`: + * - The component will display a summary of the report contained within `data`. + * - The value of `data.name` must be equal to the value of `name.name`. + * + * @default undefined + */ data?: ConsolidatedNameGuardReport; + /** + * - If `true`, the component will display an error state. + * - The value of this field is only considered if `data` is `undefined`. + * + * @default false + */ hadLoadingError?: boolean; + + /** + * - If `true`, the component will display the literal value of `name` even + * if it is unnormalized. WARNING: NOT RECOMMENDED. This may display names + * that are not "safe". + * - Otherwise, the component will display "display names" (i.e. normalized + * names or a "safe" version of unnormalized names). + * + * @default false + */ displayUnnormalizedNames?: boolean; - onClickOverride?: (ensName: ENSName) => void; - /* - Below number is a measure of the maximum width that the ensName - should have inside ReportBadge. If the ensName displayed is longer - than this number, the badge will truncate the ensName and display a - tooltip on hover that shows the full ensName. This number is measured in pixels. - */ - maxDisplayWidth?: number; + /** + * The maximum width that the display of `name` should have inside the + * `ReportBadge`. If the display of `name` is longer than this number, the + * badge will truncate the display of `name` and display a tooltip when the + * name is hovered that displays the full value of `name`. This number is + * measured in pixels. + * + * @default 200 + */ + maxNameDisplayWidth?: number; + + /** + * The custom `OpenReportHandler` to call when: + * - The report icon is clicked. + * - The link to inspect the name for details in the tooltip is clicked. + * - The badge is clicked. + * + * If `undefined`, the default `OpenReportHandler` will be used. + * + * @default undefined + */ + onOpenReport?: OpenReportHandler; } +const DEFAULT_MAX_NAME_DISPLAY_WIDTH = 200; + export function ReportBadge({ + name, data, - ensName, - maxDisplayWidth, - onClickOverride, hadLoadingError = false, displayUnnormalizedNames = false, + maxNameDisplayWidth = DEFAULT_MAX_NAME_DISPLAY_WIDTH, + onOpenReport, }: ReportBadgeProps) { - const buttonClass = - "flex-shrink-0 appearance-none bg-white transition-colors hover:bg-gray-50 border border-gray-200 rounded-md px-2.5 py-1.5 inline-flex items-center"; - const buttonAndCursorClass = cc([buttonClass, "cursor-pointer"]); - - const onClickHandler = () => { - if (onClickOverride) onClickOverride(ensName); - else { - window.location.href = `https://nameguard.io/inspect/${encodeURIComponent( - ensName.name, - )}`; - } - }; - - useEffect(() => { - if (data) { - if (data.name !== ensName.name) { - throw new Error( - `The data received is from: ${data.name} and not for the provided ensName, which is ${ensName.name}`, - ); - } - } - }, [data]); - return ( - ); } diff --git a/packages/nameguard-react/src/components/ReportIcon/index.tsx b/packages/nameguard-react/src/components/ReportIcon/index.tsx index b645e4552..aa7525c0f 100644 --- a/packages/nameguard-react/src/components/ReportIcon/index.tsx +++ b/packages/nameguard-react/src/components/ReportIcon/index.tsx @@ -1,152 +1,175 @@ "use client"; import React, { useEffect } from "react"; -import { - CheckResultCode, - ConsolidatedNameGuardReport, -} from "@namehash/nameguard"; +import { ConsolidatedNameGuardReport } from "@namehash/nameguard"; import cc from "classcat"; import { ENSName } from "@namehash/ens-utils"; import { Link } from "@namehash/namekit-react"; - import { Tooltip } from "@namehash/namekit-react/client"; -import { RatingLoadingIcon } from "../icons/RatingLoadingIcon"; import { RatingIcon, RatingIconSize } from "../Report/RatingIcon"; -import { checkResultCodeTextColor, ratingTextColor } from "../../utils/text"; -import { UnknownReportIcon } from "../UnknownReportIcon/UnknownReportIcon"; - -type ReportIconProps = { - onClickOverride?: (ensName: ENSName) => void; - - /* - The data prop is the consolidated report for the ensName. - The ensName prop is the ENSName object. +import { ratingTextColor } from "../../utils/text"; +import { OpenReportHandler, openReport } from "../../utils/openreport"; +import { RatingUnknownIcon } from "../icons/RatingUnknownIcon"; +import { RatingLoadingIcon } from "../icons/RatingLoadingIcon"; - The data prop should always be relative to the ensName prop. - This means that the data prop should always be the report for - the ensName provided in the ensName prop. - */ - ensName: ENSName; +interface ReportShieldProps { + /** + * The `ENSName` that this `ReportIcon` is related to. + * + * Used to provide functionality even when the `data` prop is `undefined` + * (such as during data loading). + */ + name: ENSName; + + /** + * - If `undefined` and `hasLoadingError` is `false`: + * - The component will display a loading state. + * - If `undefined` and `hasLoadingError` is `true`: + * - The component will display an unknown state. + * - If `defined`: + * - The component will display a summary of the report contained within `data`. + * - The value of `data.name` must be equal to the value of `name.name`. + * + * @default undefined + */ data?: ConsolidatedNameGuardReport; + /** + * - If `true`, the component will display an error state. + * - The value of this field is only considered if `data` is `undefined`. + * + * @default false + */ hadLoadingError?: boolean; + + /** + * The size of the `RatingIcon` to display. + * + * @default RatingIconSize.small + */ size?: RatingIconSize; -} & React.ComponentProps<"svg">; -declare global { - interface Window { - location: Location; - } + /** + * The custom `OpenReportHandler` to call when: + * - The report icon is clicked. + * - The link to inspect the name for details in the tooltip is clicked. + * + * If `undefined`, the default `OpenReportHandler` will be used. + * + * @default undefined + */ + onOpenReport?: OpenReportHandler; } export function ReportIcon({ - ensName, + name, data, - onClickOverride, - hadLoadingError, + hadLoadingError = false, size = RatingIconSize.small, + onOpenReport, /* - Props are applied to the shield icon which is the onHover trigger element + Props are applied to the Report Icon triggeer which is the onHover trigger element for the tooltip with Report information. For examples, please visit the - https://nameguard.io/docs/report and see the ReportIcon docs. Any - additional props are passed to the shield icon that when hovered, - displays the tooltip with the report information. + https://nameguard.io/docs/report and see the ReportIcon docs. Any + additional props received are passed to the Report Icon that when + hovered, displays the tooltip with the report information. */ ...props -}: ReportIconProps) { - const onClickHandler = () => { - if (onClickOverride) onClickOverride(ensName); - else { - window.location.href = `https://nameguard.io/inspect/${encodeURIComponent( - ensName.name, - )}`; +}: ReportShieldProps) { + useEffect(() => { + if (data) { + if (data.name !== name.name) { + throw new Error( + `ReportIcon error: The name in the provided data: "${data.name}" does not match the expected name "${name.name}".`, + ); + } } - }; + }, [data]); - if (hadLoadingError) { - return ( - - - Inspect name for details - - - ); - } + let icon: React.ReactNode; + let tooltipIcon: React.ReactNode; + let tooltipTitleClass: string; + let tooltipTitle: string; + let tooltipSubtitle: string | undefined; + let tooltipMessage: string | undefined; - if (!data) { - return ( + if (hadLoadingError) { + icon = ; + tooltipIcon = ; + tooltipTitleClass = "font-semibold mb-1 text-white"; + tooltipTitle = "Error loading report"; + } else if (!data) { + // TODO: an isInteractive prop is planned to be added to `RatingLoadingIcon` + // in https://app.shortcut.com/ps-web3/story/25745/refine-loading-state-of-ratingicon + icon = ; + // TODO: the need to pass this `fill` prop is planned to be removed in + // https://app.shortcut.com/ps-web3/story/25745/refine-loading-state-of-ratingicon + tooltipIcon = ( ); - } + tooltipTitleClass = "font-semibold mb-1 text-white"; + tooltipTitle = "Loading report..."; + } else { + const { title, subtitle, rating, risk_count, highest_risk } = data; - useEffect(() => { - if (data) { - if (data.name !== ensName.name) { - throw new Error( - `The data received is from: ${data.name} and not for the provided ensName, which is ${ensName.name}`, - ); - } + icon = ( + + ); + tooltipIcon = ( + + ); + tooltipTitleClass = cc(["font-semibold mb-1", ratingTextColor(rating)]); + tooltipTitle = title; + if (risk_count >= 1) { + tooltipSubtitle = `${risk_count} risk${risk_count !== 1 ? "s" : ""} detected`; } - }, [data]); + tooltipMessage = highest_risk?.message || subtitle; + } - const { title, subtitle, rating, risk_count, highest_risk } = data; - - const textClass = cc(["font-semibold mb-1", ratingTextColor(rating)]); - - return ( - - } - > -
-
- -
+ const iconButton = ( +
openReport(name, onOpenReport)}> + {icon} +
+ ); -
-
- {title} - {risk_count >= 1 && ( - - {risk_count} risk{risk_count !== 1 && "s"} detected - - )} -
+ const minTooltipWidth = tooltipSubtitle ? 300 : 200; + + const tooltip = ( +
+
{tooltipIcon}
+ +
+
+ {tooltipTitle} + {tooltipSubtitle && ( + + {tooltipSubtitle} + + )} +
+ {tooltipMessage && (
- {highest_risk?.message || subtitle} + {tooltipMessage}
+ )} - - Inspect name for details - -
+ openReport(name, onOpenReport)} + variant="underline" + size="small" + > + Inspect name for details +
- +
); + + return {tooltip}; } diff --git a/packages/nameguard-react/src/components/Search/SearchEmptyState.tsx b/packages/nameguard-react/src/components/Search/SearchEmptyState.tsx index a92c30a9f..a55a442d5 100644 --- a/packages/nameguard-react/src/components/Search/SearchEmptyState.tsx +++ b/packages/nameguard-react/src/components/Search/SearchEmptyState.tsx @@ -130,17 +130,19 @@ export const SearchEmptyState = () => { {(hadLoadingError || isLoading) && examples.map((_, index) => ( ))} {data?.results?.map((report, index) => ( ))}
diff --git a/packages/nameguard-react/src/components/Share/Share.tsx b/packages/nameguard-react/src/components/Share/Share.tsx index 33bac8dba..c7e13002e 100644 --- a/packages/nameguard-react/src/components/Share/Share.tsx +++ b/packages/nameguard-react/src/components/Share/Share.tsx @@ -15,15 +15,16 @@ import { Tooltip } from "@namehash/namekit-react/client"; import { CheckResultCode } from "@namehash/nameguard"; import { checkResultCodeTextColor } from "../../utils/text"; import { DisplayedName } from "../DisplayedName/DisplayedName"; -import { buildENSName } from "@namehash/ens-utils"; +import { buildENSName, ENSName } from "@namehash/ens-utils"; +import { ReportURLGenerator, getReportURL } from "../../utils/url"; type ShareProps = { - name?: string; + name: ENSName; + generator?: ReportURLGenerator; }; -function createTwitterLink(name: string) { - const tweetText = `Check out the NameGuard Report for ${name}\n`; - const url = `https://nameguard.io/inspect/${encodeURIComponent(name)}`; +function createTwitterLink(name: ENSName, url: string) { + const tweetText = `Check out the NameGuard Report for ${name.displayName}\n`; return `https://twitter.com/intent/tweet?text=${encodeURIComponent( tweetText, @@ -43,21 +44,19 @@ function createMailToLink(subject, body) { return `mailto:?subject=${subjectEncoded}&body=${bodyEncoded}`; } -export function Share({ name }: ShareProps) { +export function Share({ name, generator }: ShareProps) { const [isOpen, setIsOpen] = useState(false); - const twitterLink = createTwitterLink(name); - const telegramLink = createTelegramLink( - `https://nameguard.io/inspect/${encodeURIComponent(name)}`, - ); + const targetUrl = getReportURL(name, generator).href; + + const twitterLink = createTwitterLink(name, targetUrl); + const telegramLink = createTelegramLink(targetUrl); const emailLink = createMailToLink( - `NameGuard Report for ${name}`, - `Check this out!\nhttps://nameguard.io/inspect/${encodeURIComponent(name)}`, + `NameGuard Report for ${name.displayName}`, + `Check this out!\n${targetUrl}`, ); const copyLinkToClipboard = () => { - navigator.clipboard.writeText( - `https://nameguard.io/inspect/${encodeURIComponent(name)}`, - ); + navigator.clipboard.writeText(targetUrl); toast.success("Link copied to clipboard", { icon: ( {name && ( - <> - - + )}
diff --git a/packages/nameguard-react/src/components/UnknownReportIcon/UnknownReportIcon.tsx b/packages/nameguard-react/src/components/UnknownReportIcon/UnknownReportIcon.tsx deleted file mode 100644 index cfff3e664..000000000 --- a/packages/nameguard-react/src/components/UnknownReportIcon/UnknownReportIcon.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; - -import { Tooltip } from "@namehash/namekit-react/client"; -import { RatingIconSize } from "../Report/RatingIcon"; -import { RatingUnknownIcon } from "../icons/RatingUnknownIcon"; - -type UnknownShieldProps = { - size?: RatingIconSize; -} & React.ComponentProps<"svg">; - -export const UnknownReportIcon = ({ - size = RatingIconSize.small, - - /* - Props are applied to the shield icon which is the onHover trigger element - for the tooltip with Report information. For examples, please visit the - https://nameguard.io/docs/report and see the ReportIcon docs. Any - additional props are passed to the shield icon that when hovered, - displays the tooltip with the report information. - */ - ...props -}: UnknownShieldProps) => { - return ( - -
-
- -
- -
-
- - Unable to analyze - -
- -
- Refresh the page to try again. -
-
-
-
- ); -}; diff --git a/packages/nameguard-react/src/components/icons/RatingLoadingSmallIcon.tsx b/packages/nameguard-react/src/components/icons/RatingLoadingSmallIcon.tsx index 1810be571..774546bfe 100644 --- a/packages/nameguard-react/src/components/icons/RatingLoadingSmallIcon.tsx +++ b/packages/nameguard-react/src/components/icons/RatingLoadingSmallIcon.tsx @@ -15,8 +15,8 @@ export const RatingLoadingSmallIcon = ( void; + +export function defaultOpenReportHandler(name: ENSName, generator?: ReportURLGenerator): void { + redirectToReport(name, generator); +} + +export function openReport(name: ENSName, handler?: OpenReportHandler, generator?: ReportURLGenerator): void { + if (handler) { + handler(name, generator); + } else { + defaultOpenReportHandler(name, generator); + } +} diff --git a/packages/nameguard-react/src/utils/url.test.ts b/packages/nameguard-react/src/utils/url.test.ts new file mode 100644 index 000000000..3f0c5078b --- /dev/null +++ b/packages/nameguard-react/src/utils/url.test.ts @@ -0,0 +1,18 @@ +import { buildENSName } from "@namehash/ens-utils"; +import { describe, it, expect } from "vitest"; +import { defaultReportURLGenerator } from "./url"; + +describe("defaultNameGuardReportUrl", () => { + it("returns default NameGuard report URL for notrab.eth", () => { + const result = defaultReportURLGenerator(buildENSName("notrab.eth")).href; + const expectedResult = "https://nameguard.io/inspect/notrab.eth"; + + expect(result).toBe(expectedResult); + }); + it("returns default NameGuard report URL for 🐈‍⬛.eth", () => { + const result = defaultReportURLGenerator(buildENSName("🐈‍⬛.eth")).href; + const expectedResult = `https://nameguard.io/inspect/%F0%9F%90%88%E2%80%8D%E2%AC%9B.eth`; + + expect(result).toBe(expectedResult); + }); +}); diff --git a/packages/nameguard-react/src/utils/url.ts b/packages/nameguard-react/src/utils/url.ts new file mode 100644 index 000000000..ddcc1e414 --- /dev/null +++ b/packages/nameguard-react/src/utils/url.ts @@ -0,0 +1,21 @@ +import { ENSName } from "@namehash/ens-utils"; + +export type ReportURLGenerator = (name: ENSName) => URL; + +export function defaultReportURLGenerator(ensName: ENSName): URL { + return new URL( + `https://nameguard.io/inspect/${encodeURIComponent(ensName.name)}`, + ); +} + +export function getReportURL(name: ENSName, generator?: ReportURLGenerator): URL { + if (generator) { + return generator(name); + } else { + return defaultReportURLGenerator(name); + } +} + +export function redirectToReport(ensName: ENSName, generator?: ReportURLGenerator) { + window.location.href = getReportURL(ensName, generator).href; +} diff --git a/packages/namekit-react/package.json b/packages/namekit-react/package.json index 288766e2e..151bb0493 100644 --- a/packages/namekit-react/package.json +++ b/packages/namekit-react/package.json @@ -41,8 +41,10 @@ "dependencies": { "@headlessui-float/react": "0.11.4", "@headlessui/react": "1.7.17", + "@namehash/ens-webfont": "workspace:*", "@namehash/ens-utils": "workspace:*", - "@namehash/ens-webfont": "workspace:*" + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { "@namekit/tsconfig": "workspace:*", diff --git a/packages/namekit-react/tsup.config.js b/packages/namekit-react/tsup.config.js index f09ef5ce3..eb309e9b8 100644 --- a/packages/namekit-react/tsup.config.js +++ b/packages/namekit-react/tsup.config.js @@ -7,6 +7,9 @@ export default defineConfig({ }, dts: true, format: ["esm"], + splitting: true, + sourcemap: true, + minify: false, clean: true, skipNodeModulesBundle: true, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7f1a6be3..1b6220c00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ importers: '@headlessui/react': specifier: 1.7.17 version: 1.7.17(react-dom@18.2.0)(react@18.2.0) + '@namehash/ens-utils': + specifier: workspace:* + version: link:../../packages/ens-utils '@namehash/nameguard': specifier: workspace:* version: link:../../packages/nameguard-sdk @@ -309,7 +312,7 @@ importers: version: 8.1.8(prettier@3.2.5)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.4)(vite@5.1.7) '@storybook/test': specifier: ^8.1.8 - version: 8.1.8 + version: 8.1.8(vitest@1.6.0) '@types/react': specifier: ^18.3.2 version: 18.3.3 @@ -451,7 +454,7 @@ importers: version: 8.1.8(prettier@3.2.5)(react-dom@18.3.1)(react@18.3.1)(typescript@5.3.3) '@storybook/test': specifier: ^8.1.8 - version: 8.1.8 + version: 8.1.8(vitest@1.6.0) '@types/react': specifier: 18.3.3 version: 18.3.3 @@ -479,6 +482,9 @@ importers: typescript: specifier: ^5.2.2 version: 5.3.3 + vitest: + specifier: ^1.6.0 + version: 1.6.0 packages/nameguard-sdk: dependencies: @@ -519,6 +525,12 @@ importers: '@namehash/ens-webfont': specifier: workspace:* version: link:../ens-webfont + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) devDependencies: '@namekit/tsconfig': specifier: workspace:* @@ -538,12 +550,6 @@ importers: postcss: specifier: 8.4.38 version: 8.4.38 - react: - specifier: ^18.2.0 - version: 18.3.1 - react-dom: - specifier: ^18.2.0 - version: 18.3.1(react@18.3.1) tailwindcss: specifier: 3.4.4 version: 3.4.4 @@ -5306,7 +5312,7 @@ packages: '@storybook/instrumenter': 8.1.1 '@storybook/preview-api': 8.1.1 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.5 + '@testing-library/jest-dom': 6.4.5(vitest@1.6.0) '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) '@vitest/expect': 1.3.1 '@vitest/spy': 1.4.0 @@ -5319,7 +5325,7 @@ packages: - vitest dev: true - /@storybook/test@8.1.8: + /@storybook/test@8.1.8(vitest@1.6.0): resolution: {integrity: sha512-SuKQZ2VPBLRK7zgkK+BAzau0HHdYjcsHFbVFa3E4r5n29KRXziW0hOcyBqMRh3jVi9tWKswTmDAOaPfPCxQjHA==} dependencies: '@storybook/client-logger': 8.1.8 @@ -5327,7 +5333,7 @@ packages: '@storybook/instrumenter': 8.1.8 '@storybook/preview-api': 8.1.8 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.5 + '@testing-library/jest-dom': 6.4.5(vitest@1.6.0) '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) '@vitest/expect': 1.3.1 '@vitest/spy': 1.4.0 @@ -5433,7 +5439,7 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.4.5: + /@testing-library/jest-dom@6.4.5(vitest@1.6.0): resolution: {integrity: sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: @@ -5462,6 +5468,7 @@ packages: dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 + vitest: 1.6.0 dev: true /@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4): @@ -12863,6 +12870,27 @@ packages: - terser dev: true + /vite-node@1.6.0: + resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.1 + vite: 5.1.7(@types/node@20.12.12) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-node@1.6.0(@types/node@20.12.7): resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -13012,6 +13040,61 @@ packages: - terser dev: true + /vitest@1.6.0: + resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.10 + pathe: 1.1.2 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.1.7(@types/node@20.12.12) + vite-node: 1.6.0 + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vitest@1.6.0(@types/node@20.12.7): resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} engines: {node: ^18.0.0 || >=20.0.0}