diff --git a/app/me/notifications/page.tsx b/app/me/notifications/page.tsx index 8b7e58fa..59a71019 100644 --- a/app/me/notifications/page.tsx +++ b/app/me/notifications/page.tsx @@ -104,10 +104,6 @@ export default function NotificationsPage() { const { setUnreadCount, clearUnreadCount } = useNotificationStore(); - useEffect(() => { - setUnreadCount(unreadCount); - }, [unreadCount, setUnreadCount]); - useNotificationPolling(notificationsHook, { interval: 30000, enabled: true, diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index c4703ddb..d4b3ac94 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -27,9 +27,10 @@ import { SidebarMenuItem, } from '@/components/ui/sidebar'; import Image from 'next/image'; -import Link from 'next/link'; import { useNotificationStore } from '@/lib/stores/notification-store'; import { Logo } from './landing-page/navbar'; +import { useNotifications } from '@/hooks/useNotifications'; +import { authClient } from '@/lib/auth-client'; const getNavigationData = (counts?: { participating?: number; @@ -115,8 +116,8 @@ const getNavigationData = (counts?: { url: '/me/notifications', icon: IconBell, badge: - counts?.unreadNotifications && counts.unreadNotifications > 0 - ? counts.unreadNotifications.toString() + (counts?.unreadNotifications ?? 0) > 0 + ? String(counts?.unreadNotifications) : undefined, }, ], @@ -136,6 +137,12 @@ export function AppSidebar({ user: UserData; counts?: { participating?: number; submissions?: number; projects?: number }; } & React.ComponentProps) { + const { data: session } = authClient.useSession(); + const userId = session?.user?.id; + + // Initialize notifications hook to ensure it fetches globally and syncs with store + useNotifications({ enabled: !!userId }); + const unreadNotifications = useNotificationStore(state => state.unreadCount); const navigationData = React.useMemo( diff --git a/components/nav-user.tsx b/components/nav-user.tsx index edcd65d0..df269f80 100644 --- a/components/nav-user.tsx +++ b/components/nav-user.tsx @@ -26,6 +26,9 @@ import { useSidebar, } from '@/components/ui/sidebar'; import { Badge } from './ui/badge'; +import Link from 'next/link'; +import { useAuthActions } from '@/hooks/use-auth'; +import { useNotificationStore } from '@/lib/stores/notification-store'; export interface NavUserProps { user: { @@ -37,6 +40,14 @@ export interface NavUserProps { export const NavUser = ({ user }: NavUserProps): React.ReactElement => { const { isMobile } = useSidebar(); + const { logout } = useAuthActions(); + const { unreadCount: unreadNotifications, clearUnreadCount } = + useNotificationStore(); + + const handleLogout = () => { + logout(); + clearUnreadCount(); + }; return ( @@ -68,7 +79,7 @@ export const NavUser = ({ user }: NavUserProps): React.ReactElement => { {
- + {user.name} - + {user.email}
@@ -96,22 +107,44 @@ export const NavUser = ({ user }: NavUserProps): React.ReactElement => { - - - Account Settings + + + + Account Settings + - - - Billing + + + + Billing + - - - Notifications - 3 + + + + Notifications + {unreadNotifications > 0 && ( + + {unreadNotifications} + + )} + - - + + Log out diff --git a/components/wallet/FamilyWalletDrawer.tsx b/components/wallet/FamilyWalletDrawer.tsx index e9e7cb67..9f49efac 100644 --- a/components/wallet/FamilyWalletDrawer.tsx +++ b/components/wallet/FamilyWalletDrawer.tsx @@ -212,6 +212,10 @@ export function FamilyWalletDrawer({ setValidateResult('idle'); try { const result = await validateSendDestination(dest, currency); + + // Protect against stale responses if the user has changed the destination + if (sendDestination.trim() !== dest) return; + if (result.valid) { setValidateResult('valid'); setValidateError(''); @@ -222,15 +226,45 @@ export function FamilyWalletDrawer({ ); } } catch (err: unknown) { + // Protect against stale responses + if (sendDestination.trim() !== dest) return; + const { message, details } = getErrorDisplay(err); setValidateResult('invalid'); setValidateError(message); setValidateErrorDetails(details); } finally { - setValidateLoading(false); + // We still want to clear loading if it's the latest call + if (sendDestination.trim() === dest) { + setValidateLoading(false); + } } }, [sendDestination, sendCurrency, getErrorDisplay]); + // Auto-validate destination address + useEffect(() => { + const trimmedDest = sendDestination.trim(); + + // Reset state if empty + if (!trimmedDest) { + setValidateResult('idle'); + setValidateError(''); + return; + } + + // Immediate trigger if 56 chars + if (trimmedDest.length === 56) { + handleValidateDestination(); + return; + } + + const timer = setTimeout(() => { + handleValidateDestination(); + }, 500); + + return () => clearTimeout(timer); + }, [sendDestination, sendCurrency, handleValidateDestination]); + const handleSendSubmit = useCallback(async () => { const dest = sendDestination.trim(); const currency = sendCurrency || 'XLM'; @@ -833,37 +867,26 @@ export function FamilyWalletDrawer({ -
+
{ setSendDestination(e.target.value); - setValidateResult('idle'); - setValidateError(''); }} - className='font-mono text-sm' + className='pr-10 font-mono text-sm' /> - + ) : validateResult === 'invalid' && + sendDestination.trim().length >= 56 ? ( + + ) : null} +
{validateResult === 'invalid' && validateError && ( diff --git a/hooks/useNotifications.ts b/hooks/useNotifications.ts index 8493ad1f..953a504a 100644 --- a/hooks/useNotifications.ts +++ b/hooks/useNotifications.ts @@ -3,11 +3,13 @@ import { useSocket } from './useSocket'; import { getNotifications } from '@/lib/api/notifications'; import { Notification } from '@/types/notifications'; import { reportError } from '@/lib/error-reporting'; +import { useNotificationStore } from '@/lib/stores/notification-store'; interface UseNotificationsOptions { page?: number; limit?: number; autoFetch?: boolean; + enabled?: boolean; } export interface UseNotificationsReturn { @@ -32,11 +34,17 @@ export function useNotifications( // Handle overloaded arguments const userId = typeof input === 'string' ? input : undefined; const options = typeof input === 'object' ? input : {}; - const { page: initialPage = 1, limit = 10, autoFetch = true } = options; + const { + page: initialPage = 1, + limit = 10, + autoFetch = true, + enabled = true, + } = options; const { socket, isConnected } = useSocket({ namespace: '/notifications', userId, + autoConnect: enabled && autoFetch, }); const [notifications, setNotifications] = useState([]); @@ -45,6 +53,16 @@ export function useNotifications( const [error, setError] = useState(null); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(initialPage); + const [hasFetched, setHasFetched] = useState(false); + + const { setUnreadCount: setGlobalUnreadCount } = useNotificationStore(); + + // Sync with global store + useEffect(() => { + if (hasFetched) { + setGlobalUnreadCount(unreadCount); + } + }, [unreadCount, setGlobalUnreadCount, hasFetched]); // Merge server list with current state: dedupe by id, preserve optimistic read state (short rollback path) const mergeNotifications = useCallback( @@ -79,6 +97,7 @@ export function useNotifications( mergeNotifications(prev, response.notifications) ); setTotal(response.total || 0); + setHasFetched(true); } } catch (err) { reportError(err, { context: 'notifications-fetch' }); @@ -142,6 +161,7 @@ export function useNotifications( // Listen for unread count updates const handleUnreadCount = (data: { count: number }) => { setUnreadCount(data.count); + setHasFetched(true); }; // Listen for notification updates