-
Notifications
You must be signed in to change notification settings - Fork 495
feat: add localStorage nickname mapping for wallet addresses #743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
cc08dc3
d5334e7
f0638cf
169d30e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export { getNickname, removeNickname, setNickname } from './lib/nicknames'; | ||
| export { useNickname } from './model/use-nickname'; | ||
| export { EditIcon } from './ui/EditIcon'; | ||
| export { NicknameEditor } from './ui/NicknameEditor'; | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| /** | ||
| * Tests for nickname localStorage utility functions | ||
| */ | ||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { getNickname, removeNickname, setNickname } from '../nicknames'; | ||
|
|
||
| describe('nicknames', () => { | ||
| // Clear localStorage before each test for isolation | ||
| beforeEach(() => { | ||
| localStorage.clear(); | ||
| }); | ||
|
|
||
| describe('getNickname', () => { | ||
| it('returns null when no nickname exists', () => { | ||
| const result = getNickname('TestAddress123'); | ||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it('returns the nickname when it exists', () => { | ||
| // Setup: add nickname to localStorage | ||
| localStorage.setItem( | ||
| 'solana-explorer-nicknames', | ||
| JSON.stringify({ | ||
| TestAddress123: 'My Wallet', | ||
| }) | ||
| ); | ||
|
|
||
| const result = getNickname('TestAddress123'); | ||
| expect(result).toBe('My Wallet'); | ||
| }); | ||
|
|
||
| it('handles invalid JSON gracefully', () => { | ||
| // Setup: corrupt localStorage data | ||
| localStorage.setItem('solana-explorer-nicknames', 'invalid-json'); | ||
|
|
||
| const result = getNickname('TestAddress123'); | ||
| expect(result).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('setNickname', () => { | ||
| it('saves a nickname to localStorage', () => { | ||
| setNickname('TestAddress123', 'My Wallet'); | ||
|
|
||
| // Verify it was saved correctly | ||
| const stored = JSON.parse(localStorage.getItem('solana-explorer-nicknames') || '{}'); | ||
| expect(stored['TestAddress123']).toBe('My Wallet'); | ||
| }); | ||
|
|
||
| it('trims whitespace from nicknames', () => { | ||
| setNickname('TestAddress123', ' My Wallet '); | ||
|
|
||
| const stored = JSON.parse(localStorage.getItem('solana-explorer-nicknames') || '{}'); | ||
| expect(stored['TestAddress123']).toBe('My Wallet'); | ||
| }); | ||
|
|
||
| it('removes nickname when empty string provided', () => { | ||
| // Setup: save a nickname | ||
| setNickname('TestAddress123', 'My Wallet'); | ||
|
|
||
| // Update with empty/whitespace-only string | ||
| setNickname('TestAddress123', ' '); | ||
|
|
||
| const stored = JSON.parse(localStorage.getItem('solana-explorer-nicknames') || '{}'); | ||
| expect(stored['TestAddress123']).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('preserves other nicknames', () => { | ||
| setNickname('Address1', 'Wallet 1'); | ||
| setNickname('Address2', 'Wallet 2'); | ||
|
|
||
| const stored = JSON.parse(localStorage.getItem('solana-explorer-nicknames') || '{}'); | ||
| expect(stored['Address1']).toBe('Wallet 1'); | ||
| expect(stored['Address2']).toBe('Wallet 2'); | ||
| }); | ||
|
|
||
| it('dispatches nicknameUpdated event', () => { | ||
| const listener = vi.fn(); | ||
| window.addEventListener('nicknameUpdated', listener); | ||
|
|
||
| setNickname('TestAddress123', 'My Wallet'); | ||
|
|
||
| // Verify event was dispatched with correct address | ||
| expect(listener).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| detail: { address: 'TestAddress123' }, | ||
| }) | ||
| ); | ||
|
|
||
| window.removeEventListener('nicknameUpdated', listener); | ||
| }); | ||
| }); | ||
|
|
||
| describe('removeNickname', () => { | ||
| it('removes a nickname from localStorage', () => { | ||
| // Setup: save a nickname first | ||
| setNickname('TestAddress123', 'My Wallet'); | ||
|
|
||
| removeNickname('TestAddress123'); | ||
|
|
||
| const stored = JSON.parse(localStorage.getItem('solana-explorer-nicknames') || '{}'); | ||
| expect(stored['TestAddress123']).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('preserves other nicknames when removing one', () => { | ||
| // Setup: save multiple nicknames | ||
| setNickname('Address1', 'Wallet 1'); | ||
| setNickname('Address2', 'Wallet 2'); | ||
|
|
||
| removeNickname('Address1'); | ||
|
|
||
| const stored = JSON.parse(localStorage.getItem('solana-explorer-nicknames') || '{}'); | ||
| expect(stored['Address1']).toBeUndefined(); | ||
| expect(stored['Address2']).toBe('Wallet 2'); | ||
| }); | ||
|
|
||
| it('dispatches nicknameUpdated event', () => { | ||
| const listener = vi.fn(); | ||
| window.addEventListener('nicknameUpdated', listener); | ||
|
|
||
| removeNickname('TestAddress123'); | ||
|
|
||
| // Verify event was dispatched with correct address | ||
| expect(listener).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| detail: { address: 'TestAddress123' }, | ||
| }) | ||
| ); | ||
|
|
||
| window.removeEventListener('nicknameUpdated', listener); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| 'use client'; | ||
|
|
||
| /** | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that the 'use client' directive should be added here.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added 'use client' directive to |
||
| * Utility functions for managing wallet address nicknames in localStorage | ||
| */ | ||
|
|
||
| const STORAGE_KEY = 'solana-explorer-nicknames'; | ||
|
|
||
| export const getNickname = (address: string): string | null => { | ||
| if (typeof window === 'undefined') return null; | ||
|
|
||
| try { | ||
| const nicknames = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); | ||
| return nicknames[address] || null; | ||
| } catch (error) { | ||
| console.error('Error reading nicknames from localStorage:', error); | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| export const setNickname = (address: string, nickname: string): void => { | ||
| if (typeof window === 'undefined') return; | ||
|
|
||
| try { | ||
| const nicknames = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); | ||
| if (nickname.trim()) { | ||
| nicknames[address] = nickname.trim(); | ||
| } else { | ||
| delete nicknames[address]; | ||
| } | ||
| localStorage.setItem(STORAGE_KEY, JSON.stringify(nicknames)); | ||
|
|
||
| // Dispatch custom event to notify other components | ||
| window.dispatchEvent(new CustomEvent('nicknameUpdated', { detail: { address } })); | ||
| } catch (error) { | ||
| console.error('Error saving nickname to localStorage:', error); | ||
| } | ||
| }; | ||
|
|
||
| export const removeNickname = (address: string): void => { | ||
| if (typeof window === 'undefined') return; | ||
|
|
||
| try { | ||
| const nicknames = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); | ||
| delete nicknames[address]; | ||
| localStorage.setItem(STORAGE_KEY, JSON.stringify(nicknames)); | ||
|
|
||
| // Dispatch custom event to notify other components | ||
| window.dispatchEvent(new CustomEvent('nicknameUpdated', { detail: { address } })); | ||
| } catch (error) { | ||
| console.error('Error removing nickname from localStorage:', error); | ||
| } | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| 'use client'; | ||
|
|
||
| import { useEffect, useState } from 'react'; | ||
|
|
||
| import { getNickname } from '../lib/nicknames'; | ||
|
|
||
| /** | ||
| * Hook to manage nickname for a given address | ||
| * Listens for nickname updates across all instances | ||
| */ | ||
| export function useNickname(address: string) { | ||
| const [nickname, setNickname] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| setNickname(getNickname(address)); | ||
|
|
||
| // Listen for nickname updates | ||
| const handleNicknameUpdate = (event: CustomEvent) => { | ||
| if (event.detail.address === address) { | ||
| setNickname(getNickname(address)); | ||
| } | ||
| }; | ||
|
|
||
| window.addEventListener('nicknameUpdated', handleNicknameUpdate as EventListener); | ||
| return () => { | ||
| window.removeEventListener('nicknameUpdated', handleNicknameUpdate as EventListener); | ||
| }; | ||
| }, [address]); | ||
|
|
||
| return nickname; | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import React from 'react'; | ||
|
|
||
| type Props = { | ||
| width?: number; | ||
| height?: number; | ||
| className?: string; | ||
| }; | ||
|
|
||
| export function EditIcon({ width = 14, height = 14, className }: Props) { | ||
| return ( | ||
| <svg | ||
| width={width} | ||
| height={height} | ||
| viewBox="0 0 24 24" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| strokeWidth="2" | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| className={className} | ||
| > | ||
| <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /> | ||
| <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" /> | ||
| </svg> | ||
| ); | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Explorer uses react-feathers icons. So it is better to use this https://feathericons.com/?query=edit
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rogaldh updated! |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We started to split the logic inside the Explorer according to the FSD. I'd suggest moving it to the
features/directory.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restructured to the following FSD pattern:
app/features/nicknames/lib/for utility functionsapp/features/nicknames/model/for hooksapp/features/nicknames/ui/for components & storiesapp/features/nicknames/index.tsfor feature exports