From f1fc8facb478be50701d52f8db334a84a01a6016 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 18 Mar 2026 22:01:51 +0100 Subject: [PATCH] feat: move utility items from header to sidebar Move theme toggle, notifications, app switcher, and user profile from the top header bar into the sidebar. App switcher at the top (scope switch), utilities at the bottom. Header now only shows on mobile for the hamburger menu. --- components/dashboard/ContentHeader.tsx | 99 ++------------ components/dashboard/Sidebar.tsx | 121 +++++++++++++++--- .../notifications/NotificationCenter.tsx | 8 +- 3 files changed, 118 insertions(+), 110 deletions(-) diff --git a/components/dashboard/ContentHeader.tsx b/components/dashboard/ContentHeader.tsx index fd5ab99..7f67b84 100644 --- a/components/dashboard/ContentHeader.tsx +++ b/components/dashboard/ContentHeader.tsx @@ -1,102 +1,21 @@ 'use client' -import { useEffect, useState } from 'react' -import { useRouter } from 'next/navigation' -import Link from 'next/link' -import { ThemeToggle, AppLauncher, UserMenu, type CipheraApp, MenuIcon } from '@ciphera-net/ui' -import { useAuth } from '@/lib/auth/context' -import { useSettingsModal } from '@/lib/settings-modal-context' -import { getUserOrganizations, switchContext, type OrganizationMember } from '@/lib/api/organization' -import { setSessionAction } from '@/app/actions/auth' -import { logger } from '@/lib/utils/logger' -import NotificationCenter from '@/components/notifications/NotificationCenter' - -const CIPHERA_APPS: CipheraApp[] = [ - { - id: 'pulse', - name: 'Pulse', - description: 'Your current app — Privacy-first analytics', - icon: 'https://ciphera.net/pulse_icon_no_margins.png', - href: 'https://pulse.ciphera.net', - isAvailable: false, - }, - { - id: 'drop', - name: 'Drop', - description: 'Secure file sharing', - icon: 'https://ciphera.net/drop_icon_no_margins.png', - href: 'https://drop.ciphera.net', - isAvailable: true, - }, - { - id: 'auth', - name: 'Auth', - description: 'Your Ciphera account settings', - icon: 'https://ciphera.net/auth_icon_no_margins.png', - href: 'https://auth.ciphera.net', - isAvailable: true, - }, -] +import { MenuIcon } from '@ciphera-net/ui' export default function ContentHeader({ onMobileMenuOpen, }: { onMobileMenuOpen: () => void }) { - const auth = useAuth() - const router = useRouter() - const { openSettings } = useSettingsModal() - const [orgs, setOrgs] = useState([]) - - useEffect(() => { - if (auth.user) { - getUserOrganizations() - .then((organizations) => setOrgs(Array.isArray(organizations) ? organizations : [])) - .catch(err => logger.error('Failed to fetch orgs', err)) - } - }, [auth.user]) - - const handleSwitchOrganization = async (orgId: string | null) => { - if (!orgId) return - try { - const { access_token } = await switchContext(orgId) - await setSessionAction(access_token) - sessionStorage.setItem('pulse_switching_org', 'true') - window.location.reload() - } catch (err) { - logger.error('Failed to switch organization', err) - } - } - return ( -
- {/* Left: mobile hamburger */} -
- -
- - {/* Right: actions */} -
- - - - router.push('/onboarding')} - allowPersonalOrganization={false} - onOpenSettings={openSettings} - /> -
+
+
) } diff --git a/components/dashboard/Sidebar.tsx b/components/dashboard/Sidebar.tsx index ce2f135..e273f7f 100644 --- a/components/dashboard/Sidebar.tsx +++ b/components/dashboard/Sidebar.tsx @@ -5,6 +5,10 @@ import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' import { listSites, type Site } from '@/lib/api/sites' import { useAuth } from '@/lib/auth/context' +import { useSettingsModal } from '@/lib/settings-modal-context' +import { getUserOrganizations, switchContext, type OrganizationMember } from '@/lib/api/organization' +import { setSessionAction } from '@/app/actions/auth' +import { logger } from '@/lib/utils/logger' import { FAVICON_SERVICE_URL } from '@/lib/utils/icons' import { LayoutDashboardIcon, @@ -20,7 +24,39 @@ import { ChevronUpDownIcon, PlusIcon, XIcon, + ThemeToggle, + AppLauncher, + UserMenu, + type CipheraApp, } from '@ciphera-net/ui' +import NotificationCenter from '@/components/notifications/NotificationCenter' + +const CIPHERA_APPS: CipheraApp[] = [ + { + id: 'pulse', + name: 'Pulse', + description: 'Your current app — Privacy-first analytics', + icon: 'https://ciphera.net/pulse_icon_no_margins.png', + href: 'https://pulse.ciphera.net', + isAvailable: false, + }, + { + id: 'drop', + name: 'Drop', + description: 'Secure file sharing', + icon: 'https://ciphera.net/drop_icon_no_margins.png', + href: 'https://drop.ciphera.net', + isAvailable: true, + }, + { + id: 'auth', + name: 'Auth', + description: 'Your Ciphera account settings', + icon: 'https://ciphera.net/auth_icon_no_margins.png', + href: 'https://auth.ciphera.net', + isAvailable: true, + }, +] const SIDEBAR_KEY = 'pulse_sidebar_collapsed' const EXPANDED = 256 @@ -239,10 +275,14 @@ export default function Sidebar({ }: { siteId: string; mobileOpen: boolean; onMobileClose: () => void; onMobileOpen: () => void }) { - const { user } = useAuth() + const auth = useAuth() + const { user } = auth const canEdit = user?.role === 'owner' || user?.role === 'admin' const pathname = usePathname() + const router = useRouter() + const { openSettings } = useSettingsModal() const [sites, setSites] = useState([]) + const [orgs, setOrgs] = useState([]) const [pendingHref, setPendingHref] = useState(null) const wasCollapsedRef = useRef(false) // Safe to read localStorage directly — this component is loaded with ssr:false @@ -251,6 +291,25 @@ export default function Sidebar({ }) useEffect(() => { listSites().then(setSites).catch(() => {}) }, []) + useEffect(() => { + if (user) { + getUserOrganizations() + .then((organizations) => setOrgs(Array.isArray(organizations) ? organizations : [])) + .catch(err => logger.error('Failed to fetch orgs', err)) + } + }, [user]) + + const handleSwitchOrganization = async (orgId: string | null) => { + if (!orgId) return + try { + const { access_token } = await switchContext(orgId) + await setSessionAction(access_token) + sessionStorage.setItem('pulse_switching_org', 'true') + window.location.reload() + } catch (err) { + logger.error('Failed to switch organization', err) + } + } useEffect(() => { setPendingHref(null); onMobileClose() }, [pathname, onMobileClose]) useEffect(() => { @@ -284,6 +343,11 @@ export default function Sidebar({ return (
+ {/* App Switcher — top of sidebar (scope-level switch) */} +
+ +
+ {/* Logo — fixed layout, text fades */} @@ -315,23 +379,44 @@ export default function Sidebar({ ))} - {/* Bottom */} -
- {canEdit && ( - - )} - {!isMobile && ( - - )} + {/* Bottom — utility items */} +
+ {/* Theme, Notifications, Profile */} +
+ + + router.push('/onboarding')} + allowPersonalOrganization={false} + onOpenSettings={openSettings} + compact + anchor="right" + /> +
+ + {/* Settings + Collapse */} +
+ {canEdit && ( + + )} + {!isMobile && ( + + )} +
) diff --git a/components/notifications/NotificationCenter.tsx b/components/notifications/NotificationCenter.tsx index 7576864..2b74a18 100644 --- a/components/notifications/NotificationCenter.tsx +++ b/components/notifications/NotificationCenter.tsx @@ -37,7 +37,7 @@ function BellIcon({ className }: { className?: string }) { const LOADING_DELAY_MS = 250 const POLL_INTERVAL_MS = 90_000 -export default function NotificationCenter() { +export default function NotificationCenter({ anchor = 'bottom' }: { anchor?: 'bottom' | 'right' }) { const [open, setOpen] = useState(false) const [notifications, setNotifications] = useState([]) const [unreadCount, setUnreadCount] = useState(0) @@ -152,7 +152,11 @@ export default function NotificationCenter() { id="notification-dropdown" role="dialog" aria-label="Notifications" - className="fixed left-4 right-4 top-16 sm:absolute sm:left-auto sm:right-0 sm:top-full sm:mt-2 sm:w-96 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden z-[100]" + className={`fixed left-4 right-4 top-16 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden z-[100] ${ + anchor === 'right' + ? 'sm:absolute sm:left-full sm:top-0 sm:ml-2 sm:right-auto sm:w-96' + : 'sm:absolute sm:left-auto sm:right-0 sm:top-full sm:mt-2 sm:w-96' + }`} >

Notifications