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)
|
const showOfflineBar = Boolean(auth.user && !isOnline)
|
||||||
// Site pages use DashboardShell with full sidebar — no Header needed
|
// Site pages use DashboardShell with full sidebar — no Header needed
|
||||||
const isSitePage = pathname.startsWith('/sites/') && pathname !== '/sites/new'
|
const isSitePage = pathname.startsWith('/sites/') && pathname !== '/sites/new'
|
||||||
|
const isHomePage = pathname === '/'
|
||||||
// Checkout page has its own minimal layout — no app header/footer
|
// Checkout page has its own minimal layout — no app header/footer
|
||||||
const isCheckoutPage = pathname.startsWith('/checkout')
|
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
|
// 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticated site pages: full sidebar layout
|
// Authenticated site pages: full sidebar layout
|
||||||
// DashboardShell inside children handles everything
|
// DashboardShell inside children handles everything
|
||||||
if (isAuthenticated && isSitePage) {
|
if (isAuthenticated && (isSitePage || isHomePage)) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showOfflineBar && <OfflineBanner isOnline={isOnline} />}
|
{showOfflineBar && <OfflineBanner isOnline={isOnline} />}
|
||||||
|
|||||||
94
app/page.tsx
94
app/page.tsx
@@ -23,19 +23,16 @@ import PulseFAQ from '@/components/marketing/PulseFAQ'
|
|||||||
import { toast } from '@ciphera-net/ui'
|
import { toast } from '@ciphera-net/ui'
|
||||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||||
import { getSitesLimitForPlan } from '@/lib/plans'
|
import { getSitesLimitForPlan } from '@/lib/plans'
|
||||||
import { formatDate } from '@/lib/utils/formatDate'
|
import DashboardShell from '@/components/dashboard/DashboardShell'
|
||||||
import { useUnifiedSettings } from '@/lib/unified-settings-context'
|
|
||||||
|
|
||||||
type SiteStatsMap = Record<string, { stats: Stats }>
|
type SiteStatsMap = Record<string, { stats: Stats }>
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { user, loading: authLoading } = useAuth()
|
const { user, loading: authLoading } = useAuth()
|
||||||
const { openUnifiedSettings } = useUnifiedSettings()
|
|
||||||
const [sites, setSites] = useState<Site[]>([])
|
const [sites, setSites] = useState<Site[]>([])
|
||||||
const [sitesLoading, setSitesLoading] = useState(true)
|
const [sitesLoading, setSitesLoading] = useState(true)
|
||||||
const [siteStats, setSiteStats] = useState<SiteStatsMap>({})
|
const [siteStats, setSiteStats] = useState<SiteStatsMap>({})
|
||||||
const [subscription, setSubscription] = useState<SubscriptionDetails | null>(null)
|
const [subscription, setSubscription] = useState<SubscriptionDetails | null>(null)
|
||||||
const [subscriptionLoading, setSubscriptionLoading] = useState(false)
|
|
||||||
const [showFinishSetupBanner, setShowFinishSetupBanner] = useState(true)
|
const [showFinishSetupBanner, setShowFinishSetupBanner] = useState(true)
|
||||||
const [deleteModalSite, setDeleteModalSite] = useState<Site | null>(null)
|
const [deleteModalSite, setDeleteModalSite] = useState<Site | null>(null)
|
||||||
const [deletedSites, setDeletedSites] = useState<Site[]>([])
|
const [deletedSites, setDeletedSites] = useState<Site[]>([])
|
||||||
@@ -116,13 +113,10 @@ export default function HomePage() {
|
|||||||
|
|
||||||
const loadSubscription = async () => {
|
const loadSubscription = async () => {
|
||||||
try {
|
try {
|
||||||
setSubscriptionLoading(true)
|
|
||||||
const sub = await getSubscription()
|
const sub = await getSubscription()
|
||||||
setSubscription(sub)
|
setSubscription(sub)
|
||||||
} catch {
|
} catch {
|
||||||
setSubscription(null)
|
setSubscription(null)
|
||||||
} finally {
|
|
||||||
setSubscriptionLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +230,8 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 && (
|
{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">
|
<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">
|
<p className="text-sm text-neutral-300">
|
||||||
@@ -263,10 +258,10 @@ export default function HomePage() {
|
|||||||
</div>
|
</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>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">Your Sites</h1>
|
<h1 className="text-lg font-semibold text-neutral-200 mb-1">Your Sites</h1>
|
||||||
<p className="mt-1 text-sm text-neutral-400">Manage your analytics sites and view insights.</p>
|
<p className="text-sm text-neutral-400">Manage your analytics sites and view insights.</p>
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const siteLimit = getSitesLimitForPlan(subscription?.plan_id)
|
const siteLimit = getSitesLimitForPlan(subscription?.plan_id)
|
||||||
@@ -299,80 +294,6 @@ export default function HomePage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 && (
|
{!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">
|
<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
|
<img
|
||||||
@@ -452,6 +373,7 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 { collapsed, toggle } = useSidebar()
|
||||||
const { data: realtime } = useRealtime(siteId)
|
const { data: realtime } = useRealtime(siteId ?? '')
|
||||||
const lastUpdatedRef = useRef<number | null>(null)
|
const lastUpdatedRef = useRef<number | null>(null)
|
||||||
const [, setTick] = useState(0)
|
const [, setTick] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (realtime) lastUpdatedRef.current = Date.now()
|
if (siteId && realtime) lastUpdatedRef.current = Date.now()
|
||||||
}, [realtime])
|
}, [siteId, realtime])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastUpdatedRef.current == null) return
|
if (lastUpdatedRef.current == null) return
|
||||||
@@ -57,7 +57,8 @@ function GlassTopBar({ siteId }: { siteId: string }) {
|
|||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
}, [realtime])
|
}, [realtime])
|
||||||
|
|
||||||
const pageTitle = usePageTitle()
|
const dashboardTitle = usePageTitle()
|
||||||
|
const pageTitle = siteId ? dashboardTitle : 'Your Sites'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hidden md:flex items-center justify-between shrink-0 px-3 pt-1.5 pb-1">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Realtime indicator */}
|
{/* Realtime indicator */}
|
||||||
{lastUpdatedRef.current != null && (
|
{siteId && lastUpdatedRef.current != null && (
|
||||||
<div className="flex items-center gap-1.5 text-xs text-neutral-500">
|
<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="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" />
|
<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,
|
siteId,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
siteId: string
|
siteId: string | null
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const [mobileOpen, setMobileOpen] = useState(false)
|
const [mobileOpen, setMobileOpen] = useState(false)
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ function SettingsButton({
|
|||||||
interface SidebarContentProps {
|
interface SidebarContentProps {
|
||||||
isMobile: boolean
|
isMobile: boolean
|
||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
siteId: string
|
siteId: string | null
|
||||||
sites: Site[]
|
sites: Site[]
|
||||||
canEdit: boolean
|
canEdit: boolean
|
||||||
pendingHref: string | null
|
pendingHref: string | null
|
||||||
@@ -414,32 +414,38 @@ function SidebarContent({
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Site Picker */}
|
{/* Site Picker */}
|
||||||
<SitePicker sites={sites} siteId={siteId} collapsed={c} onExpand={onExpand} onCollapse={onCollapse} wasCollapsed={wasCollapsed} pickerOpenCallback={pickerOpenCallbackRef} />
|
{siteId && (
|
||||||
|
<SitePicker sites={sites} siteId={siteId} collapsed={c} onExpand={onExpand} onCollapse={onCollapse} wasCollapsed={wasCollapsed} pickerOpenCallback={pickerOpenCallbackRef} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Nav Groups */}
|
{/* Nav Groups */}
|
||||||
<nav className="flex-1 overflow-y-auto overflow-x-hidden px-2 space-y-4">
|
{siteId ? (
|
||||||
{NAV_GROUPS.map((group) => (
|
<nav className="flex-1 overflow-y-auto overflow-x-hidden px-2 space-y-4">
|
||||||
<div key={group.label}>
|
{NAV_GROUPS.map((group) => (
|
||||||
{c ? (
|
<div key={group.label}>
|
||||||
<div className="mx-3 my-2 border-t border-white/[0.04]" />
|
{c ? (
|
||||||
) : (
|
<div className="mx-3 my-2 border-t border-white/[0.04]" />
|
||||||
<div className="h-5 flex items-center overflow-hidden">
|
) : (
|
||||||
<p className="px-2.5 text-[11px] font-semibold text-neutral-400 dark:text-neutral-500 uppercase tracking-wider whitespace-nowrap">
|
<div className="h-5 flex items-center overflow-hidden">
|
||||||
{group.label}
|
<p className="px-2.5 text-[11px] font-semibold text-neutral-400 dark:text-neutral-500 uppercase tracking-wider whitespace-nowrap">
|
||||||
</p>
|
{group.label}
|
||||||
</div>
|
</p>
|
||||||
)}
|
</div>
|
||||||
<div className="space-y-0.5">
|
|
||||||
{group.items.map((item) => (
|
|
||||||
<NavLink key={item.label} item={item} siteId={siteId} collapsed={c} onClick={isMobile ? onMobileClose : undefined} pendingHref={pendingHref} onNavigate={onNavigate} />
|
|
||||||
))}
|
|
||||||
{group.label === 'Infrastructure' && canEdit && (
|
|
||||||
<SettingsButton item={SETTINGS_ITEM} collapsed={c} onClick={isMobile ? onMobileClose : undefined} />
|
|
||||||
)}
|
)}
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{group.items.map((item) => (
|
||||||
|
<NavLink key={item.label} item={item} siteId={siteId} collapsed={c} onClick={isMobile ? onMobileClose : undefined} pendingHref={pendingHref} onNavigate={onNavigate} />
|
||||||
|
))}
|
||||||
|
{group.label === 'Infrastructure' && canEdit && (
|
||||||
|
<SettingsButton item={SETTINGS_ITEM} collapsed={c} onClick={isMobile ? onMobileClose : undefined} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</nav>
|
||||||
</nav>
|
) : (
|
||||||
|
<div className="flex-1" />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Bottom — utility items */}
|
{/* Bottom — utility items */}
|
||||||
<div className="border-t border-white/[0.06] px-2 py-3 shrink-0">
|
<div className="border-t border-white/[0.06] px-2 py-3 shrink-0">
|
||||||
@@ -488,7 +494,7 @@ function SidebarContent({
|
|||||||
export default function Sidebar({
|
export default function Sidebar({
|
||||||
siteId, mobileOpen, onMobileClose, onMobileOpen,
|
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 auth = useAuth()
|
||||||
const { user } = auth
|
const { user } = auth
|
||||||
|
|||||||
Reference in New Issue
Block a user