diff --git a/Frontend/src/app/layout.tsx b/Frontend/src/app/layout.tsx index 0a9e469..237e6c1 100644 --- a/Frontend/src/app/layout.tsx +++ b/Frontend/src/app/layout.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next' import './globals.css' -import { WalletButton } from '@/components/WalletButton' +import { WalletAccountMenu } from '@/components/WalletAccountMenu' export const metadata: Metadata = { title: 'Vaulty — Save Consistently. Grow Your Wealth.', @@ -17,8 +17,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
Vaulty - {/* WalletButton is a Client Component — safe to render in a Server layout */} - + {/* WalletAccountMenu is a Client Component — safe to render in a Server layout */} +
@@ -26,4 +26,4 @@ export default function RootLayout({ children }: { children: React.ReactNode }) ) -} +} \ No newline at end of file diff --git a/Frontend/src/components/WalletAccountMenu.tsx b/Frontend/src/components/WalletAccountMenu.tsx new file mode 100644 index 0000000..a3df3ec --- /dev/null +++ b/Frontend/src/components/WalletAccountMenu.tsx @@ -0,0 +1,232 @@ +'use client' + +/** + * WalletAccountMenu — Accessible, responsive wallet connection menu that shows: + * - Connected/disconnected state on all pages + * - Shortened public key + * - Current network (testnet/mainnet) + * - Connect, disconnect, and error states + * - Full keyboard and screen reader support + */ + +import { useState, useRef, useEffect } from 'react' +import { useWallet } from '@/hooks/useWallet' +import { Button } from '@/components/Button' + +/** Truncate a Stellar public key for display: GABCD…WXYZ */ +function truncateKey(key: string): string { + if (key.length <= 12) return key + return `${key.slice(0, 6)}…${key.slice(-4)}` +} + +export function WalletAccountMenu() { + const { + wallet, + isConnecting, + error, + errorKind, + networkMismatch, + connect, + disconnect, + clearError, + } = useWallet() + + const [isOpen, setIsOpen] = useState(false) + const menuRef = useRef(null) + const triggerRef = useRef(null) + + // Close menu when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + menuRef.current && + !menuRef.current.contains(event.target as Node) && + triggerRef.current && + !triggerRef.current.contains(event.target as Node) + ) { + setIsOpen(false) + } + } + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setIsOpen(false) + triggerRef.current?.focus() + } + } + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside) + document.addEventListener('keydown', handleEscape) + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + document.removeEventListener('keydown', handleEscape) + } + }, [isOpen]) + + // Focus first menu item when opened + useEffect(() => { + if (isOpen && menuRef.current) { + const firstButton = menuRef.current.querySelector('button') + firstButton?.focus() + } + }, [isOpen]) + + // Keyboard navigation for dropdown + const handleKeyDown = (event: React.KeyboardEvent) => { + if (!isOpen) { + if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') { + event.preventDefault() + setIsOpen(true) + } + return + } + + const focusableElements = menuRef.current?.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ) + if (!focusableElements) return + + const firstElement = focusableElements[0] as HTMLElement + const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement + const currentIndex = Array.from(focusableElements).indexOf(event.target as HTMLElement) + + if (event.key === 'ArrowDown') { + event.preventDefault() + const nextIndex = currentIndex < focusableElements.length - 1 ? currentIndex + 1 : 0 + ;(focusableElements[nextIndex] as HTMLElement).focus() + } else if (event.key === 'ArrowUp') { + event.preventDefault() + const prevIndex = currentIndex > 0 ? currentIndex - 1 : focusableElements.length - 1 + ;(focusableElements[prevIndex] as HTMLElement).focus() + } else if (event.key === 'Tab') { + if (!event.shiftKey && event.target === lastElement) { + setIsOpen(false) + } else if (event.shiftKey && event.target === firstElement) { + setIsOpen(false) + } + } + } + + const handleDisconnect = () => { + disconnect() + setIsOpen(false) + } + + return ( +
+ {/* Network mismatch warning */} + {networkMismatch && ( +
+ Wrong network. Switch your Freighter wallet to{' '} + + {process.env.NEXT_PUBLIC_STELLAR_NETWORK?.toUpperCase() ?? 'TESTNET'} + {' '} + and reconnect. +
+ )} + + {/* Error display */} + {error && ( +
+

{error}

+ {errorKind === 'wallet_not_installed' && ( + + Install Freighter → + + )} + +
+ )} + + {/* Trigger button */} + {wallet.isConnected && wallet.publicKey ? ( + <> + + + {/* Dropdown menu */} + {isOpen && ( +
+
+

Connected

+

{truncateKey(wallet.publicKey)}

+

Network: {wallet.network}

+
+ +
+ )} + + ) : ( + + )} +
+ ) +} \ No newline at end of file