+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx
index 546470e..e314f77 100644
--- a/src/components/layout/sidebar.tsx
+++ b/src/components/layout/sidebar.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState } from 'react';
+import Image from 'next/image';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
@@ -13,9 +14,9 @@ import {
Receipt,
FileText,
FolderOpen,
- Mail,
Bell,
Camera,
+ Globe,
Settings,
Shield,
Home,
@@ -32,13 +33,32 @@ import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { UserMenu } from '@/components/layout/user-menu';
import type { UserPortRole } from '@/lib/db/schema/users';
import type { Role } from '@/lib/db/schema/users';
+import type { Port } from '@/lib/db/schema/ports';
+
+const LOGO_URL =
+ 'https://s3.portnimara.com/images/Port%20Nimara%20New%20Logo-Circular%20Frame_250px.png';
interface SidebarProps {
portRoles: (UserPortRole & { port: { id: string; slug: string; name: string }; role: Role })[];
isSuperAdmin?: boolean;
user?: { name: string; email: string };
+ /** Ports the user has access to. Drives the footer port switcher. */
+ ports?: Port[];
+}
+
+/**
+ * Turn a snake_cased DB role identifier (e.g. "super_admin") into a human
+ * label ("Super Admin"). Empty/missing → "Staff" fallback.
+ */
+function humanizeRole(roleName: string | null | undefined): string {
+ if (!roleName) return 'Staff';
+ return roleName
+ .split('_')
+ .map((part) => (part ? part[0]!.toUpperCase() + part.slice(1) : part))
+ .join(' ');
}
interface NavItem {
@@ -103,15 +123,38 @@ function buildNavSections(portSlug: string | undefined): NavSection[] {
marinaRequired: true,
items: [
{ href: `${base}/expenses`, label: 'Expenses', icon: Receipt },
- { href: `${base}/scan`, label: 'Scan receipt', icon: Camera },
{ href: `${base}/invoices`, label: 'Invoices', icon: FileText },
+ // Dedicated explainer page covers "Add to Home Screen" install
+ // + walkthrough; the mobile-only scanner UI itself lives at /scan
+ // and is reached via the install or the explainer page button.
+ {
+ href: `${base}/invoices/upload-receipts`,
+ label: 'How to upload receipts',
+ icon: Camera,
+ },
+ ],
+ },
+ {
+ title: 'Insights',
+ marinaRequired: true,
+ items: [
+ // Marketing / Umami integration. Distinct from the main dashboard
+ // (which is sales-focused) so the audience and the metrics don't
+ // compete for visual real estate.
+ {
+ href: `${base}/website-analytics`,
+ label: 'Website analytics',
+ icon: Globe,
+ },
],
},
{
title: 'Communication',
marinaRequired: true,
items: [
- { href: `${base}/email`, label: 'Email', icon: Mail },
+ // Email tab removed: we deferred building a full inbox/threading
+ // feature (would require Google OAuth + scope review + IMAP
+ // syncing infra). Reminders stays since it's already wired up.
{ href: `${base}/reminders`, label: 'Reminders', icon: Bell },
],
},
@@ -185,6 +228,7 @@ function SidebarContent({
hasMarinaAccess,
hasResidentialAccess,
user,
+ ports,
onToggleCollapse,
}: {
collapsed: boolean;
@@ -194,6 +238,7 @@ function SidebarContent({
hasMarinaAccess: boolean;
hasResidentialAccess: boolean;
user?: SidebarProps['user'];
+ ports?: Port[];
/** When provided, renders the collapse toggle row above the user footer (desktop). */
onToggleCollapse?: () => void;
}) {
@@ -201,29 +246,61 @@ function SidebarContent({
const [adminExpanded, setAdminExpanded] = useState(true);
const sections = buildNavSections(portSlug);
+ // Pre-compute every nav href the sidebar offers across all sections so
+ // the active-state check can do longest-prefix-match. Without this,
+ // /invoices/upload-receipts would highlight both "Invoices" and "How to
+ // upload receipts" - every ancestor prefix matches.
+ const allHrefs = sections.flatMap((s) => s.items.map((i) => i.href));
+
function isActive(href: string, exact?: boolean): boolean {
if (exact) return pathname === href;
- return pathname.startsWith(href);
+ if (!pathname.startsWith(href)) return false;
+ // Active only if no other sidebar item has a strictly more-specific
+ // prefix match. That makes the highlight follow the URL into the
+ // deepest registered route (e.g. /invoices/upload-receipts wins over
+ // /invoices when both are sidebar items).
+ return !allHrefs.some((other) => other.length > href.length && pathname.startsWith(other));
}
+ // Sidebar header showcases the Port Nimara logo as the visual anchor of
+ // the brand. Collapse toggle is a small overlay button so it doesn't
+ // compete with the logo for attention.
return (
- {/* Logo area */}
+ {/* Brand header - logo centered (large when expanded, smaller when
+ collapsed). Collapse toggle floats top-right as a tiny chevron. */}
+ {/* User footer - entire row is the trigger for the UserMenu so the
+ user can click their name/avatar to access Profile / Settings /
+ port-switcher / sign-out. The same UserMenu component drives the
+ top-right avatar dropdown, so the menu items stay consistent. */}
+
@@ -334,7 +416,7 @@ function SidebarContent({
);
}
-export function Sidebar({ portRoles, isSuperAdmin = false, user }: SidebarProps) {
+export function Sidebar({ portRoles, isSuperAdmin = false, user, ports }: SidebarProps) {
const sidebarCollapsed = useUIStore((s) => s.sidebarCollapsed);
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
const currentPortSlug = useUIStore((s) => s.currentPortSlug);
@@ -370,6 +452,7 @@ export function Sidebar({ portRoles, isSuperAdmin = false, user }: SidebarProps)
hasMarinaAccess={hasMarinaAccess}
hasResidentialAccess={hasResidentialAccess}
user={user}
+ ports={ports}
onToggleCollapse={toggleSidebar}
/>
diff --git a/src/components/layout/topbar.tsx b/src/components/layout/topbar.tsx
index 51ee6f7..5a84449 100644
--- a/src/components/layout/topbar.tsx
+++ b/src/components/layout/topbar.tsx
@@ -1,6 +1,6 @@
'use client';
-import { Plus, Moon, Sun, LogOut, User, Settings, Bell } from 'lucide-react';
+import { Plus } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useUIStore } from '@/stores/ui-store';
@@ -15,11 +15,10 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
-import { PortSwitcher } from '@/components/layout/port-switcher';
import { Breadcrumbs } from '@/components/layout/breadcrumbs';
import { CommandSearch } from '@/components/search/command-search';
-import { NotificationBell } from '@/components/notifications/notification-bell';
-import { AlertBell } from '@/components/alerts/alert-bell';
+import { Inbox } from '@/components/layout/inbox';
+import { UserMenu } from '@/components/layout/user-menu';
import type { Port } from '@/lib/db/schema/ports';
interface TopbarProps {
@@ -30,31 +29,29 @@ interface TopbarProps {
export function Topbar({ ports, user }: TopbarProps) {
const router = useRouter();
const currentPortSlug = useUIStore((s) => s.currentPortSlug);
- const darkMode = useUIStore((s) => s.darkMode);
- const toggleDarkMode = useUIStore((s) => s.toggleDarkMode);
-
const base = currentPortSlug ? `/${currentPortSlug}` : '';
- function handleToggleDarkMode() {
- toggleDarkMode();
- document.documentElement.classList.toggle('dark');
- }
-
return (
-
- {/* Breadcrumbs / page title */}
-
+ // Three-column grid: breadcrumbs left, search center, actions right.
+ // The brand logo lives in the sidebar header (per design feedback) so the
+ // topbar center is dedicated to the global search bar.
+
+ {/* LEFT: breadcrumbs / page title */}
+
- {/* Actions row */}
-
- {/* Global search — inline with dropdown results */}
-
-
- {/* Port switcher — hidden for single port */}
-
+ {/* CENTER: global search - capped width and horizontally centered
+ inside its grid slot so it sits visually in the middle of the
+ topbar regardless of breadcrumb / action-row width. */}
+
+
+
+
+
+ {/* RIGHT: action row */}
+
{/* + New dropdown */}
@@ -88,17 +85,21 @@ export function Topbar({ ports, user }: TopbarProps) {
- {/* Phase B operational alerts — distinct from user notifications */}
-
-
- {/* Notification bell — real-time via socket */}
-
+ {/* Unified inbox - combines system alerts (operational) and personal
+ notifications (user-targeted) into a single bell with two tabs.
+ Replaces the previous AlertBell + NotificationBell pair. */}
+
- {/* User menu */}
-
-
+ {/* User menu - single source of truth for the profile dropdown.
+ Same component is mounted in the sidebar footer so the menu
+ items (incl. port switcher) stay consistent across triggers. */}
+
@@ -107,56 +108,8 @@ export function Topbar({ ports, user }: TopbarProps) {
-
-
-
-