From 19622985b5e8e3ae5abc3612be6cd3ec7e033224 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 9 May 2026 04:11:01 +0200 Subject: [PATCH] fix(layout): mobile UX cleanup + interest-stage legend popover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mobile UX: - Hide ColumnPicker on `< sm` viewports (cards, no columns to toggle). - Hide kanban toggle in interest list on mobile and snap viewMode back to 'table' if the persisted choice was 'board'. - Drop dead "Inbox" link from the More-sheet (email/IMAP feature is deferred per sidebar.tsx note). - Repoint Notifications nav from `/notifications` (no page.tsx — 404) to `/notifications/preferences` and re-label as "Notification preferences" (the bell stays the surface for actual notifications). - Hide Website Analytics on both desktop sidebar and mobile More-sheet when Umami isn't configured for the port (`useUmamiActive()`). Interests: - New `` popover button in the filter row decodes the card stripe colours to pipeline stage names, kept in sync with `STAGE_DOT` automatically. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/interests/interest-list.tsx | 16 ++++++- src/components/interests/stage-legend.tsx | 52 +++++++++++++++++++++ src/components/layout/mobile/more-sheet.tsx | 23 +++++++-- src/components/layout/sidebar.tsx | 10 +++- src/components/shared/column-picker.tsx | 7 ++- 5 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 src/components/interests/stage-legend.tsx diff --git a/src/components/interests/interest-list.tsx b/src/components/interests/interest-list.tsx index 02a7d783..46d6fe77 100644 --- a/src/components/interests/interest-list.tsx +++ b/src/components/interests/interest-list.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useParams } from 'next/navigation'; import { Plus, @@ -35,6 +35,7 @@ import { ColumnPicker } from '@/components/shared/column-picker'; import { SaveViewDialog } from '@/components/shared/save-view-dialog'; import { useTablePreferences } from '@/hooks/use-table-preferences'; import { InterestCard } from '@/components/interests/interest-card'; +import { StageLegend } from '@/components/interests/stage-legend'; import { TagPicker } from '@/components/shared/tag-picker'; import { Dialog, @@ -63,6 +64,13 @@ export function InterestList() { const queryClient = useQueryClient(); const { viewMode, setViewMode } = usePipelineStore(); + // Force the list view at mobile widths even when the user previously + // toggled the kanban from desktop — the board is desktop-only. + useEffect(() => { + if (typeof window === 'undefined') return; + if (viewMode === 'board' && window.innerWidth < 640) setViewMode('table'); + }, [viewMode, setViewMode]); + const [createOpen, setCreateOpen] = useState(false); const [editInterest, setEditInterest] = useState(null); const [archiveInterest, setArchiveInterest] = useState(null); @@ -157,7 +165,10 @@ export function InterestList() { variant="gradient" actions={
-
+ {/* Kanban view is desktop-only — mobile drops the toggle and + falls back to the list/cards view (the board's column + horizontal-scroll model is unusable at phone widths). */} +
+ + + + +

+ Pipeline stage colors +

+
    + {PIPELINE_STAGES.map((stage: PipelineStage) => ( +
  • + + {STAGE_LABELS[stage]} +
  • + ))} +
+
+ + ); +} diff --git a/src/components/layout/mobile/more-sheet.tsx b/src/components/layout/mobile/more-sheet.tsx index aa7b540f..a695ea41 100644 --- a/src/components/layout/mobile/more-sheet.tsx +++ b/src/components/layout/mobile/more-sheet.tsx @@ -11,7 +11,6 @@ import { Building2, Globe, Home, - Mail, Receipt, Settings, Shield, @@ -26,6 +25,7 @@ import { DrawerTitle, DrawerClose, } from '@/components/shared/drawer'; +import { useUmamiActive } from '@/components/website-analytics/use-website-analytics'; type MoreItem = { label: string; @@ -38,14 +38,21 @@ type MoreItem = { // reps reach the active deals via the Interests tab on a client detail // (or via the new bottom-sheet drawer). Yachts is asset-record traffic // best reached contextually from inside an interest or client. +// +// Inbox is intentionally absent — the email/threading inbox feature was +// deferred (see sidebar.tsx). Re-add this entry once IMAP/SMTP wiring +// + Google OAuth review are done. Website analytics is filtered below +// when Umami isn't configured for this port. const MORE_ITEMS: MoreItem[] = [ { label: 'Interests', icon: Bookmark, segment: 'interests' }, { label: 'Yachts', icon: Ship, segment: 'yachts' }, { label: 'Companies', icon: Building2, segment: 'companies' }, { label: 'Expenses', icon: Receipt, segment: 'expenses' }, - { label: 'Inbox', icon: Mail, segment: 'email' }, { label: 'Reservations', icon: Anchor, segment: 'berth-reservations' }, - { label: 'Notifications', icon: BellRing, segment: 'notifications' }, + // Notifications themselves live on the topbar bell — this entry deep-links + // to the per-channel preferences page. Pointing at the bare `/notifications` + // segment 404s today (no page.tsx, only `/preferences`). + { label: 'Notification preferences', icon: BellRing, segment: 'notifications/preferences' }, { label: 'Residential', icon: Home, segment: 'residential/clients' }, { label: 'Website analytics', icon: Globe, segment: 'website-analytics' }, { label: 'Alerts', icon: ShieldAlert, segment: 'alerts' }, @@ -65,6 +72,14 @@ export function MoreSheet({ const pathname = usePathname(); const portSlug = pathname.split('/').filter(Boolean)[0] ?? 'port-nimara'; + // Hide "Website analytics" if Umami isn't wired up for this port — the + // dedicated tile on the dashboard already does the same. + const umami = useUmamiActive('today'); + const umamiConfigured = umami.data?.error !== 'umami_not_configured'; + const items = MORE_ITEMS.filter( + (item) => item.segment !== 'website-analytics' || umamiConfigured, + ); + return ( @@ -72,7 +87,7 @@ export function MoreSheet({ More
    - {MORE_ITEMS.map((item) => { + {items.map((item) => { const Icon = item.icon; return (
  • diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index 912d8b69..ba1296bb 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -34,6 +34,7 @@ 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 { useUmamiActive } from '@/components/website-analytics/use-website-analytics'; import type { UserPortRole } from '@/lib/db/schema/users'; import type { Role } from '@/lib/db/schema/users'; import type { Port } from '@/lib/db/schema/ports'; @@ -76,6 +77,8 @@ interface NavSection { marinaRequired?: boolean; /** When true, only render if the user has residential-side access. */ residentialRequired?: boolean; + /** When true, only render if Umami analytics is wired up for the port. */ + umamiRequired?: boolean; } function buildNavSections(portSlug: string | undefined): NavSection[] { @@ -140,10 +143,12 @@ function buildNavSections(portSlug: string | undefined): NavSection[] { { title: 'Insights', marinaRequired: true, + umamiRequired: 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. + // compete for visual real estate. Whole section is hidden when + // Umami isn't wired up — see SidebarContent. { href: `${base}/website-analytics`, label: 'Website analytics', @@ -250,6 +255,8 @@ function SidebarContent({ const pathname = usePathname(); const [adminExpanded, setAdminExpanded] = useState(true); const sections = buildNavSections(portSlug); + const umami = useUmamiActive('today'); + const umamiConfigured = umami.data?.error !== 'umami_not_configured'; // Small label under the user identity when the user has access to more // than one port — disambiguates which port is currently active without @@ -324,6 +331,7 @@ function SidebarContent({ if (section.adminRequired && !hasAdminAccess) return null; if (section.marinaRequired && !hasMarinaAccess) return null; if (section.residentialRequired && !hasResidentialAccess) return null; + if (section.umamiRequired && !umamiConfigured) return null; return (
    diff --git a/src/components/shared/column-picker.tsx b/src/components/shared/column-picker.tsx index ae323edd..cc4f4673 100644 --- a/src/components/shared/column-picker.tsx +++ b/src/components/shared/column-picker.tsx @@ -68,9 +68,12 @@ export function ColumnPicker({ return ( -