From 75d1fdc9b6ed2a552bca3396b3be3b098f076f08 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Mon, 30 Mar 2026 11:09:44 -0400 Subject: [PATCH 01/12] update footer --- src/app/components/Footer.tsx | 296 +++++++++++++++++++------- src/app/components/FooterElements.tsx | 59 +++++ 2 files changed, 283 insertions(+), 72 deletions(-) create mode 100644 src/app/components/FooterElements.tsx diff --git a/src/app/components/Footer.tsx b/src/app/components/Footer.tsx index 4dd38a8..1f6c744 100644 --- a/src/app/components/Footer.tsx +++ b/src/app/components/Footer.tsx @@ -1,14 +1,17 @@ 'use client'; import React from 'react'; -import '../styles/Footer.css'; -import { Button, IconButton, useTheme } from '@mui/material'; +import { Box, IconButton, Typography, useTheme } from '@mui/material'; import { GitHub, LinkedIn, OpenInNew } from '@mui/icons-material'; import { MOBILITY_DATA_LINKS } from '../constants/Navigation'; import { fontFamily } from '../Theme'; +import Image from 'next/image'; +import { useTranslations } from 'next-intl'; +import { FooterLink, FooterColumnTitle } from './FooterElements'; const Footer: React.FC = () => { - // TODO: revisit theming for SSR components const theme = useTheme(); + const t = useTranslations('footer'); + const FooterColumnWidth = '185px'; const SlackSvg = ( { viewBox='0 0 24 24' > ); + const currentYear = new Date().getFullYear(); + return ( - + + + + + {t('maintainedBy')} + + + + {t('copyright', { year: currentYear })} + + + + ); }; diff --git a/src/app/components/FooterElements.tsx b/src/app/components/FooterElements.tsx new file mode 100644 index 0000000..e024f57 --- /dev/null +++ b/src/app/components/FooterElements.tsx @@ -0,0 +1,59 @@ +'use client'; +import React from 'react'; +import { Link, Typography, useTheme } from '@mui/material'; +import NextLink from 'next/link'; +import { fontFamily } from '../Theme'; + +interface FooterLinkProps { + href: string; + external?: boolean; + children: React.ReactNode; +} + +export const FooterLink: React.FC = ({ + href, + external, + children, +}) => { + const theme = useTheme(); + return ( + + {children} + + ); +}; +export const FooterColumnTitle: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const theme = useTheme(); + return ( + + {children} + + ); +}; From e1f8eb0b513a8f4e7856d444b94176618a86c6d6 Mon Sep 17 00:00:00 2001 From: Alessandro Kreslin Date: Mon, 30 Mar 2026 11:13:45 -0400 Subject: [PATCH 02/12] header updated --- src/app/components/AuthSessionProvider.tsx | 11 +- src/app/components/Header.style.ts | 5 +- src/app/components/Header.tsx | 357 ++++++++++++--------- src/app/components/HeaderMobileDrawer.tsx | 56 ++++ src/app/components/HeaderSearchBar.tsx | 134 ++++++++ src/app/constants/Navigation.ts | 23 +- 6 files changed, 414 insertions(+), 172 deletions(-) create mode 100644 src/app/components/HeaderSearchBar.tsx diff --git a/src/app/components/AuthSessionProvider.tsx b/src/app/components/AuthSessionProvider.tsx index 60558ec..59621f0 100644 --- a/src/app/components/AuthSessionProvider.tsx +++ b/src/app/components/AuthSessionProvider.tsx @@ -18,12 +18,14 @@ interface AuthSession { isAuthReady: boolean; email: string | null; isAuthenticated: boolean; + displayName?: string | null; } const AuthReadyContext = createContext({ isAuthReady: false, email: null, isAuthenticated: false, + displayName: null, }); /** @@ -58,6 +60,7 @@ export function AuthSessionProvider({ isAuthReady: false, email: null, isAuthenticated: false, + displayName: null, }); const intervalRef = useRef | null>(null); @@ -73,6 +76,7 @@ export function AuthSessionProvider({ isAuthReady: true, email: user.email ?? null, isAuthenticated: !user.isAnonymous, + displayName: user.displayName ?? null, }); setUserCookieSession().catch(() => { console.error('Failed to establish session cookie'); @@ -90,7 +94,12 @@ export function AuthSessionProvider({ 5 * 60 * 1000, ); // 5 minutes } else { - setSession({ isAuthReady: false, email: null, isAuthenticated: false }); + setSession({ + isAuthReady: false, + email: null, + isAuthenticated: false, + displayName: null, + }); dispatch(anonymousLogin()); } }); diff --git a/src/app/components/Header.style.ts b/src/app/components/Header.style.ts index c1057ff..d12fc9d 100644 --- a/src/app/components/Header.style.ts +++ b/src/app/components/Header.style.ts @@ -16,10 +16,7 @@ export const animatedButtonStyling = ( ): SystemStyleObject => ({ minWidth: 'fit-content', px: 0, - mx: { - md: 1, - lg: 2, - }, + mx: { xs: 1.5, lg: 2 }, fontFamily: fontFamily.secondary, '&:hover, &.active': { backgroundColor: 'transparent', diff --git a/src/app/components/Header.tsx b/src/app/components/Header.tsx index 3c088ae..5725afb 100644 --- a/src/app/components/Header.tsx +++ b/src/app/components/Header.tsx @@ -4,13 +4,15 @@ import * as React from 'react'; import dynamic from 'next/dynamic'; import { AppBar, + Avatar, Box, + Divider, Drawer, IconButton, + ListSubheader, Toolbar, Typography, Button, - ListItemIcon, Menu, MenuItem, Select, @@ -20,8 +22,6 @@ import { } from '@mui/material'; import MenuIcon from '@mui/icons-material/Menu'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; -import LogoutIcon from '@mui/icons-material/Logout'; -import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import { navigationAccountItem, SIGN_IN_TARGET, @@ -32,15 +32,13 @@ import { import type NavigationItem from '../interface/Navigation'; import { usePathname, useRouter } from 'next/navigation'; import Image from 'next/image'; -import { BikeScooterOutlined, OpenInNew } from '@mui/icons-material'; +import { OpenInNew } from '@mui/icons-material'; import { useRemoteConfig } from '../context/RemoteConfigProvider'; -import { NestedMenuItem } from 'mui-nested-menu'; -import DirectionsBusIcon from '@mui/icons-material/DirectionsBus'; -import DepartureBoardIcon from '@mui/icons-material/DepartureBoard'; import { fontFamily } from '../Theme'; import { defaultRemoteConfigValues } from '../interface/RemoteConfig'; import { animatedButtonStyling } from './Header.style'; import ThemeToggle from './ThemeToggle'; +import HeaderSearchBar from './HeaderSearchBar'; import { useTranslations, useLocale } from 'next-intl'; import Link from 'next/link'; import { useAuthSession } from './AuthSessionProvider'; @@ -74,7 +72,7 @@ function useClientSearchParams(): URLSearchParams | null { } export default function DrawerAppBar(): React.ReactElement { - const { email: userEmail, isAuthenticated } = useAuthSession(); + const { email: userEmail, isAuthenticated, displayName: userDisplayName } = useAuthSession(); const clientSearchParams = useClientSearchParams(); const hasTransitFeedsRedirectParam = clientSearchParams?.get('utm_source') === 'transitfeeds'; @@ -91,7 +89,7 @@ export default function DrawerAppBar(): React.ReactElement { >(buildNavigationItems(defaultRemoteConfigValues)); const locale = useLocale(); const { config } = useRemoteConfig(); - const t = useTranslations('common'); + const tCommon = useTranslations('common'); React.useEffect(() => { if (hasTransitFeedsRedirectParam) { @@ -125,27 +123,46 @@ export default function DrawerAppBar(): React.ReactElement { const handleLogoutClick = (): void => { setOpenDialog(true); - handleMenuClose(); + setAccountAnchorEl(null); }; const container = typeof window !== 'undefined' ? () => window.document.body : undefined; - const [anchorEl, setAnchorEl] = React.useState(null); + const [validatorAnchorEl, setValidatorAnchorEl] = + React.useState(null); + const validatorCloseTimer = + React.useRef>(undefined); - const handleMenuOpen = (event: React.MouseEvent): void => { - setAnchorEl(event.currentTarget); + const handleValidatorOpen = (e: React.MouseEvent): void => { + clearTimeout(validatorCloseTimer.current); + setValidatorAnchorEl(e.currentTarget); }; - const handleMenuClose = (): void => { - setAnchorEl(null); + const handleValidatorClose = (): void => { + validatorCloseTimer.current = setTimeout(() => { + setValidatorAnchorEl(null); + }, 80); }; - const handleMenuItemClick = (item: NavigationItem | string): void => { - handleMenuClose(); - handleNavigation(item); + const [accountAnchorEl, setAccountAnchorEl] = + React.useState(null); + const accountCloseTimer = + React.useRef>(undefined); + + const handleAccountOpen = (e: React.MouseEvent): void => { + clearTimeout(accountCloseTimer.current); + setAccountAnchorEl(e.currentTarget); + }; + + const handleAccountClose = (): void => { + accountCloseTimer.current = setTimeout(() => { + setAccountAnchorEl(null); + }, 80); }; + const [isSearchOpen, setIsSearchOpen] = React.useState(false); + const metricsOptionsEnabled = config.enableMetrics || userEmail?.endsWith('mobilitydata.org') === true; @@ -164,15 +181,35 @@ export default function DrawerAppBar(): React.ReactElement { sx={{ background: theme.palette.background.paper, fontFamily: fontFamily.secondary, - borderBottom: '1px solid', - borderColor: theme.palette.divider, }} > - +