import type { PondView } from '@dorfteich/shared'; import { useQuery } from '@tanstack/react-query'; import { Menu, Monitor, Moon, Search, Settings, Sun } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../auth/auth-context'; import { IconButton } from '../components/IconButton'; import { apiGet } from '../lib/api'; import { isTypingTarget } from '../lib/keyboard'; import { useDismissable } from '../lib/use-dismissable'; import { SearchPalette } from '../search/SearchPalette'; import { NotificationsBell } from '../notifications/NotificationsBell'; import { nextThemeMode, useThemeMode, type ThemeMode } from '../theme/theme'; import { usePageActionsSlot } from './page-actions'; import { PondSwitcher } from './PondSwitcher'; import { useCurrentPondRoute } from './use-pond-route'; import { singleKeyShortcutsDisabled } from '../lib/single-key-shortcuts'; /** Icon per CHOSEN mode (not per effective theme): the monitor tells the * user "following the OS" apart from an explicit light/dark pick. */ const THEME_MODE_ICONS: Record = { light: Sun, dark: Moon, system: Monitor, }; /** Three-way theme cycle (issue #182) — sits between the bell and the user * menu; also rendered for signed-out visitors (device-local preference). */ function ThemeToggle(): React.JSX.Element { const { t } = useTranslation(); const [mode, setMode] = useThemeMode(); const Icon = THEME_MODE_ICONS[mode]; return ( setMode(nextThemeMode(mode))} > ); } interface TopBarProps { sidebarCollapsed: boolean; onToggleSidebar: () => void; } export function TopBar({ sidebarCollapsed, onToggleSidebar }: TopBarProps): React.JSX.Element { const { t } = useTranslation(); const { user, logout } = useAuth(); const navigate = useNavigate(); const [menuOpen, setMenuOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); const userMenuRef = useRef(null); useDismissable(userMenuRef, menuOpen, () => setMenuOpen(false)); const { setElement: setPageActionsElement, setPresenceElement } = usePageActionsSlot(); // Pond-settings shortcut next to the pond name (M10 follow-up): owners get // a gear icon while a pond route is active. Shares the sidebar's query key. const { pondSlug } = useCurrentPondRoute(); const pond = useQuery({ queryKey: ['pond', pondSlug], queryFn: () => apiGet(`/ponds/${pondSlug}`), enabled: Boolean(user && pondSlug), }); const isPondOwner = Boolean(user && pond.data && user.id === pond.data.ownerId); async function handleLogout(): Promise { setMenuOpen(false); await logout(); navigate('/login'); } // Global "/" shortcut opens search (unless typing in a field) — issue #50. useEffect(() => { if (!user) return undefined; const onKeyDown = (event: KeyboardEvent): void => { if ( event.key === '/' && !isTypingTarget(event.target) && !searchOpen && !singleKeyShortcutsDisabled() ) { event.preventDefault(); setSearchOpen(true); } }; document.addEventListener('keydown', onKeyDown); return () => document.removeEventListener('keydown', onKeyDown); }, [user, searchOpen]); return (
Dorfteich {user && } {isPondOwner && pondSlug && ( )} {/* Page-scoped slots, rendered only for signed-in users: live presence (#102) and the page's icon actions (#101) portal in while a page route is active. */} {user &&
} {user &&
} {user && ( )} {searchOpen && user && setSearchOpen(false)} />} {user && } {user ? (
{menuOpen && (
setMenuOpen(false)}> {t('auth:menu.settings')} {user.isSiteAdmin && ( setMenuOpen(false)}> {t('auth:menu.admin')} )}
)}
) : ( )}
); }