feat: wrap home page in DashboardShell, remove stat cards

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.
This commit is contained in:
Usman Baig
2026-03-28 19:12:45 +01:00
parent 07546576c1
commit a6054469ee
4 changed files with 49 additions and 119 deletions

View File

@@ -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 && <OfflineBanner isOnline={isOnline} />}

View File

@@ -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<string, { stats: Stats }>
export default function HomePage() {
const { user, loading: authLoading } = useAuth()
const { openUnifiedSettings } = useUnifiedSettings()
const [sites, setSites] = useState<Site[]>([])
const [sitesLoading, setSitesLoading] = useState(true)
const [siteStats, setSiteStats] = useState<SiteStatsMap>({})
const [subscription, setSubscription] = useState<SubscriptionDetails | null>(null)
const [subscriptionLoading, setSubscriptionLoading] = useState(false)
const [showFinishSetupBanner, setShowFinishSetupBanner] = useState(true)
const [deleteModalSite, setDeleteModalSite] = useState<Site | null>(null)
const [deletedSites, setDeletedSites] = useState<Site[]>([])
@@ -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 (
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
<DashboardShell siteId={null}>
<div className="px-6">
{showFinishSetupBanner && (
<div className="mb-6 flex items-center justify-between gap-4 rounded-2xl border border-brand-orange/30 bg-brand-orange/10 px-4 py-3">
<p className="text-sm text-neutral-300">
@@ -263,10 +258,10 @@ export default function HomePage() {
</div>
)}
<div className="mb-8 flex items-center justify-between">
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-white">Your Sites</h1>
<p className="mt-1 text-sm text-neutral-400">Manage your analytics sites and view insights.</p>
<h1 className="text-lg font-semibold text-neutral-200 mb-1">Your Sites</h1>
<p className="text-sm text-neutral-400">Manage your analytics sites and view insights.</p>
</div>
{(() => {
const siteLimit = getSitesLimitForPlan(subscription?.plan_id)
@@ -299,80 +294,6 @@ export default function HomePage() {
)}
</div>
{/* * Global Overview - min-h ensures no layout shift when Plan & usage loads */}
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="flex min-h-[100px] sm:min-h-[160px] flex-col rounded-2xl border border-neutral-800 bg-neutral-900 p-4">
<p className="text-sm text-neutral-400">Total Sites</p>
<p className="text-2xl font-bold text-white">{sites.length}</p>
</div>
<div className="flex min-h-[100px] sm:min-h-[160px] flex-col rounded-2xl border border-neutral-800 bg-neutral-900 p-4">
<p className="text-sm text-neutral-400">Total Visitors (24h)</p>
<p className="text-2xl font-bold text-white">
{sites.length === 0 || Object.keys(siteStats).length < sites.length
? '--'
: Object.values(siteStats).reduce((sum, { stats }) => sum + (stats?.visitors ?? 0), 0).toLocaleString()}
</p>
</div>
<div className="flex min-h-[160px] flex-col rounded-2xl border border-neutral-800 bg-brand-orange/10 p-4">
<p className="text-sm text-brand-orange">Plan & usage</p>
{subscriptionLoading ? (
<div className="animate-pulse space-y-2">
<div className="h-6 w-24 rounded bg-brand-orange/20" />
<div className="h-4 w-full rounded bg-brand-orange/20" />
<div className="h-4 w-3/4 rounded bg-brand-orange/20" />
<div className="h-4 w-20 rounded bg-brand-orange/20 pt-2" />
</div>
) : subscription ? (
<>
<p className="text-lg font-bold text-brand-orange">
{(() => {
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`
})()}
</p>
{(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'))) && (
<p className="text-sm text-neutral-400 mt-1">
{typeof subscription.sites_count === 'number' && (
<span>Sites: {(() => {
const limit = getSitesLimitForPlan(subscription.plan_id)
return limit != null && typeof subscription.sites_count === 'number' ? `${subscription.sites_count}/${limit}` : subscription.sites_count
})()}</span>
)}
{typeof subscription.sites_count === 'number' && (subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number') && ' · '}
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number' && (
<span>Pageviews: {subscription.pageview_usage.toLocaleString()}/{subscription.pageview_limit.toLocaleString()}</span>
)}
{!subscription.cancel_at_period_end && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && subscription.current_period_end && (
<span className="block mt-1">
Renews {formatDate(new Date(subscription.current_period_end))}
</span>
)}
</p>
)}
<div className="mt-2 flex gap-2">
{subscription.has_payment_method ? (
<button onClick={() => openUnifiedSettings({ context: 'workspace', tab: 'billing' })} className="text-sm font-medium text-brand-orange hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:rounded cursor-pointer">
Manage billing
</button>
) : (
<Link href="/pricing" className="text-sm font-medium text-brand-orange hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:rounded">
Upgrade
</Link>
)}
</div>
</>
) : (
<p className="text-lg font-bold text-brand-orange">Free Plan</p>
)}
</div>
</div>
{!sitesLoading && sites.length === 0 && (
<div className="mb-8 rounded-2xl border-2 border-dashed border-brand-orange/30 bg-brand-orange/10 p-8 text-center flex flex-col items-center">
<img
@@ -453,5 +374,6 @@ export default function HomePage() {
</div>
)}
</div>
</DashboardShell>
)
}

View File

@@ -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<number | null>(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 (
<div className="hidden md:flex items-center justify-between shrink-0 px-3 pt-1.5 pb-1">
@@ -74,7 +75,7 @@ function GlassTopBar({ siteId }: { siteId: string }) {
</div>
{/* Realtime indicator */}
{lastUpdatedRef.current != null && (
{siteId && lastUpdatedRef.current != null && (
<div className="flex items-center gap-1.5 text-xs text-neutral-500">
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75" />
@@ -91,7 +92,7 @@ export default function DashboardShell({
siteId,
children,
}: {
siteId: string
siteId: string | null
children: React.ReactNode
}) {
const [mobileOpen, setMobileOpen] = useState(false)

View File

@@ -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,9 +414,12 @@ function SidebarContent({
</Link>
{/* Site Picker */}
{siteId && (
<SitePicker sites={sites} siteId={siteId} collapsed={c} onExpand={onExpand} onCollapse={onCollapse} wasCollapsed={wasCollapsed} pickerOpenCallback={pickerOpenCallbackRef} />
)}
{/* Nav Groups */}
{siteId ? (
<nav className="flex-1 overflow-y-auto overflow-x-hidden px-2 space-y-4">
{NAV_GROUPS.map((group) => (
<div key={group.label}>
@@ -440,6 +443,9 @@ function SidebarContent({
</div>
))}
</nav>
) : (
<div className="flex-1" />
)}
{/* Bottom — utility items */}
<div className="border-t border-white/[0.06] px-2 py-3 shrink-0">
@@ -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