From 07546576c18788af81567081ece36ad441cfc4fb Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 28 Mar 2026 18:57:57 +0100 Subject: [PATCH 01/20] fix(pricing): default slider to first tier (10k) instead of third (100k) --- components/PricingSection.tsx | 2 +- lib/plans.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/PricingSection.tsx b/components/PricingSection.tsx index 053f94b..3047dd7 100644 --- a/components/PricingSection.tsx +++ b/components/PricingSection.tsx @@ -105,7 +105,7 @@ export default function PricingSection() { const searchParams = useSearchParams() const router = useRouter() const [isYearly, setIsYearly] = useState(false) - const [sliderIndex, setSliderIndex] = useState(2) // Default to 100k (index 2) + const [sliderIndex, setSliderIndex] = useState(0) // Default to 10k (index 0) const [loadingPlan, setLoadingPlan] = useState(null) const { user } = useAuth() diff --git a/lib/plans.ts b/lib/plans.ts index 0707111..1056804 100644 --- a/lib/plans.ts +++ b/lib/plans.ts @@ -33,11 +33,11 @@ export const TRAFFIC_TIERS = [ export function getTierIndexForLimit(limit: number): number { const idx = TRAFFIC_TIERS.findIndex((t) => t.value === limit) - return idx >= 0 ? idx : 2 + return idx >= 0 ? idx : 0 } export function getLimitForTierIndex(index: number): number { - if (index < 0 || index >= TRAFFIC_TIERS.length) return 100000 + if (index < 0 || index >= TRAFFIC_TIERS.length) return 10000 return TRAFFIC_TIERS[index].value } From a6054469eec62564bd215978679f588f27e0e6f6 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 28 Mar 2026 19:12:45 +0100 Subject: [PATCH 02/20] feat: wrap home page in DashboardShell, remove stat cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Home page now uses the same sidebar layout as dashboard pages. Sidebar shows simplified home mode (logo, app switcher, profile) without site-specific nav groups. Stat cards removed — plan info lives in settings, site count is self-evident from the list. --- app/layout-content.tsx | 5 +- app/page.tsx | 94 +++---------------------- components/dashboard/DashboardShell.tsx | 15 ++-- components/dashboard/Sidebar.tsx | 54 +++++++------- 4 files changed, 49 insertions(+), 119 deletions(-) diff --git a/app/layout-content.tsx b/app/layout-content.tsx index 745e0e9..c321a0c 100644 --- a/app/layout-content.tsx +++ b/app/layout-content.tsx @@ -95,6 +95,7 @@ function LayoutInner({ children }: { children: React.ReactNode }) { const showOfflineBar = Boolean(auth.user && !isOnline) // Site pages use DashboardShell with full sidebar — no Header needed const isSitePage = pathname.startsWith('/sites/') && pathname !== '/sites/new' + const isHomePage = pathname === '/' // Checkout page has its own minimal layout — no app header/footer const isCheckoutPage = pathname.startsWith('/checkout') @@ -103,13 +104,13 @@ function LayoutInner({ children }: { children: React.ReactNode }) { } // While auth is loading on a site or checkout page, render nothing to prevent flash of public header - if (auth.loading && (isSitePage || isCheckoutPage)) { + if (auth.loading && (isSitePage || isCheckoutPage || isHomePage)) { return null } // Authenticated site pages: full sidebar layout // DashboardShell inside children handles everything - if (isAuthenticated && isSitePage) { + if (isAuthenticated && (isSitePage || isHomePage)) { return ( <> {showOfflineBar && } diff --git a/app/page.tsx b/app/page.tsx index 22c4b7c..bd565ac 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -23,19 +23,16 @@ import PulseFAQ from '@/components/marketing/PulseFAQ' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@ciphera-net/ui' import { getSitesLimitForPlan } from '@/lib/plans' -import { formatDate } from '@/lib/utils/formatDate' -import { useUnifiedSettings } from '@/lib/unified-settings-context' +import DashboardShell from '@/components/dashboard/DashboardShell' type SiteStatsMap = Record export default function HomePage() { const { user, loading: authLoading } = useAuth() - const { openUnifiedSettings } = useUnifiedSettings() const [sites, setSites] = useState([]) const [sitesLoading, setSitesLoading] = useState(true) const [siteStats, setSiteStats] = useState({}) const [subscription, setSubscription] = useState(null) - const [subscriptionLoading, setSubscriptionLoading] = useState(false) const [showFinishSetupBanner, setShowFinishSetupBanner] = useState(true) const [deleteModalSite, setDeleteModalSite] = useState(null) const [deletedSites, setDeletedSites] = useState([]) @@ -116,13 +113,10 @@ export default function HomePage() { const loadSubscription = async () => { try { - setSubscriptionLoading(true) const sub = await getSubscription() setSubscription(sub) } catch { setSubscription(null) - } finally { - setSubscriptionLoading(false) } } @@ -236,7 +230,8 @@ export default function HomePage() { } return ( -
+ +
{showFinishSetupBanner && (

@@ -263,10 +258,10 @@ export default function HomePage() {

)} -
+
-

Your Sites

-

Manage your analytics sites and view insights.

+

Your Sites

+

Manage your analytics sites and view insights.

{(() => { const siteLimit = getSitesLimitForPlan(subscription?.plan_id) @@ -299,80 +294,6 @@ export default function HomePage() { )}
- {/* * Global Overview - min-h ensures no layout shift when Plan & usage loads */} -
-
-

Total Sites

-

{sites.length}

-
-
-

Total Visitors (24h)

-

- {sites.length === 0 || Object.keys(siteStats).length < sites.length - ? '--' - : Object.values(siteStats).reduce((sum, { stats }) => sum + (stats?.visitors ?? 0), 0).toLocaleString()} -

-
-
-

Plan & usage

- {subscriptionLoading ? ( -
-
-
-
-
-
- ) : subscription ? ( - <> -

- {(() => { - const raw = - subscription.plan_id?.startsWith('price_') - ? 'Pro' - : subscription.plan_id === 'free' || !subscription.plan_id - ? 'Free' - : subscription.plan_id - const label = raw === 'Free' || raw === 'Pro' ? raw : raw.charAt(0).toUpperCase() + raw.slice(1) - return `${label} Plan` - })()} -

- {(typeof subscription.sites_count === 'number' || (subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number') || (!subscription.cancel_at_period_end && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing'))) && ( -

- {typeof subscription.sites_count === 'number' && ( - Sites: {(() => { - const limit = getSitesLimitForPlan(subscription.plan_id) - return limit != null && typeof subscription.sites_count === 'number' ? `${subscription.sites_count}/${limit}` : subscription.sites_count - })()} - )} - {typeof subscription.sites_count === 'number' && (subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number') && ' · '} - {subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' && ( - Pageviews: {subscription.pageview_usage.toLocaleString()}/{subscription.pageview_limit.toLocaleString()} - )} - {!subscription.cancel_at_period_end && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && subscription.current_period_end && ( - - Renews {formatDate(new Date(subscription.current_period_end))} - - )} -

- )} -
- {subscription.has_payment_method ? ( - - ) : ( - - Upgrade - - )} -
- - ) : ( -

Free Plan

- )} -
-
- {!sitesLoading && sites.length === 0 && (
)} -
+
+ ) } diff --git a/components/dashboard/DashboardShell.tsx b/components/dashboard/DashboardShell.tsx index 20b36db..579a15a 100644 --- a/components/dashboard/DashboardShell.tsx +++ b/components/dashboard/DashboardShell.tsx @@ -41,15 +41,15 @@ const Sidebar = dynamic(() => import('./Sidebar'), { ), }) -function GlassTopBar({ siteId }: { siteId: string }) { +function GlassTopBar({ siteId }: { siteId: string | null }) { const { collapsed, toggle } = useSidebar() - const { data: realtime } = useRealtime(siteId) + const { data: realtime } = useRealtime(siteId ?? '') const lastUpdatedRef = useRef(null) const [, setTick] = useState(0) useEffect(() => { - if (realtime) lastUpdatedRef.current = Date.now() - }, [realtime]) + if (siteId && realtime) lastUpdatedRef.current = Date.now() + }, [siteId, realtime]) useEffect(() => { if (lastUpdatedRef.current == null) return @@ -57,7 +57,8 @@ function GlassTopBar({ siteId }: { siteId: string }) { return () => clearInterval(timer) }, [realtime]) - const pageTitle = usePageTitle() + const dashboardTitle = usePageTitle() + const pageTitle = siteId ? dashboardTitle : 'Your Sites' return (
@@ -74,7 +75,7 @@ function GlassTopBar({ siteId }: { siteId: string }) {
{/* Realtime indicator */} - {lastUpdatedRef.current != null && ( + {siteId && lastUpdatedRef.current != null && (
@@ -91,7 +92,7 @@ export default function DashboardShell({ siteId, children, }: { - siteId: string + siteId: string | null children: React.ReactNode }) { const [mobileOpen, setMobileOpen] = useState(false) diff --git a/components/dashboard/Sidebar.tsx b/components/dashboard/Sidebar.tsx index 52153e4..cdd057d 100644 --- a/components/dashboard/Sidebar.tsx +++ b/components/dashboard/Sidebar.tsx @@ -364,7 +364,7 @@ function SettingsButton({ interface SidebarContentProps { isMobile: boolean collapsed: boolean - siteId: string + siteId: string | null sites: Site[] canEdit: boolean pendingHref: string | null @@ -414,32 +414,38 @@ function SidebarContent({ {/* Site Picker */} - + {siteId && ( + + )} {/* Nav Groups */} - + ) : ( +
+ )} {/* Bottom — utility items */}
@@ -488,7 +494,7 @@ function SidebarContent({ export default function Sidebar({ siteId, mobileOpen, onMobileClose, onMobileOpen, }: { - siteId: string; mobileOpen: boolean; onMobileClose: () => void; onMobileOpen: () => void + siteId: string | null; mobileOpen: boolean; onMobileClose: () => void; onMobileOpen: () => void }) { const auth = useAuth() const { user } = auth From 9413fb2a0788373fec7346906698a6a463219cb6 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 28 Mar 2026 19:15:33 +0100 Subject: [PATCH 03/20] fix: match home page max-width to dashboard (max-w-7xl) --- app/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/page.tsx b/app/page.tsx index bd565ac..c5c0e8e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -231,7 +231,7 @@ export default function HomePage() { return ( -
+
{showFinishSetupBanner && (

From 45c518b3ba439e341b7f5b5b2f2de42ddcc3203b Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 28 Mar 2026 19:24:41 +0100 Subject: [PATCH 04/20] feat: add home sidebar nav (sites list, workspace, resources) Three nav groups in home mode: - Your Sites: each site with favicon, Add New Site - Workspace: Integrations, Pricing, Workspace Settings - Resources: Documentation (external link) Same styling as site dashboard sidebar nav items. --- components/dashboard/Sidebar.tsx | 141 +++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 5 deletions(-) diff --git a/components/dashboard/Sidebar.tsx b/components/dashboard/Sidebar.tsx index cdd057d..70662ba 100644 --- a/components/dashboard/Sidebar.tsx +++ b/components/dashboard/Sidebar.tsx @@ -14,7 +14,7 @@ import { getUserOrganizations, switchContext, type OrganizationMember } from '@/ import { setSessionAction } from '@/app/actions/auth' import { logger } from '@/lib/utils/logger' import { FAVICON_SERVICE_URL } from '@/lib/utils/favicon' -import { Gauge as GaugeIcon } from '@phosphor-icons/react' +import { Gauge as GaugeIcon, Plugs as PlugsIcon, Tag as TagIcon } from '@phosphor-icons/react' import { LayoutDashboardIcon, PathIcon, @@ -27,6 +27,7 @@ import { ChevronUpDownIcon, PlusIcon, XIcon, + BookOpenIcon, AppLauncher, UserMenu, type CipheraApp, @@ -330,9 +331,9 @@ function NavLink({ // ─── Settings Button (opens unified modal instead of navigating) ───── function SettingsButton({ - item, collapsed, onClick, + item, collapsed, onClick, settingsContext = 'site', }: { - item: NavItem; collapsed: boolean; onClick?: () => void + item: NavItem; collapsed: boolean; onClick?: () => void; settingsContext?: 'site' | 'workspace' }) { const { openUnifiedSettings } = useUnifiedSettings() @@ -340,7 +341,7 @@ function SettingsButton({

+ + + {canDelete && ( )}
From 663abc9b9e5d294364fcc9a3c50313b138c8ec5a Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 28 Mar 2026 19:42:42 +0100 Subject: [PATCH 06/20] feat: DashboardShell for all auth pages, site settings modal from home - layout-content wraps integrations/pricing in DashboardShell - GlassTopBar derives title per page (Integrations, Pricing, etc.) - Site card gear icon opens settings modal with siteId context - Removed delete button from site cards (accessible via site settings) - Extended InitialTab to accept optional siteId for cross-page use --- app/page.tsx | 17 +-------- .../settings/unified/UnifiedSettingsModal.tsx | 6 +++- components/sites/SiteList.tsx | 35 ++++++------------- lib/unified-settings-context.tsx | 2 +- 4 files changed, 17 insertions(+), 43 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index c506644..8df1ad6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -33,7 +33,6 @@ export default function HomePage() { const [siteStats, setSiteStats] = useState({}) const [subscription, setSubscription] = useState(null) const [showFinishSetupBanner, setShowFinishSetupBanner] = useState(true) - const [deleteModalSite, setDeleteModalSite] = useState(null) const [deletedSites, setDeletedSites] = useState([]) const [permanentDeleteSiteModal, setPermanentDeleteSiteModal] = useState(null) @@ -119,11 +118,6 @@ export default function HomePage() { } } - const handleDelete = (id: string) => { - const site = sites.find((s) => s.id === id) - if (site) setDeleteModalSite(site) - } - const handleRestore = async (id: string) => { try { await restoreSite(id) @@ -312,18 +306,9 @@ export default function HomePage() { )} {(sitesLoading || sites.length > 0) && ( - + )} - setDeleteModalSite(null)} - onDeleted={loadSites} - siteName={deleteModalSite?.name || ''} - siteDomain={deleteModalSite?.domain || ''} - siteId={deleteModalSite?.id || ''} - /> - setPermanentDeleteSiteModal(null)} diff --git a/components/settings/unified/UnifiedSettingsModal.tsx b/components/settings/unified/UnifiedSettingsModal.tsx index 4cbee82..5b44988 100644 --- a/components/settings/unified/UnifiedSettingsModal.tsx +++ b/components/settings/unified/UnifiedSettingsModal.tsx @@ -303,7 +303,11 @@ export default function UnifiedSettingsModal() { useEffect(() => { if (!isOpen || !user?.org_id) return - if (typeof window !== 'undefined') { + if (initTab?.siteId) { + // Site ID passed explicitly (e.g. from home page site card) + setActiveSiteId(initTab.siteId) + if (!initTab?.context) setContext('site') + } else if (typeof window !== 'undefined') { const match = window.location.pathname.match(/\/sites\/([a-f0-9-]+)/) if (match) { setActiveSiteId(match[1]) diff --git a/components/sites/SiteList.tsx b/components/sites/SiteList.tsx index 8065672..c5344a8 100644 --- a/components/sites/SiteList.tsx +++ b/components/sites/SiteList.tsx @@ -5,8 +5,8 @@ import Image from 'next/image' import { Site } from '@/lib/api/sites' import type { Stats } from '@/lib/api/stats' import { formatNumber } from '@ciphera-net/ui' -import { BarChartIcon, SettingsIcon, TrashIcon, BookOpenIcon, ExternalLinkIcon, Button } from '@ciphera-net/ui' -import { useAuth } from '@/lib/auth/context' +import { BarChartIcon, SettingsIcon, BookOpenIcon, ExternalLinkIcon, Button } from '@ciphera-net/ui' +import { useUnifiedSettings } from '@/lib/unified-settings-context' import { FAVICON_SERVICE_URL } from '@/lib/utils/favicon' export type SiteStatsMap = Record @@ -15,18 +15,16 @@ interface SiteListProps { sites: Site[] siteStats: SiteStatsMap loading: boolean - onDelete: (id: string) => void } interface SiteCardProps { site: Site stats: Stats | null statsLoading: boolean - onDelete: (id: string) => void - canDelete: boolean } -function SiteCard({ site, stats, statsLoading, onDelete, canDelete }: SiteCardProps) { +function SiteCard({ site, stats, statsLoading }: SiteCardProps) { + const { openUnifiedSettings } = useUnifiedSettings() const visitors24h = stats?.visitors ?? 0 const pageviews = stats?.pageviews ?? 0 @@ -104,31 +102,20 @@ function SiteCard({ site, stats, statsLoading, onDelete, canDelete }: SiteCardPr View Dashboard - openUnifiedSettings({ context: 'site', tab: 'general', siteId: site.id })} + className="flex items-center justify-center rounded-lg border border-neutral-200 px-3 hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800 text-neutral-500 hover:text-neutral-300 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2 cursor-pointer" title="Site Settings" > - - {canDelete && ( - - )} +
) } -export default function SiteList({ sites, siteStats, loading, onDelete }: SiteListProps) { - const { user } = useAuth() - const canDelete = user?.role === 'owner' || user?.role === 'admin' +export default function SiteList({ sites, siteStats, loading }: SiteListProps) { if (loading) { return ( @@ -172,8 +159,6 @@ export default function SiteList({ sites, siteStats, loading, onDelete }: SiteLi site={site} stats={data?.stats ?? null} statsLoading={!data} - onDelete={onDelete} - canDelete={canDelete} /> ) })} diff --git a/lib/unified-settings-context.tsx b/lib/unified-settings-context.tsx index 5711314..fafacc3 100644 --- a/lib/unified-settings-context.tsx +++ b/lib/unified-settings-context.tsx @@ -2,7 +2,7 @@ import { createContext, useContext, useState, useCallback } from 'react' -type InitialTab = { context?: 'site' | 'workspace' | 'account'; tab?: string } | null +type InitialTab = { context?: 'site' | 'workspace' | 'account'; tab?: string; siteId?: string } | null interface UnifiedSettingsContextType { isOpen: boolean From 9feffa5cc605ca33637a09ca6b9c0fd38b5291c9 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 28 Mar 2026 20:04:51 +0100 Subject: [PATCH 07/20] feat: add breadcrumb navigation to GlassTopBar Site pages show: Your Sites > site-name > Page Title Each segment is clickable for navigation back. Home/non-site pages show plain title as before. --- components/dashboard/DashboardShell.tsx | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/components/dashboard/DashboardShell.tsx b/components/dashboard/DashboardShell.tsx index c2c40b3..76d023c 100644 --- a/components/dashboard/DashboardShell.tsx +++ b/components/dashboard/DashboardShell.tsx @@ -2,11 +2,13 @@ import { useState, useCallback, useEffect, useRef } from 'react' import dynamic from 'next/dynamic' +import Link from 'next/link' import { usePathname } from 'next/navigation' import { formatUpdatedAgo } from '@ciphera-net/ui' -import { SidebarSimple } from '@phosphor-icons/react' +import { CaretRight, SidebarSimple } from '@phosphor-icons/react' import { SidebarProvider, useSidebar } from '@/lib/sidebar-context' import { useRealtime } from '@/lib/swr/dashboard' +import { getSite } from '@/lib/api/sites' import ContentHeader from './ContentHeader' const PAGE_TITLES: Record = { @@ -58,6 +60,7 @@ function GlassTopBar({ siteId }: { siteId: string | null }) { const { data: realtime } = useRealtime(siteId ?? '') const lastUpdatedRef = useRef(null) const [, setTick] = useState(0) + const [siteName, setSiteName] = useState(null) useEffect(() => { if (siteId && realtime) lastUpdatedRef.current = Date.now() @@ -69,13 +72,18 @@ function GlassTopBar({ siteId }: { siteId: string | null }) { return () => clearInterval(timer) }, [realtime]) + useEffect(() => { + if (!siteId) { setSiteName(null); return } + getSite(siteId).then((s) => setSiteName(s.name)).catch(() => {}) + }, [siteId]) + const dashboardTitle = usePageTitle() const homeTitle = useHomePageTitle() const pageTitle = siteId ? dashboardTitle : homeTitle return (
- {/* Left: collapse toggle + page title */} + {/* Left: collapse toggle + breadcrumbs */}
- {pageTitle} + {siteId && siteName ? ( + + ) : ( + {pageTitle} + )}
{/* Realtime indicator */} From 2113ee348a9456adc11d70a1f765d969a0cd5bba Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 28 Mar 2026 20:14:14 +0100 Subject: [PATCH 08/20] feat: add site switcher dropdown to breadcrumbs Site name in breadcrumbs is now clickable with a dropdown showing all sites with favicons. Selecting a site navigates to the same section on that site. Lazy-loads site list on first open. --- components/dashboard/DashboardShell.tsx | 77 +++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/components/dashboard/DashboardShell.tsx b/components/dashboard/DashboardShell.tsx index 76d023c..a128f29 100644 --- a/components/dashboard/DashboardShell.tsx +++ b/components/dashboard/DashboardShell.tsx @@ -3,12 +3,13 @@ import { useState, useCallback, useEffect, useRef } from 'react' import dynamic from 'next/dynamic' import Link from 'next/link' -import { usePathname } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { formatUpdatedAgo } from '@ciphera-net/ui' -import { CaretRight, SidebarSimple } from '@phosphor-icons/react' +import { CaretDown, CaretRight, SidebarSimple } from '@phosphor-icons/react' import { SidebarProvider, useSidebar } from '@/lib/sidebar-context' import { useRealtime } from '@/lib/swr/dashboard' -import { getSite } from '@/lib/api/sites' +import { getSite, listSites, type Site } from '@/lib/api/sites' +import { FAVICON_SERVICE_URL } from '@/lib/utils/favicon' import ContentHeader from './ContentHeader' const PAGE_TITLES: Record = { @@ -55,6 +56,74 @@ const Sidebar = dynamic(() => import('./Sidebar'), { ), }) +// ─── Breadcrumb Site Picker ──────────────────────────────── + +function BreadcrumbSitePicker({ currentSiteId, currentSiteName }: { currentSiteId: string; currentSiteName: string }) { + const [open, setOpen] = useState(false) + const [sites, setSites] = useState([]) + const ref = useRef(null) + const pathname = usePathname() + const router = useRouter() + + useEffect(() => { + if (open && sites.length === 0) { + listSites().then(setSites).catch(() => {}) + } + }, [open, sites.length]) + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) + } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }, []) + + const switchSite = (id: string) => { + // Navigate to same section on the new site + router.push(`/sites/${id}${pathname.replace(/^\/sites\/[^/]+/, '')}`) + setOpen(false) + } + + return ( +
+ + {open && ( +
+
+ {sites.map((site) => ( + + ))} +
+
+ )} +
+ ) +} + +// ─── Glass Top Bar ───────────────────────────────────────── + function GlassTopBar({ siteId }: { siteId: string | null }) { const { collapsed, toggle } = useSidebar() const { data: realtime } = useRealtime(siteId ?? '') @@ -96,7 +165,7 @@ function GlassTopBar({ siteId }: { siteId: string | null }) { From ff256a59867fd3f95df3ea1a609142a2433a10b8 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 28 Mar 2026 20:23:08 +0100 Subject: [PATCH 09/20] fix: center breadcrumb caret, remove dropdown padding gaps --- components/dashboard/DashboardShell.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/dashboard/DashboardShell.tsx b/components/dashboard/DashboardShell.tsx index a128f29..5b97278 100644 --- a/components/dashboard/DashboardShell.tsx +++ b/components/dashboard/DashboardShell.tsx @@ -89,14 +89,14 @@ function BreadcrumbSitePicker({ currentSiteId, currentSiteName }: { currentSiteI
{open && (
-
+
{sites.map((site) => ( + const filtered = sites.filter( + (s) => s.name.toLowerCase().includes(search.toLowerCase()) || s.domain.toLowerCase().includes(search.toLowerCase()) + ) + + const dropdown = ( + {open && ( -
+ +
+ setSearch(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Escape') closePicker() }} + className="w-full px-3 py-1.5 text-sm bg-white/[0.04] border border-white/[0.08] rounded-lg outline-none focus:ring-2 focus:ring-brand-orange/40 text-white placeholder:text-neutral-400" + autoFocus + /> +
- {sites.map((site) => ( + {filtered.map((site) => ( ))} + {filtered.length === 0 &&

No sites found

}
-
+
+ closePicker()} className="flex items-center gap-2 px-3 py-1.5 text-sm text-brand-orange hover:bg-white/[0.06] rounded-lg"> + + Add new site + +
+ )} +
+ ) + + return ( +
+ + {typeof document !== 'undefined' ? createPortal(dropdown, document.body) : dropdown}
) } diff --git a/components/dashboard/Sidebar.tsx b/components/dashboard/Sidebar.tsx index 70662ba..5214f48 100644 --- a/components/dashboard/Sidebar.tsx +++ b/components/dashboard/Sidebar.tsx @@ -1,8 +1,6 @@ 'use client' import { useState, useEffect, useRef, useCallback } from 'react' -import { createPortal } from 'react-dom' -import { motion, AnimatePresence } from 'framer-motion' import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' import { listSites, type Site } from '@/lib/api/sites' @@ -24,7 +22,6 @@ import { CloudUploadIcon, HeartbeatIcon, SettingsIcon, - ChevronUpDownIcon, PlusIcon, XIcon, BookOpenIcon, @@ -112,183 +109,6 @@ function Label({ children, collapsed }: { children: React.ReactNode; collapsed: ) } -// ─── Site Picker ──────────────────────────────────────────── - -function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollapsed, pickerOpenCallback }: { - sites: Site[]; siteId: string; collapsed: boolean - onExpand: () => void; onCollapse: () => void; wasCollapsed: React.MutableRefObject - pickerOpenCallback: React.MutableRefObject<(() => void) | null> -}) { - const [open, setOpen] = useState(false) - const [search, setSearch] = useState('') - const [faviconFailed, setFaviconFailed] = useState(false) - const [faviconLoaded, setFaviconLoaded] = useState(false) - const ref = useRef(null) - const panelRef = useRef(null) - const buttonRef = useRef(null) - const [fixedPos, setFixedPos] = useState<{ left: number; top: number } | null>(null) - const pathname = usePathname() - const router = useRouter() - const currentSite = sites.find((s) => s.id === siteId) - const faviconUrl = currentSite?.domain ? `${FAVICON_SERVICE_URL}?domain=${currentSite.domain}&sz=64` : null - - const updatePosition = useCallback(() => { - if (buttonRef.current) { - const rect = buttonRef.current.getBoundingClientRect() - if (collapsed) { - // Collapsed: open to the right, like AppLauncher/UserMenu/Notifications - let top = rect.top - if (panelRef.current) { - const maxTop = window.innerHeight - panelRef.current.offsetHeight - 8 - top = Math.min(top, Math.max(8, maxTop)) - } - setFixedPos({ left: rect.right + 8, top }) - } else { - // Expanded: open below the button - let top = rect.bottom + 4 - if (panelRef.current) { - const maxTop = window.innerHeight - panelRef.current.offsetHeight - 8 - top = Math.min(top, Math.max(8, maxTop)) - } - setFixedPos({ left: rect.left, top }) - } - } - }, [collapsed]) - - useEffect(() => { - const handler = (e: MouseEvent) => { - const target = e.target as Node - if ( - ref.current && !ref.current.contains(target) && - (!panelRef.current || !panelRef.current.contains(target)) - ) { - if (open) { - setOpen(false); setSearch('') - } - } - } - document.addEventListener('mousedown', handler) - return () => document.removeEventListener('mousedown', handler) - }, [open, onCollapse, wasCollapsed]) - - useEffect(() => { - if (open) { - updatePosition() - requestAnimationFrame(() => updatePosition()) - } - }, [open, updatePosition]) - - const closePicker = () => { - setOpen(false); setSearch('') - } - - const switchSite = (id: string) => { - router.push(`/sites/${id}${pathname.replace(/^\/sites\/[^/]+/, '')}`) - closePicker() - } - - const filtered = sites.filter( - (s) => s.name.toLowerCase().includes(search.toLowerCase()) || s.domain.toLowerCase().includes(search.toLowerCase()) - ) - - const dropdown = ( - - {open && ( - -
- setSearch(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Escape') closePicker() - }} - className="w-full px-3 py-1.5 text-sm bg-white/[0.04] border border-white/[0.08] rounded-lg outline-none focus:ring-2 focus:ring-brand-orange/40 text-white placeholder:text-neutral-400" - autoFocus - /> -
-
- {filtered.map((site) => ( - - ))} - {filtered.length === 0 &&

No sites found

} -
-
- closePicker()} className="flex items-center gap-2 px-3 py-1.5 text-sm text-brand-orange hover:bg-white/[0.06] rounded-lg"> - - Add new site - -
-
- )} -
- ) - - return ( -
- - - {typeof document !== 'undefined' ? createPortal(dropdown, document.body) : dropdown} -
- ) -} - // ─── Nav Item ─────────────────────────────────────────────── function NavLink({ @@ -448,11 +268,7 @@ interface SidebarContentProps { pendingHref: string | null onNavigate: (href: string) => void onMobileClose: () => void - onExpand: () => void - onCollapse: () => void onToggle: () => void - wasCollapsed: React.MutableRefObject - pickerOpenCallbackRef: React.MutableRefObject<(() => void) | null> auth: ReturnType orgs: OrganizationMember[] onSwitchOrganization: (orgId: string | null) => Promise @@ -462,8 +278,8 @@ interface SidebarContentProps { function SidebarContent({ isMobile, collapsed, siteId, sites, canEdit, pendingHref, - onNavigate, onMobileClose, onExpand, onCollapse, onToggle, - wasCollapsed, pickerOpenCallbackRef, auth, orgs, onSwitchOrganization, openSettings, openOrgSettings, + onNavigate, onMobileClose, onToggle, + auth, orgs, onSwitchOrganization, openSettings, openOrgSettings, }: SidebarContentProps) { const router = useRouter() const c = isMobile ? false : collapsed @@ -491,11 +307,6 @@ function SidebarContent({ - {/* Site Picker */} - {siteId && ( - - )} - {/* Nav Groups */} {siteId ? (