From db46f7801ce05d09f1b20638f676f6db6f09c3bd Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 15 Oct 2024 18:49:09 +0100 Subject: [PATCH 1/8] feat(namekit-react): Identity --- .../stories/Namekit/Identity.stories.tsx | 59 +++++++ packages/namekit-react/package.json | 1 + .../namekit-react/src/components/Identity.tsx | 145 ++++++++++++++++++ packages/namekit-react/src/index.ts | 1 + pnpm-lock.yaml | 50 +++--- 5 files changed, 229 insertions(+), 27 deletions(-) create mode 100644 apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx create mode 100644 packages/namekit-react/src/components/Identity.tsx diff --git a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx new file mode 100644 index 000000000..562b7eb84 --- /dev/null +++ b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Identity } from "@namehash/namekit-react"; + +const meta: Meta = { + title: "Namekit/Identity", + component: Identity.Root, + argTypes: { + address: { control: "text" }, + network: { + control: { + type: "select", + options: ["mainnet", "sepolia"], + }, + }, + className: { control: "text" }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const IdentityCard: React.FC<{ + address: string; + network?: "mainnet" | "sepolia"; +}> = ({ address, network }) => ( + + + + + + +); + +export const Default: Story = { + args: { + address: "0x838aD0EAE54F99F1926dA7C3b6bFbF617389B4D9", + network: "mainnet", + className: "rounded-xl", + }, + render: (args) => , +}; + +export const MultipleCards: Story = { + render: () => ( + <> + + + + + + + + ), +}; diff --git a/packages/namekit-react/package.json b/packages/namekit-react/package.json index 1f5972738..4babffc8d 100644 --- a/packages/namekit-react/package.json +++ b/packages/namekit-react/package.json @@ -43,6 +43,7 @@ "@headlessui/react": "1.7.17", "@namehash/ens-utils": "workspace:*", "@namehash/ens-webfont": "workspace:*", + "@namehash/nameguard": "workspace:*", "classcat": "5.0.5" }, "devDependencies": { diff --git a/packages/namekit-react/src/components/Identity.tsx b/packages/namekit-react/src/components/Identity.tsx new file mode 100644 index 000000000..426b9bc0a --- /dev/null +++ b/packages/namekit-react/src/components/Identity.tsx @@ -0,0 +1,145 @@ +import React, { + createContext, + useContext, + useState, + useEffect, + ReactNode, +} from "react"; +import { + createClient, + Network, + type SecurePrimaryNameResult, +} from "@namehash/nameguard"; + +interface IdentityContextType extends SecurePrimaryNameResult { + address: string; +} + +const IdentityContext = createContext(null); + +const useIdentity = () => { + const context = useContext(IdentityContext); + + if (!context) { + throw new Error("useIdentity must be used within an IdentityProvider"); + } + + return context; +}; + +interface SubComponentProps { + className?: string; + children?: ReactNode; +} + +interface RootProps { + address: string; + network?: Network; + className?: string; + children: ReactNode; +} + +const Root = ({ + address, + network = "mainnet", + className, + children, + ...props +}: RootProps) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + + const nameguard = createClient({ network }); + + const result = await nameguard.getSecurePrimaryName(address, { + returnNameGuardReport: true, + }); + + setData({ ...result, address }); + } catch (err) { + setError( + err instanceof Error ? err.message : "An unknown error occurred", + ); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [address, network]); + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; + if (!data) return null; + + return ( + +
+ {children} +
+
+ ); +}; + +const Avatar = ({ className, children, ...props }: SubComponentProps) => { + const { display_name } = useIdentity(); + + return ( +
+ {display_name} + {children} +
+ ); +}; + +const Name = ({ className, ...props }: SubComponentProps) => { + const { display_name } = useIdentity(); + + return ( +
+ {display_name} +
+ ); +}; + +const Address = ({ className, ...props }: SubComponentProps) => { + const { address } = useIdentity(); + + return ( +
+ {address} +
+ ); +}; + +const NameGuardShield = ({ className, ...props }: SubComponentProps) => { + const { nameguard_report } = useIdentity(); + + return ( +
+
+ Rating: {nameguard_report?.rating} +
+
+ Risks: {nameguard_report?.risk_count} +
+
+ ); +}; + +export const Identity = { + Root, + Avatar, + Name, + Address, + NameGuardShield, +}; diff --git a/packages/namekit-react/src/index.ts b/packages/namekit-react/src/index.ts index 77e97e8d9..9eaeac848 100644 --- a/packages/namekit-react/src/index.ts +++ b/packages/namekit-react/src/index.ts @@ -13,3 +13,4 @@ export { ENSTextArea } from "./components/ENSTextArea"; export { Input } from "./components/Input"; export { ENSInput } from "./components/ENSInput"; export { Link } from "./components/Link"; +export { Identity } from "./components/Identity"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2276af63b..79f4d4d92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -568,6 +568,9 @@ importers: '@namehash/ens-webfont': specifier: workspace:* version: link:../ens-webfont + '@namehash/nameguard': + specifier: workspace:* + version: link:../nameguard-sdk classcat: specifier: 5.0.5 version: 5.0.5 @@ -3548,6 +3551,7 @@ packages: eslint@8.57.1: resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true esniff@1.1.3: @@ -7591,7 +7595,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -7762,7 +7766,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -9023,7 +9027,7 @@ snapshots: '@typescript-eslint/types': 8.8.1 '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.2) '@typescript-eslint/visitor-keys': 8.8.1 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) eslint: 8.57.1 optionalDependencies: typescript: 5.6.2 @@ -9049,7 +9053,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.6.2) '@typescript-eslint/utils': 8.8.0(eslint@8.57.1)(typescript@5.6.2) - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) ts-api-utils: 1.3.0(typescript@5.6.2) optionalDependencies: typescript: 5.6.2 @@ -9082,7 +9086,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.8.0 '@typescript-eslint/visitor-keys': 8.8.0 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.4 @@ -9097,7 +9101,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.8.1 '@typescript-eslint/visitor-keys': 8.8.1 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.4 @@ -9917,20 +9921,12 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.3.4: - dependencies: - ms: 2.1.2 - debug@4.3.4(supports-color@8.1.1): dependencies: ms: 2.1.2 optionalDependencies: supports-color: 8.1.1 - debug@4.3.7: - dependencies: - ms: 2.1.3 - debug@4.3.7(supports-color@8.1.1): dependencies: ms: 2.1.3 @@ -10332,7 +10328,7 @@ snapshots: eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.1) eslint-plugin-react: 7.34.1(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) @@ -10355,8 +10351,8 @@ snapshots: debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.16.1 eslint: 8.57.1 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -10369,10 +10365,10 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1): dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.16.1 eslint: 8.57.1 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.7.5 @@ -10384,7 +10380,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -10395,7 +10391,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1): + eslint-module-utils@2.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -10406,7 +10402,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -10416,7 +10412,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -10443,7 +10439,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@8.8.1(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -10526,7 +10522,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -13282,7 +13278,7 @@ snapshots: vite-node@2.1.2(@types/node@22.7.4): dependencies: cac: 6.7.14 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) pathe: 1.1.2 vite: 5.1.7(@types/node@22.7.4) transitivePeerDependencies: @@ -13347,7 +13343,7 @@ snapshots: '@vitest/spy': 2.1.2 '@vitest/utils': 2.1.2 chai: 5.1.1 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) magic-string: 0.30.11 pathe: 1.1.2 std-env: 3.7.0 From 450998f8039749cf5347e03d53a9e6a791af09d1 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 15 Oct 2024 19:03:09 +0100 Subject: [PATCH 2/8] wrap the client component --- .../storybook.namekit.io/stories/Namekit/Identity.stories.tsx | 2 +- packages/namekit-react/src/client.ts | 4 ++++ packages/namekit-react/src/index.ts | 1 - 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx index 562b7eb84..188b21323 100644 --- a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx +++ b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx @@ -1,6 +1,6 @@ import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import { Identity } from "@namehash/namekit-react"; +import { Identity } from "@namehash/namekit-react/client"; const meta: Meta = { title: "Namekit/Identity", diff --git a/packages/namekit-react/src/client.ts b/packages/namekit-react/src/client.ts index 6f68c586d..858797217 100644 --- a/packages/namekit-react/src/client.ts +++ b/packages/namekit-react/src/client.ts @@ -1,3 +1,5 @@ +"use client"; + import "@namehash/ens-webfont"; import "./styles.css"; @@ -12,3 +14,5 @@ export { CurrencySymbolSize, } from "./components/CurrencySymbol/CurrencySymbol"; export { TruncatedText } from "./components/TruncatedText"; + +export { Identity } from "./components/Identity"; diff --git a/packages/namekit-react/src/index.ts b/packages/namekit-react/src/index.ts index 9eaeac848..77e97e8d9 100644 --- a/packages/namekit-react/src/index.ts +++ b/packages/namekit-react/src/index.ts @@ -13,4 +13,3 @@ export { ENSTextArea } from "./components/ENSTextArea"; export { Input } from "./components/Input"; export { ENSInput } from "./components/ENSInput"; export { Link } from "./components/Link"; -export { Identity } from "./components/Identity"; From 1d92fb193382694127da7ee62b002d53581f234c Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 22 Oct 2024 20:02:30 +0100 Subject: [PATCH 3/8] feat: handle avatar and nameguard report state --- .../stories/Namekit/Identity.stories.tsx | 9 +- .../namekit-react/src/components/Identity.tsx | 118 +++++++++++++----- 2 files changed, 91 insertions(+), 36 deletions(-) diff --git a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx index 188b21323..78e863c0e 100644 --- a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx +++ b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx @@ -46,14 +46,11 @@ export const MultipleCards: Story = { render: () => ( <> - - - + - ), }; diff --git a/packages/namekit-react/src/components/Identity.tsx b/packages/namekit-react/src/components/Identity.tsx index 426b9bc0a..d1d40c1af 100644 --- a/packages/namekit-react/src/components/Identity.tsx +++ b/packages/namekit-react/src/components/Identity.tsx @@ -11,8 +11,13 @@ import { type SecurePrimaryNameResult, } from "@namehash/nameguard"; -interface IdentityContextType extends SecurePrimaryNameResult { +interface IdentityContextType { + network: string; address: string; + returnNameGuardReport: boolean; + loadingState: "loading" | "error" | "success"; + error?: string; + identityData?: SecurePrimaryNameResult; } const IdentityContext = createContext(null); @@ -37,6 +42,7 @@ interface RootProps { network?: Network; className?: string; children: ReactNode; + returnNameGuardReport?: boolean; } const Root = ({ @@ -44,39 +50,44 @@ const Root = ({ network = "mainnet", className, children, + returnNameGuardReport = false, ...props }: RootProps) => { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [data, setData] = useState({ + address, + network, + returnNameGuardReport, + loadingState: "loading", + }); useEffect(() => { const fetchData = async () => { try { - setLoading(true); + setData((prev) => ({ ...prev, loadingState: "loading" })); const nameguard = createClient({ network }); const result = await nameguard.getSecurePrimaryName(address, { - returnNameGuardReport: true, + returnNameGuardReport, }); - setData({ ...result, address }); + setData((prev) => ({ + ...prev, + loadingState: "success", + identityData: result, + })); } catch (err) { - setError( - err instanceof Error ? err.message : "An unknown error occurred", - ); - } finally { - setLoading(false); + setData((prev) => ({ + ...prev, + loadingState: "error", + error: + err instanceof Error ? err.message : "An unknown error occurred", + })); } }; fetchData(); - }, [address, network]); - - if (loading) return
Loading...
; - if (error) return
Error: {error}
; - if (!data) return null; + }, [address, network, returnNameGuardReport]); return ( @@ -87,26 +98,65 @@ const Root = ({ ); }; -const Avatar = ({ className, children, ...props }: SubComponentProps) => { - const { display_name } = useIdentity(); +const Avatar = ({ className, ...props }: SubComponentProps) => { + const { identityData, loadingState, network } = useIdentity(); + + if (loadingState === "loading") { + return ( +
+
+
+ ); + } + + if (loadingState === "error" || !identityData?.display_name) { + return ( +
+
+ ! +
+
+ ); + } + + const avatarUrl = `https://metadata.ens.domains/${network}/avatar/${identityData.display_name}`; return ( -
+
{display_name} { + e.currentTarget.src = "path/to/fallback/image.png"; + }} /> - {children}
); }; const Name = ({ className, ...props }: SubComponentProps) => { - const { display_name } = useIdentity(); + const { identityData, loadingState, address } = useIdentity(); + + if (loadingState === "loading") { + return ( +
+ ); + } + + const displayName = + identityData?.display_name || + address.slice(0, 6) + "..." + address.slice(-4); return ( -
- {display_name} +
+ {displayName}
); }; @@ -115,22 +165,30 @@ const Address = ({ className, ...props }: SubComponentProps) => { const { address } = useIdentity(); return ( -
+
{address}
); }; const NameGuardShield = ({ className, ...props }: SubComponentProps) => { - const { nameguard_report } = useIdentity(); + const { identityData, returnNameGuardReport, loadingState } = useIdentity(); + + if ( + !returnNameGuardReport || + loadingState !== "success" || + !identityData?.nameguard_report + ) { + return null; + } return (
- Rating: {nameguard_report?.rating} + Rating: {identityData.nameguard_report.rating}
- Risks: {nameguard_report?.risk_count} + Risks: {identityData.nameguard_report.risk_count}
); From 105a4871004221ca4714ef27f7da624f8f69d760 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 22 Oct 2024 20:21:38 +0100 Subject: [PATCH 4/8] feat: add ens profile link --- .../stories/Namekit/Identity.stories.tsx | 10 ++++- .../namekit-react/src/components/Identity.tsx | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx index 78e863c0e..f4126074e 100644 --- a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx +++ b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx @@ -24,12 +24,18 @@ type Story = StoryObj; const IdentityCard: React.FC<{ address: string; network?: "mainnet" | "sepolia"; -}> = ({ address, network }) => ( - + returnNameGuardReport?: boolean; +}> = ({ address, network, returnNameGuardReport }) => ( + + ); diff --git a/packages/namekit-react/src/components/Identity.tsx b/packages/namekit-react/src/components/Identity.tsx index d1d40c1af..369d626ac 100644 --- a/packages/namekit-react/src/components/Identity.tsx +++ b/packages/namekit-react/src/components/Identity.tsx @@ -194,10 +194,49 @@ const NameGuardShield = ({ className, ...props }: SubComponentProps) => { ); }; +const ENSLogo = () => ( + + + + + + + + +); + +const ENSProfileLink = ({ className, ...props }: SubComponentProps) => { + const { identityData, loadingState } = useIdentity(); + + if (loadingState !== "success" || !identityData?.display_name) { + return null; + } + + return ( + + + ENS Profile + + ); +}; + export const Identity = { Root, Avatar, Name, Address, NameGuardShield, + ENSProfileLink, }; From ef9dbc52490688c0a8987e1b4fe7430f1fec90ed Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Wed, 23 Oct 2024 12:22:14 +0100 Subject: [PATCH 5/8] feat: follower count --- .../stories/Namekit/Identity.stories.tsx | 1 + .../namekit-react/src/components/Identity.tsx | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx index f4126074e..84ab2bc0d 100644 --- a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx +++ b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx @@ -36,6 +36,7 @@ const IdentityCard: React.FC<{ + ); diff --git a/packages/namekit-react/src/components/Identity.tsx b/packages/namekit-react/src/components/Identity.tsx index 369d626ac..7b7b758d1 100644 --- a/packages/namekit-react/src/components/Identity.tsx +++ b/packages/namekit-react/src/components/Identity.tsx @@ -18,6 +18,7 @@ interface IdentityContextType { loadingState: "loading" | "error" | "success"; error?: string; identityData?: SecurePrimaryNameResult; + followersCount?: string; } const IdentityContext = createContext(null); @@ -86,7 +87,26 @@ const Root = ({ } }; + const fetchFollowersData = async () => { + try { + const response = await fetch( + `https://api.ethfollow.xyz/api/v1/users/${address}/stats`, + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const result = await response.json(); + setData((prev) => ({ + ...prev, + followersCount: result.followers_count, + })); + } catch (err) { + console.error("Error fetching followers data:", err); + } + }; + fetchData(); + fetchFollowersData(); }, [address, network, returnNameGuardReport]); return ( @@ -194,6 +214,35 @@ const NameGuardShield = ({ className, ...props }: SubComponentProps) => { ); }; +const Followers = ({ className, ...props }: SubComponentProps) => { + const { followersCount, loadingState } = useIdentity(); + + if (loadingState === "loading") { + return ( +
+ Loading followers... +
+ ); + } + + if (followersCount === undefined) { + return ( +
+ Fetching followers... +
+ ); + } + + return ( +
+ {followersCount} followers +
+ ); +}; + const ENSLogo = () => ( Date: Mon, 28 Oct 2024 17:51:55 +0000 Subject: [PATCH 6/8] rename ethfollow func --- packages/namekit-react/src/components/Identity.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/namekit-react/src/components/Identity.tsx b/packages/namekit-react/src/components/Identity.tsx index 7b7b758d1..f517aeafb 100644 --- a/packages/namekit-react/src/components/Identity.tsx +++ b/packages/namekit-react/src/components/Identity.tsx @@ -87,7 +87,7 @@ const Root = ({ } }; - const fetchFollowersData = async () => { + const fetchEthFollowUserStats = async () => { try { const response = await fetch( `https://api.ethfollow.xyz/api/v1/users/${address}/stats`, @@ -106,7 +106,7 @@ const Root = ({ }; fetchData(); - fetchFollowersData(); + fetchEthFollowUserStats(); }, [address, network, returnNameGuardReport]); return ( From ec2cb9c2ed636f67c1802caa65602a9846830063 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Sun, 3 Nov 2024 12:58:07 +0000 Subject: [PATCH 7/8] feat: apply feedback from lightwalker --- .../stories/Namekit/Identity.stories.tsx | 138 ++++++++++++++++-- packages/namekit-react/src/client.ts | 2 +- .../namekit-react/src/components/Identity.tsx | 93 +++++++----- 3 files changed, 187 insertions(+), 46 deletions(-) diff --git a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx index 84ab2bc0d..968e012dd 100644 --- a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx +++ b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx @@ -1,6 +1,6 @@ import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import { Identity } from "@namehash/namekit-react/client"; +import { Identity, NameKitProvider } from "@namehash/namekit-react/client"; const meta: Meta = { title: "Namekit/Identity", @@ -21,7 +21,7 @@ export default meta; type Story = StoryObj; -const IdentityCard: React.FC<{ +const DefaultIdentityCard: React.FC<{ address: string; network?: "mainnet" | "sepolia"; returnNameGuardReport?: boolean; @@ -35,29 +35,147 @@ const IdentityCard: React.FC<{ - + +
+ + View on ENS App +
+ ); +const customAppConfig = { + profileLinks: { + getProfileURL: (address: string) => `/profiles/${address}`, + getProfileLink: (address: string, children: React.ReactNode) => ( + {children} + ), + }, +}; + +const CustomAppIdentityCard: React.FC<{ address: string }> = ({ address }) => ( + + + + + + + + + +); + +const modalConfig = { + profileLinks: { + getProfileURL: (address: string) => `#${address}`, + getProfileLink: (address: string, children: React.ReactNode) => ( + + ), + }, +}; + +const ModalIdentityCard: React.FC<{ address: string }> = ({ address }) => ( + + + + + + <> + Open Profile Modal + + + + + +); + export const Default: Story = { args: { address: "0x838aD0EAE54F99F1926dA7C3b6bFbF617389B4D9", network: "mainnet", className: "rounded-xl", }, - render: (args) => , + render: (args) => , }; export const MultipleCards: Story = { render: () => ( <> - - - + + + ), }; + +export const ProfileLinkVariants: Story = { + render: () => { + const address = "0x838aD0EAE54F99F1926dA7C3b6bFbF617389B4D9"; + + return ( +
+
+

Default ENS App Link

+ + +
+ + View on ENS App +
+
+
+
+ +
+

Custom App Link

+ + + + + + + +
+ +
+

Modal Trigger

+ + + + <> + Open Profile Modal + + + + + +
+
+ ); + }, +}; + +const ENSLogo = () => ( + + + + + + + + +); diff --git a/packages/namekit-react/src/client.ts b/packages/namekit-react/src/client.ts index 858797217..c7c38f1fc 100644 --- a/packages/namekit-react/src/client.ts +++ b/packages/namekit-react/src/client.ts @@ -15,4 +15,4 @@ export { } from "./components/CurrencySymbol/CurrencySymbol"; export { TruncatedText } from "./components/TruncatedText"; -export { Identity } from "./components/Identity"; +export { Identity, NameKitProvider } from "./components/Identity"; diff --git a/packages/namekit-react/src/components/Identity.tsx b/packages/namekit-react/src/components/Identity.tsx index f517aeafb..f800ed4dd 100644 --- a/packages/namekit-react/src/components/Identity.tsx +++ b/packages/namekit-react/src/components/Identity.tsx @@ -10,9 +10,34 @@ import { Network, type SecurePrimaryNameResult, } from "@namehash/nameguard"; +import cc from "classcat"; + +export interface ProfileLinkConfig { + getProfileURL: (address: string) => string; + getProfileLink: (address: string, children: ReactNode) => JSX.Element; +} + +interface NameKitConfig { + profileLinks?: ProfileLinkConfig; +} + +const NameKitConfigContext = createContext({}); + +export const NameKitProvider: React.FC<{ + children: React.ReactNode; + config: NameKitConfig; +}> = ({ children, config }) => { + return ( + + {children} + + ); +}; + +export const useNameKitConfig = () => useContext(NameKitConfigContext); interface IdentityContextType { - network: string; + network: Network; address: string; returnNameGuardReport: boolean; loadingState: "loading" | "error" | "success"; @@ -243,42 +268,40 @@ const Followers = ({ className, ...props }: SubComponentProps) => { ); }; -const ENSLogo = () => ( - - - - - - - - -); - -const ENSProfileLink = ({ className, ...props }: SubComponentProps) => { - const { identityData, loadingState } = useIdentity(); - - if (loadingState !== "success" || !identityData?.display_name) { +interface ProfileLinkProps extends SubComponentProps { + config?: ProfileLinkConfig; +} + +const ProfileLink = ({ + className, + children, + config: instanceConfig, + ...props +}: ProfileLinkProps) => { + const { loadingState, address } = useIdentity(); + const globalConfig = useNameKitConfig(); + + if (loadingState !== "success") { return null; } - return ( - - - ENS Profile - - ); + const config = instanceConfig || + globalConfig.profileLinks || { + getProfileURL: (address) => `https://app.ens.domains/${address}`, + getProfileLink: (address, children) => ( + + {children} + + ), + }; + + return config.getProfileLink(address, children); }; export const Identity = { @@ -287,6 +310,6 @@ export const Identity = { Name, Address, NameGuardShield, - ENSProfileLink, + ProfileLink, Followers, }; From 752ac3a2232cb582ed86fab5c5e9045f8aac18b2 Mon Sep 17 00:00:00 2001 From: Jamie Barton Date: Tue, 5 Nov 2024 12:47:50 +0000 Subject: [PATCH 8/8] updates to profile link --- .../stories/Namekit/Identity.stories.tsx | 118 ++++++++--------- packages/namekit-react/src/client.ts | 6 +- .../namekit-react/src/components/Identity.tsx | 119 +++++++++++++----- 3 files changed, 151 insertions(+), 92 deletions(-) diff --git a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx index 968e012dd..4d886724e 100644 --- a/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx +++ b/apps/storybook.namekit.io/stories/Namekit/Identity.stories.tsx @@ -1,6 +1,10 @@ -import React from "react"; +import React, { useRef } from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import { Identity, NameKitProvider } from "@namehash/namekit-react/client"; +import { + Identity, + NameKitProvider, + ProfileLinkGenerator, +} from "@namehash/namekit-react/client"; const meta: Meta = { title: "Namekit/Identity", @@ -21,6 +25,15 @@ export default meta; type Story = StoryObj; +const TwitterProfileLink = new ProfileLinkGenerator( + "Twitter", + "https://twitter.com/", +); +const GitHubProfileLink = new ProfileLinkGenerator( + "GitHub", + "https://github.com/", +); + const DefaultIdentityCard: React.FC<{ address: string; network?: "mainnet" | "sepolia"; @@ -45,17 +58,10 @@ const DefaultIdentityCard: React.FC<{ ); -const customAppConfig = { - profileLinks: { - getProfileURL: (address: string) => `/profiles/${address}`, - getProfileLink: (address: string, children: React.ReactNode) => ( - {children} - ), - }, -}; - const CustomAppIdentityCard: React.FC<{ address: string }> = ({ address }) => ( - + @@ -66,36 +72,35 @@ const CustomAppIdentityCard: React.FC<{ address: string }> = ({ address }) => ( ); -const modalConfig = { - profileLinks: { - getProfileURL: (address: string) => `#${address}`, - getProfileLink: (address: string, children: React.ReactNode) => ( - - ), - }, -}; +const ModalIdentityCard: React.FC<{ address: string }> = ({ address }) => { + const dialogRef = useRef(null); -const ModalIdentityCard: React.FC<{ address: string }> = ({ address }) => ( - - - - - - <> - Open Profile Modal - - - - - -); + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + dialogRef.current?.showModal(); + }; + + return ( + + + + + + <> + Open Profile Modal + + + + + + Hello {address} + + + + ); +}; export const Default: Story = { args: { @@ -110,8 +115,8 @@ export const MultipleCards: Story = { render: () => ( <> - - + + ), }; @@ -121,10 +126,12 @@ export const ProfileLinkVariants: Story = { const address = "0x838aD0EAE54F99F1926dA7C3b6bFbF617389B4D9"; return ( -
+

Default ENS App Link

+ +
@@ -136,27 +143,20 @@ export const ProfileLinkVariants: Story = {

Custom App Link

- + - - - + + +
-

Modal Trigger

- - - - <> - Open Profile Modal - - - - - +

Modal Link

+
); diff --git a/packages/namekit-react/src/client.ts b/packages/namekit-react/src/client.ts index c7c38f1fc..087449af6 100644 --- a/packages/namekit-react/src/client.ts +++ b/packages/namekit-react/src/client.ts @@ -15,4 +15,8 @@ export { } from "./components/CurrencySymbol/CurrencySymbol"; export { TruncatedText } from "./components/TruncatedText"; -export { Identity, NameKitProvider } from "./components/Identity"; +export { + Identity, + NameKitProvider, + ProfileLinkGenerator, +} from "./components/Identity"; diff --git a/packages/namekit-react/src/components/Identity.tsx b/packages/namekit-react/src/components/Identity.tsx index f800ed4dd..256090bea 100644 --- a/packages/namekit-react/src/components/Identity.tsx +++ b/packages/namekit-react/src/components/Identity.tsx @@ -12,21 +12,46 @@ import { } from "@namehash/nameguard"; import cc from "classcat"; -export interface ProfileLinkConfig { - getProfileURL: (address: string) => string; - getProfileLink: (address: string, children: ReactNode) => JSX.Element; +export class ProfileLinkGenerator { + private baseURL: string; + private name: string; + + constructor(name: string, baseURL: string) { + this.name = name; + this.baseURL = baseURL; + } + + getName(): string { + return this.name; + } + + getProfileURL(address: string): string { + return `${this.baseURL}${address}`; + } } +export const ENSProfileLink = new ProfileLinkGenerator( + "ENS", + "https://app.ens.domains/", +); + +const DEFAULT_PROFILE_LINKS = [ENSProfileLink]; + interface NameKitConfig { - profileLinks?: ProfileLinkConfig; + profileLinks?: ProfileLinkGenerator[]; } const NameKitConfigContext = createContext({}); -export const NameKitProvider: React.FC<{ +interface NameKitProviderProps { children: React.ReactNode; config: NameKitConfig; -}> = ({ children, config }) => { +} + +export const NameKitProvider: React.FC = ({ + children, + config, +}) => { return ( {children} @@ -268,40 +293,69 @@ const Followers = ({ className, ...props }: SubComponentProps) => { ); }; -interface ProfileLinkProps extends SubComponentProps { - config?: ProfileLinkConfig; +interface ProfileLinkProps { + config?: ProfileLinkGenerator; + className?: string; + children?: React.ReactNode; + onClick?: (e: React.MouseEvent) => void; } -const ProfileLink = ({ - className, +const ProfileLink: React.FC = ({ + config, children, - config: instanceConfig, - ...props -}: ProfileLinkProps) => { - const { loadingState, address } = useIdentity(); - const globalConfig = useNameKitConfig(); + onClick, +}) => { + const identity = useIdentity(); + const nameKitConfig = useNameKitConfig(); - if (loadingState !== "success") { + const linkConfig = + config || nameKitConfig.profileLinks?.[0] || DEFAULT_PROFILE_LINKS[0]; + + if (!identity) { + console.warn("ProfileLink used outside of Identity context"); return null; } - const config = instanceConfig || - globalConfig.profileLinks || { - getProfileURL: (address) => `https://app.ens.domains/${address}`, - getProfileLink: (address, children) => ( - - {children} - - ), - }; + const url = linkConfig.getProfileURL(identity.address); - return config.getProfileLink(address, children); + return ( + + {children || linkConfig.getName()} + + ); +}; + +interface ProfileLinksProps { + configs?: ProfileLinkGenerator[]; + className?: string; +} + +const ProfileLinks: React.FC = ({ configs, className }) => { + const identity = useIdentity(); + const { profileLinks: globalConfigs } = useNameKitConfig(); + + const linksToRender = configs || globalConfigs || DEFAULT_PROFILE_LINKS; + + if (!identity) { + console.warn("ProfileLinks used outside of Identity context"); + return null; + } + + return ( +
+ {linksToRender.map((config) => ( + + {config.getName()} + + ))} +
+ ); }; export const Identity = { @@ -311,5 +365,6 @@ export const Identity = { Address, NameGuardShield, ProfileLink, + ProfileLinks, Followers, };