From e53d74edd184449a8ee0232324e726407359774b Mon Sep 17 00:00:00 2001 From: codytseng Date: Fri, 4 Jul 2025 21:38:32 +0800 Subject: [PATCH] feat: improve user npub QR code card --- package-lock.json | 21 +++--- package.json | 2 +- .../AccountManager/NostrConnectionLogin.tsx | 17 ++--- src/components/NpubQrCode/index.tsx | 57 +++++++++++++++ src/components/QrCode/index.tsx | 70 +++++++++++++++++++ src/components/QrCodePopover/index.tsx | 52 -------------- src/components/UserAvatar/index.tsx | 3 +- src/pages/primary/MePage/index.tsx | 4 +- src/pages/secondary/ProfilePage/index.tsx | 4 +- src/providers/ThemeProvider.tsx | 31 ++++---- 10 files changed, 169 insertions(+), 92 deletions(-) create mode 100644 src/components/NpubQrCode/index.tsx create mode 100644 src/components/QrCode/index.tsx delete mode 100644 src/components/QrCodePopover/index.tsx diff --git a/package-lock.json b/package-lock.json index 804b5f11..496b9457 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "nostr-tools": "^2.13.0", "nstart-modal": "^2.0.0", "path-to-regexp": "^8.2.0", - "qrcode.react": "^4.2.0", + "qr-code-styling": "^1.9.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^15.2.0", @@ -8300,19 +8300,22 @@ "node": ">=6" } }, + "node_modules/qr-code-styling": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz", + "integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==", + "dependencies": { + "qrcode-generator": "^1.4.4" + }, + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/qrcode-generator": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz", "integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==" }, - "node_modules/qrcode.react": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", - "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index e062d62c..e2276cad 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "nostr-tools": "^2.13.0", "nstart-modal": "^2.0.0", "path-to-regexp": "^8.2.0", - "qrcode.react": "^4.2.0", + "qr-code-styling": "^1.9.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-i18next": "^15.2.0", diff --git a/src/components/AccountManager/NostrConnectionLogin.tsx b/src/components/AccountManager/NostrConnectionLogin.tsx index a1f07f6f..c049e961 100644 --- a/src/components/AccountManager/NostrConnectionLogin.tsx +++ b/src/components/AccountManager/NostrConnectionLogin.tsx @@ -1,13 +1,13 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { useNostr } from '@/providers/NostrProvider' -import { Loader, Copy, Check } from 'lucide-react' -import { createNostrConnectURI, NostrConnectParams } from '@/providers/NostrProvider/nip46' import { DEFAULT_NOSTRCONNECT_RELAY } from '@/constants' +import { useNostr } from '@/providers/NostrProvider' +import { createNostrConnectURI, NostrConnectParams } from '@/providers/NostrProvider/nip46' +import { Check, Copy, Loader } from 'lucide-react' import { generateSecretKey, getPublicKey } from 'nostr-tools' -import { QRCodeSVG } from 'qrcode.react' -import { useState, useEffect, useRef, useLayoutEffect } from 'react' +import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import QrCode from '../QrCode' export default function NostrConnectLogin({ back, @@ -107,12 +107,7 @@ export default function NostrConnectLogin({ <>
- + {nostrConnectionErrMsg && (
{nostrConnectionErrMsg}
diff --git a/src/components/NpubQrCode/index.tsx b/src/components/NpubQrCode/index.tsx new file mode 100644 index 00000000..f55c97d2 --- /dev/null +++ b/src/components/NpubQrCode/index.tsx @@ -0,0 +1,57 @@ +import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog' +import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer' +import { useScreenSize } from '@/providers/ScreenSizeProvider' +import { QrCodeIcon } from 'lucide-react' +import { nip19 } from 'nostr-tools' +import { useMemo } from 'react' +import Nip05 from '../Nip05' +import PubkeyCopy from '../PubkeyCopy' +import QrCode from '../QrCode' +import UserAvatar from '../UserAvatar' +import Username from '../Username' + +export default function NpubQrCode({ pubkey }: { pubkey: string }) { + const { isSmallScreen } = useScreenSize() + const npub = useMemo(() => (pubkey ? nip19.npubEncode(pubkey) : ''), [pubkey]) + if (!npub) return null + + const trigger = ( +
+ +
+ ) + + const content = ( +
+
+ +
+ + +
+
+ +
+ +
+
+ ) + + if (isSmallScreen) { + return ( + + {trigger} + {content} + + ) + } + + return ( + + {trigger} + e.preventDefault()}> + {content} + + + ) +} diff --git a/src/components/QrCode/index.tsx b/src/components/QrCode/index.tsx new file mode 100644 index 00000000..c8a87a21 --- /dev/null +++ b/src/components/QrCode/index.tsx @@ -0,0 +1,70 @@ +import { useTheme } from '@/providers/ThemeProvider' +import QRCodeStyling from 'qr-code-styling' +import { useEffect, useRef, useState } from 'react' + +export default function QrCode({ value, size = 180 }: { value: string; size?: number }) { + const { theme } = useTheme() + const ref = useRef(null) + const [foregroundColor, setForegroundColor] = useState() + const [backgroundColor, setBackgroundColor] = useState() + + useEffect(() => { + setTimeout(() => { + const fgColor = `hsl(${getColor('foreground')})` + const bgColor = `hsl(${getColor('background')})` + setForegroundColor(fgColor) + setBackgroundColor(bgColor) + }, 0) + }, [theme]) + + useEffect(() => { + setTimeout(() => { + const pixelRatio = window.devicePixelRatio || 2 + + const qrCode = new QRCodeStyling({ + width: size * pixelRatio, + height: size * pixelRatio, + data: value, + dotsOptions: { + type: 'dots', + color: foregroundColor + }, + cornersDotOptions: { + type: 'extra-rounded', + color: foregroundColor + }, + cornersSquareOptions: { + type: 'extra-rounded', + color: foregroundColor + }, + backgroundOptions: { + color: backgroundColor + } + }) + + if (ref.current) { + ref.current.innerHTML = '' + qrCode.append(ref.current) + const canvas = ref.current.querySelector('canvas') + if (canvas) { + canvas.style.width = `${size}px` + canvas.style.height = `${size}px` + canvas.style.maxWidth = '100%' + canvas.style.height = 'auto' + } + } + }, 0) + + return () => { + if (ref.current) ref.current.innerHTML = '' + } + }, [value, size, foregroundColor, backgroundColor]) + + return
+} + +function getColor(name: string) { + if (typeof window !== 'undefined') { + return getComputedStyle(document.documentElement).getPropertyValue(`--${name}`).trim() + } +} diff --git a/src/components/QrCodePopover/index.tsx b/src/components/QrCodePopover/index.tsx deleted file mode 100644 index a3886935..00000000 --- a/src/components/QrCodePopover/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { useScreenSize } from '@/providers/ScreenSizeProvider' -import { QrCode } from 'lucide-react' -import { nip19 } from 'nostr-tools' -import { QRCodeSVG } from 'qrcode.react' -import { useMemo } from 'react' - -export default function QrCodePopover({ pubkey }: { pubkey: string }) { - const { isSmallScreen } = useScreenSize() - const npub = useMemo(() => (pubkey ? nip19.npubEncode(pubkey) : ''), [pubkey]) - if (!npub) return null - - if (isSmallScreen) { - return ( - - -
- -
-
- -
- -
-
-
- ) - } - - return ( - - -
- -
-
- - - -
- ) -} diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx index ad9c4f0e..a8658f45 100644 --- a/src/components/UserAvatar/index.tsx +++ b/src/components/UserAvatar/index.tsx @@ -12,6 +12,7 @@ import { useMemo } from 'react' const UserAvatarSizeCnMap = { large: 'w-24 h-24', big: 'w-16 h-16', + semiBig: 'w-12 h-12', normal: 'w-10 h-10', medium: 'w-8 h-8', small: 'w-7 h-7', @@ -26,7 +27,7 @@ export default function UserAvatar({ }: { userId: string className?: string - size?: 'large' | 'big' | 'normal' | 'medium' | 'small' | 'xSmall' | 'tiny' + size?: 'large' | 'big' | 'semiBig' | 'normal' | 'medium' | 'small' | 'xSmall' | 'tiny' }) { const { profile } = useFetchProfile(userId) const defaultAvatar = useMemo( diff --git a/src/pages/primary/MePage/index.tsx b/src/pages/primary/MePage/index.tsx index 9f6cbb39..1a0ab01f 100644 --- a/src/pages/primary/MePage/index.tsx +++ b/src/pages/primary/MePage/index.tsx @@ -2,7 +2,7 @@ import AccountManager from '@/components/AccountManager' import LoginDialog from '@/components/LoginDialog' import LogoutDialog from '@/components/LogoutDialog' import PubkeyCopy from '@/components/PubkeyCopy' -import QrCodePopover from '@/components/QrCodePopover' +import NpubQrCode from '@/components/NpubQrCode' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import { SimpleUserAvatar } from '@/components/UserAvatar' @@ -53,7 +53,7 @@ const MePage = forwardRef((_, ref) => { />
- +
diff --git a/src/pages/secondary/ProfilePage/index.tsx b/src/pages/secondary/ProfilePage/index.tsx index 76ea8e4b..4bc35f9f 100644 --- a/src/pages/secondary/ProfilePage/index.tsx +++ b/src/pages/secondary/ProfilePage/index.tsx @@ -7,7 +7,7 @@ import ProfileBanner from '@/components/ProfileBanner' import ProfileOptions from '@/components/ProfileOptions' import ProfileZapButton from '@/components/ProfileZapButton' import PubkeyCopy from '@/components/PubkeyCopy' -import QrCodePopover from '@/components/QrCodePopover' +import NpubQrCode from '@/components/NpubQrCode' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' @@ -157,7 +157,7 @@ const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number }, )}
- +
Promise } @@ -62,21 +63,23 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) { updateTheme() }, [theme]) - const value = { - themeSetting: themeSetting, - setThemeSetting: async (themeSetting: TThemeSetting) => { - storage.setThemeSetting(themeSetting) - setThemeSetting(themeSetting) - if (themeSetting === 'system') { - setTheme(getSystemTheme()) - return - } - setTheme(themeSetting) - } - } - return ( - + { + storage.setThemeSetting(themeSetting) + setThemeSetting(themeSetting) + if (themeSetting === 'system') { + setTheme(getSystemTheme()) + return + } + setTheme(themeSetting) + } + }} + > {children} )