From 04c72169287b54721b594e6f38f43492ddd977e0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 7 Feb 2026 21:34:42 -0800 Subject: [PATCH] Add UserButton to SideNav, refactor NavButton into TeamsButton - Create UserButton component at bottom of SideNav with Settings, Language, Theme, Admin, and Logout menu items - Move Settings/Logout/Admin/Docs/Support out of NavButton into UserButton - Remove LanguageButton and ThemeButton from SideNav bottom - Refactor NavButton into TeamsButton with simplified team switching - Simplify WebsiteNav and move TeamsButton to App top bar Co-Authored-By: Claude Opus 4.6 --- src/app/(main)/App.tsx | 13 ++ src/app/(main)/MobileNav.tsx | 4 +- src/app/(main)/SideNav.tsx | 23 +- .../websites/[websiteId]/WebsiteNav.tsx | 149 +++++-------- src/components/input/NavButton.tsx | 70 +----- src/components/input/TeamsButton.tsx | 92 ++++++++ src/components/input/UserButton.tsx | 204 ++++++++++++++++++ src/components/input/WebsiteSelect.tsx | 11 +- src/index.ts | 2 +- 9 files changed, 382 insertions(+), 186 deletions(-) create mode 100644 src/components/input/TeamsButton.tsx create mode 100644 src/components/input/UserButton.tsx diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index eba4e264f..33e998075 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -5,6 +5,7 @@ import { useEffect } from 'react'; import { MobileNav } from '@/app/(main)/MobileNav'; import { SideNav } from '@/app/(main)/SideNav'; import { useConfig, useLoginQuery, useNavigation } from '@/components/hooks'; +import { TeamsButton } from '@/components/input/TeamsButton'; import { LAST_TEAM_CONFIG } from '@/lib/constants'; import { removeItem, setItem } from '@/lib/storage'; import { UpdateNotice } from './UpdateNotice'; @@ -50,6 +51,18 @@ export function App({ children }) { + + + + + {children} diff --git a/src/app/(main)/MobileNav.tsx b/src/app/(main)/MobileNav.tsx index 158886db3..85d03b175 100644 --- a/src/app/(main)/MobileNav.tsx +++ b/src/app/(main)/MobileNav.tsx @@ -5,7 +5,7 @@ import { IconLabel } from '@/components/common/IconLabel'; import { useMessages, useNavigation } from '@/components/hooks'; import { Globe, Grid2x2, LinkIcon } from '@/components/icons'; import { MobileMenuButton } from '@/components/input/MobileMenuButton'; -import { NavButton } from '@/components/input/NavButton'; +import { TeamsButton } from '@/components/input/TeamsButton'; import { Logo } from '@/components/svg'; import { AdminNav } from './admin/AdminNav'; import { SettingsNav } from './settings/SettingsNav'; @@ -44,7 +44,7 @@ export function MobileNav() { return ( <> - + {links.map(link => { return ( diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx index 865a9e280..92172be98 100644 --- a/src/app/(main)/SideNav.tsx +++ b/src/app/(main)/SideNav.tsx @@ -6,26 +6,21 @@ import { Icon, Row, Text, - ThemeButton, Tooltip, TooltipTrigger, } from '@umami/react-zen'; import Link from 'next/link'; -import type { Key } from 'react'; import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav'; import { IconLabel } from '@/components/common/IconLabel'; import { useGlobalState, useMessages, useNavigation } from '@/components/hooks'; import { Globe, Grid2x2, LayoutDashboard, LinkIcon, PanelLeft } from '@/components/icons'; -import { LanguageButton } from '@/components/input/LanguageButton'; -import { NavButton } from '@/components/input/NavButton'; +import { UserButton } from '@/components/input/UserButton'; import { Logo } from '@/components/svg'; export function SideNav(props: any) { const { t, labels } = useMessages(); - const { pathname, renderUrl, websiteId, router } = useNavigation(); - const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed', false); - - const hasNav = !!(websiteId || pathname.startsWith('/admin') || pathname.includes('/settings')); + const { pathname, renderUrl, websiteId } = useNavigation(); + const [isCollapsed] = useGlobalState('sidenav-collapsed', false); const links = [ { @@ -54,10 +49,6 @@ export function SideNav(props: any) { }, ]; - const handleSelect = (id: Key) => { - router.push(id === 'user' ? '/websites' : `/teams/${id}/websites`); - }; - return ( - - - {websiteId ? ( ) : ( @@ -126,9 +114,8 @@ export function SideNav(props: any) { )} - - - + + ); diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx index 6bd892e40..01833eebb 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -32,97 +32,66 @@ export function WebsiteNav({ router.push(renderUrl(`/websites/${value}`)); }; - const renderValue = (value: any) => { - return ( - - {value?.selectedItem?.name} - - ); - }; - - if (isCollapsed !== undefined) { - return ( - - - - - - } label={isCollapsed ? '' : t(labels.back)} padding /> - - - {t(labels.back)} - - - {!isCollapsed && ( - - - - )} - {items.map(({ label: sectionLabel, items: sectionItems }, index) => ( - - {!isCollapsed && ( - - {sectionLabel} - - )} - {sectionItems.map(({ id, path, label, icon }) => { - const isSelected = selectedKey === id; - return ( - - - - - - - - {label} - - - ); - })} - - ))} - - ); - } - return ( - - - + + + + + + } label={isCollapsed ? '' : t(labels.back)} padding /> + + + {t(labels.back)} + + + + + + {items.map(({ label: sectionLabel, items: sectionItems }, index) => ( + + {!isCollapsed && ( + + {sectionLabel} + + )} + {sectionItems.map(({ id, path, label, icon }) => { + const isSelected = selectedKey === id; + return ( + + + + + + + + {label} + + + ); + })} + + ))} ); } diff --git a/src/components/input/NavButton.tsx b/src/components/input/NavButton.tsx index 9f11ab2a0..e7c7504c6 100644 --- a/src/components/input/NavButton.tsx +++ b/src/components/input/NavButton.tsx @@ -15,26 +15,10 @@ import { import { ArrowRight } from 'lucide-react'; import type { Key } from 'react'; import { IconLabel } from '@/components/common/IconLabel'; -import { - useConfig, - useLoginQuery, - useMessages, - useMobile, - useNavigation, -} from '@/components/hooks'; -import { - BookText, - ChevronRight, - ExternalLink, - LifeBuoy, - LockKeyhole, - LogOut, - Settings, - User, - Users, -} from '@/components/icons'; +import { useLoginQuery, useMessages, useMobile, useNavigation } from '@/components/hooks'; +import { ChevronRight, User, Users } from '@/components/icons'; import { Switch } from '@/components/svg'; -import { DOCS_URL, LAST_TEAM_CONFIG } from '@/lib/constants'; +import { LAST_TEAM_CONFIG } from '@/lib/constants'; import { removeItem } from '@/lib/storage'; export interface TeamsButtonProps { @@ -44,13 +28,13 @@ export interface TeamsButtonProps { export function NavButton({ showText = true }: TeamsButtonProps) { const { user } = useLoginQuery(); - const { cloudMode } = useConfig(); const { t, labels } = useMessages(); const { teamId, router } = useNavigation(); const { isMobile } = useMobile(); const team = user?.teams?.find(({ id }) => id === teamId); const selectedKeys = new Set([teamId || 'user']); const label = teamId ? team?.name : user.username; + const cloudMode = !!process.env.cloudMode; const getUrl = (url: string) => { return cloudMode ? `${process.env.cloudUrl}${url}` : url; @@ -134,52 +118,6 @@ export function NavButton({ showText = true }: TeamsButtonProps) { - - } - label={t(labels.settings)} - /> - {cloudMode && ( - <> - } - label={t(labels.documentation)} - > - - - - - } - label={t(labels.support)} - /> - - )} - {!cloudMode && user.isAdmin && ( - <> - - } - label={t(labels.admin)} - /> - - )} - - } - label={t(labels.logout)} - /> diff --git a/src/components/input/TeamsButton.tsx b/src/components/input/TeamsButton.tsx new file mode 100644 index 000000000..c9e27d5fe --- /dev/null +++ b/src/components/input/TeamsButton.tsx @@ -0,0 +1,92 @@ +import { + Button, + Column, + Icon, + Menu, + MenuItem, + MenuSection, + MenuSeparator, + MenuTrigger, + Popover, + Row, + Text, +} from '@umami/react-zen'; +import { ArrowRight } from 'lucide-react'; +import type { Key } from 'react'; +import { IconLabel } from '@/components/common/IconLabel'; +import { useLoginQuery, useMessages, useMobile, useNavigation } from '@/components/hooks'; +import { ChevronRight, User, Users } from '@/components/icons'; +import { LAST_TEAM_CONFIG } from '@/lib/constants'; +import { removeItem } from '@/lib/storage'; + +export function TeamsButton() { + const { user } = useLoginQuery(); + const { t, labels } = useMessages(); + const { teamId, router } = useNavigation(); + const team = user?.teams?.find(({ id }) => id === teamId); + const selectedKeys = new Set([teamId || 'user']); + const label = teamId ? team?.name : user.username; + + const cloudMode = !!process.env.cloudMode; + + const getUrl = (url: string) => { + return cloudMode ? `${process.env.cloudUrl}${url}` : url; + }; + + const handleAction = async (key: Key) => { + if (key === 'user') { + removeItem(LAST_TEAM_CONFIG); + if (cloudMode) { + window.location.href = '/'; + } else { + router.push('/'); + } + } + }; + + return ( + + + + + + + + } label={user.username} /> + + + + + {user?.teams?.map(({ id, name }) => ( + + }> + {name} + + + ))} + + + + + Manage teams + + + + + + + + + + + + ); +} diff --git a/src/components/input/UserButton.tsx b/src/components/input/UserButton.tsx new file mode 100644 index 000000000..8804f8e5d --- /dev/null +++ b/src/components/input/UserButton.tsx @@ -0,0 +1,204 @@ +import { + Column, + Icon, + Menu, + MenuItem, + MenuSeparator, + MenuTrigger, + Popover, + Pressable, + Row, + SubmenuTrigger, + Text, + Tooltip, + TooltipTrigger, + useTheme, +} from '@umami/react-zen'; +import { useConfig, useLocale, useLoginQuery, useMessages, useMobile } from '@/components/hooks'; +import { + BookText, + ExternalLink, + Globe, + LifeBuoy, + LockKeyhole, + LogOut, + Moon, + Settings, + Sun, + SunMoon, + UserCircle, +} from '@/components/icons'; +import { DOCS_URL } from '@/lib/constants'; +import { languages } from '@/lib/lang'; + +export interface UserButtonProps { + showText?: boolean; +} + +export function UserButton({ showText = true }: UserButtonProps) { + const { user } = useLoginQuery(); + const { cloudMode } = useConfig(); + const { t, labels } = useMessages(); + const { locale, saveLocale } = useLocale(); + const { theme, setTheme } = useTheme(); + const { isMobile } = useMobile(); + + const getUrl = (url: string) => { + return cloudMode ? `${process.env.cloudUrl}${url}` : url; + }; + + const languageItems = Object.keys(languages).map(key => ({ + value: key, + label: languages[key].label, + })); + + const items = [ + cloudMode && { + id: 'docs', + label: t(labels.documentation), + path: DOCS_URL, + icon: , + target: '_blank', + external: true, + }, + cloudMode && { + id: 'support', + label: t(labels.support), + path: getUrl('/settings/support'), + icon: , + }, + !cloudMode && + user.isAdmin && { + id: 'admin', + label: t(labels.admin), + path: '/admin', + icon: , + }, + { + id: 'separator', + separator: true, + }, + { + id: 'logout', + label: t(labels.logout), + path: getUrl('/logout'), + icon: , + }, + ].filter(Boolean); + + return ( + + + + + + + + + {showText && {user.username}} + + + + {user.username} + + + + + + + + + + {t(labels.settings)} + + + + + + + + + {t(labels.language)} + + + + saveLocale(key as string)} + style={{ maxHeight: 300, overflow: 'auto' }} + > + {languageItems.map(({ value, label }) => ( + + {label} + + ))} + + + + + + + + + + {t(labels.theme)} + + + + setTheme(key as 'light' | 'dark')} + > + + + + + + Light + + + + + + + + Dark + + + + + + {items.map(({ id, path, label, icon, separator, target, external }: any) => { + if (separator) { + return ; + } + return ( + + + {icon} + {label} + {external && ( + + + + )} + + + ); + })} + + + + + ); +} diff --git a/src/components/input/WebsiteSelect.tsx b/src/components/input/WebsiteSelect.tsx index f3b8e7ff5..62051bdb5 100644 --- a/src/components/input/WebsiteSelect.tsx +++ b/src/components/input/WebsiteSelect.tsx @@ -13,11 +13,13 @@ export function WebsiteSelect({ teamId, onChange, includeTeams, + isCollapsed, ...props }: { websiteId?: string; teamId?: string; includeTeams?: boolean; + isCollapsed?: boolean; } & SelectProps) { const { t, messages } = useMessages(); const { data: website } = useWebsiteQuery(websiteId); @@ -43,14 +45,6 @@ export function WebsiteSelect({ onChange(id); }; - const renderValue = () => { - return ( - - {name ?? website?.name} - - ); - }; - return (