diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 4679ae0..7e7b18f 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -18,6 +18,7 @@ import { motion, AnimatePresence } from 'framer-motion'; import type { UiConfig } from './types'; import { LogoMark } from './components/LogoMark'; import { NotificationBell } from './components/NotificationBell'; +import { UserAvatarDropdown } from './components/UserAvatarDropdown'; import { CopyablePublicKey } from './components/CopyablePublicKey'; import { FreighterAdapter } from './lib/wallet/FreighterAdapter'; @@ -336,6 +337,10 @@ const App = () => { ) : null} + setActiveTab('settings')} + onNotifications={() => setActiveTab('notifications')} + /> diff --git a/dashboard/src/components/UserAvatarDropdown.tsx b/dashboard/src/components/UserAvatarDropdown.tsx new file mode 100644 index 0000000..018a2ea --- /dev/null +++ b/dashboard/src/components/UserAvatarDropdown.tsx @@ -0,0 +1,276 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Settings, + LogOut, + ChevronDown, + Shield, + Bell, + HelpCircle, + Copy, + Check, +} from 'lucide-react'; + +interface UserAvatarDropdownProps { + /** Display name shown in the avatar and dropdown header */ + displayName?: string; + /** Email or sub-label shown under the display name */ + email?: string; + /** Role badge label */ + role?: string; + /** Callback when "Settings" is selected */ + onSettings?: () => void; + /** Callback when "Notifications" is selected */ + onNotifications?: () => void; + /** Callback when "Sign Out" is selected */ + onSignOut?: () => void; +} + +/** Derive initials from a display name (up to 2 characters). */ +const getInitials = (name: string): string => { + const parts = name.trim().split(/\s+/); + if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); +}; + +/** Deterministic gradient colour based on name string. */ +const getAvatarGradient = (name: string): string => { + const gradients = [ + 'from-blue-500 to-violet-600', + 'from-teal-500 to-cyan-600', + 'from-amber-500 to-orange-600', + 'from-rose-500 to-pink-600', + 'from-emerald-500 to-green-600', + 'from-indigo-500 to-purple-600', + ]; + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = (hash << 5) - hash + name.charCodeAt(i); + hash |= 0; + } + return gradients[Math.abs(hash) % gradients.length]; +}; + +export const UserAvatarDropdown: React.FC = ({ + displayName = 'Institutional Admin', + email = 'admin@anchorpoint.local', + role = 'Admin', + onSettings, + onNotifications, + onSignOut, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [copied, setCopied] = useState(false); + const containerRef = useRef(null); + + const initials = getInitials(displayName); + const gradient = getAvatarGradient(displayName); + + /* Close on outside click */ + useEffect(() => { + const handleOutside = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setIsOpen(false); + } + }; + if (isOpen) document.addEventListener('mousedown', handleOutside); + return () => document.removeEventListener('mousedown', handleOutside); + }, [isOpen]); + + /* Close on Escape */ + useEffect(() => { + const handleKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setIsOpen(false); + }; + if (isOpen) document.addEventListener('keydown', handleKey); + return () => document.removeEventListener('keydown', handleKey); + }, [isOpen]); + + const copyEmail = async () => { + try { + await navigator.clipboard.writeText(email); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + /* clipboard not available */ + } + }; + + const handleAction = (cb?: () => void) => { + setIsOpen(false); + cb?.(); + }; + + const menuItems = [ + { + id: 'settings', + icon: Settings, + label: 'Account Settings', + description: 'Manage preferences', + onClick: () => handleAction(onSettings), + }, + { + id: 'notifications', + icon: Bell, + label: 'Notifications', + description: 'View alerts & events', + onClick: () => handleAction(onNotifications), + }, + { + id: 'kyc', + icon: Shield, + label: 'KYC / Compliance', + description: 'Verification status', + onClick: () => handleAction(), + }, + { + id: 'help', + icon: HelpCircle, + label: 'Help & Support', + description: 'Docs & contact', + onClick: () => handleAction(), + }, + ]; + + return ( +
+ {/* Trigger button */} + + + {/* Dropdown panel */} + + {isOpen && ( + + {/* Header */} +
+ +
+

{displayName}

+ +
+ {/* Role badge */} + + {role} + +
+ + {/* Menu items */} +
+ {menuItems.map((item) => ( + + ))} +
+ + {/* Divider + Sign out */} +
+ +
+ + {/* Footer version pill */} +
+ AnchorPoint ยท v1.0.0 +
+
+ )} +
+
+ ); +};