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:
@@ -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} />}
|
||||
|
||||
92
app/page.tsx
92
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<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user