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