From aced2b5435bae0bb9489ff17d02d527eb0391609 Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 1 Oct 2026 08:32:06 -0400 Subject: [PATCH] fix: open club updates from the notification bell --- .../founder-portal/founder-portal-shell.tsx | 2 + .../founder-portal/founder-top-bar.tsx | 89 ++++++++++++++++++- website/tests/founder-notifications.test.tsx | 73 +++++++++++++++ 3 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 website/tests/founder-notifications.test.tsx diff --git a/website/components/founder-portal/founder-portal-shell.tsx b/website/components/founder-portal/founder-portal-shell.tsx index 43c49c8..cd73c55 100644 --- a/website/components/founder-portal/founder-portal-shell.tsx +++ b/website/components/founder-portal/founder-portal-shell.tsx @@ -60,6 +60,8 @@ export function FounderPortalShell({ onPostAnnouncement={onPostAnnouncement} onSwitchRole={onSwitchRole} onSwitchClub={onSwitchClub} + pendingApplicationsCount={pendingApplicationsCount} + upcomingRidesCount={upcomingRidesCount} /> {/* Content Body Container */} diff --git a/website/components/founder-portal/founder-top-bar.tsx b/website/components/founder-portal/founder-top-bar.tsx index 24a068f..9b872f9 100644 --- a/website/components/founder-portal/founder-top-bar.tsx +++ b/website/components/founder-portal/founder-top-bar.tsx @@ -1,7 +1,8 @@ 'use client'; -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, useId } from 'react'; import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; import { Club, UserPortalPermissions, ClubRole } from './types'; interface FounderTopBarProps { @@ -13,6 +14,8 @@ interface FounderTopBarProps { onPostAnnouncement?: () => void; onSwitchRole?: (role: ClubRole) => void; onSwitchClub?: () => void; + pendingApplicationsCount?: number; + upcomingRidesCount?: number; } export function FounderTopBar({ @@ -24,9 +27,47 @@ export function FounderTopBar({ onPostAnnouncement, onSwitchRole, onSwitchClub, + pendingApplicationsCount, + upcomingRidesCount, }: FounderTopBarProps) { const [userMenuOpen, setUserMenuOpen] = useState(false); const userMenuRef = useRef(null); + const [notificationsOpen, setNotificationsOpen] = useState(false); + const notificationsRef = useRef(null); + const notificationsButtonRef = useRef(null); + const notificationsId = useId(); + const searchParams = useSearchParams(); + const previewQuery = searchParams?.get('preview') === 'demo' ? '?preview=demo' : ''; + const managePath = `/clubs/${club.slug || club.id}/manage`; + const pendingCount = permissions.canApproveApplications ? (pendingApplicationsCount ?? 0) : 0; + + useEffect(() => { + setNotificationsOpen(false); + }, [club.id, activeNav, permissions.role]); + + useEffect(() => { + if (!notificationsOpen) return; + function handleClickOutside(event: MouseEvent) { + if (!notificationsRef.current?.contains(event.target as Node)) setNotificationsOpen(false); + } + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') { + setNotificationsOpen(false); + notificationsButtonRef.current?.focus(); + } + } + function handleFocusOutside(event: FocusEvent) { + if (!notificationsRef.current?.contains(event.target as Node)) setNotificationsOpen(false); + } + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('focusin', handleFocusOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('focusin', handleFocusOutside); + }; + }, [notificationsOpen]); useEffect(() => { function handleClickOutside(event: MouseEvent) { @@ -80,6 +121,12 @@ export function FounderTopBar({ Club Profile )} + {(activeNav === 'members' || activeNav === 'wall') && ( + <> + / + {activeNav === 'members' ? 'Members' : 'Wall'} + + )} {activeNav === 'applications' && ( <> / @@ -189,22 +236,56 @@ export function FounderTopBar({ )} {/* Notifications */} -
+
- + {pendingCount > 0 && } + {notificationsOpen && ( +
+
+

Club updates

+ +
+

{permissions.canManageSettings ? 'Open a section to review current club activity.' : 'Club management updates are available to Owners and Admins.'}

+
+ {permissions.canApproveApplications && setNotificationsOpen(false)} className="block rounded-xl border border-brand-border p-3 hover:border-brand-cyan focus-visible:outline focus-visible:outline-brand-cyan"> + {pendingCount > 0 ? `${pendingCount} pending ${pendingCount === 1 ? 'application' : 'applications'}` : 'Membership applications'} + Review membership requests. + } + {permissions.canPlanRides && setNotificationsOpen(false)} className="block rounded-xl border border-brand-border p-3 hover:border-brand-cyan focus-visible:outline focus-visible:outline-brand-cyan"> + {upcomingRidesCount && upcomingRidesCount > 0 ? `${upcomingRidesCount} upcoming ${upcomingRidesCount === 1 ? 'ride' : 'rides'}` : 'Upcoming rides'} + See scheduled club rides. + } + {permissions.canManageSettings && setNotificationsOpen(false)} className="block rounded-xl border border-brand-border p-3 hover:border-brand-cyan focus-visible:outline focus-visible:outline-brand-cyan"> + Club Wall + Read announcements and discussions. + } +
+
+ )}
{/* Rider Profile Menu Dropdown */}