'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { motion } from 'framer-motion'
import { useAuth } from '@/lib/auth/context'
import { initiateOAuthFlow, initiateSignupFlow } from '@/lib/api/oauth'
import { listSites, deleteSite, type Site } from '@/lib/api/sites'
import { getStats } from '@/lib/api/stats'
import type { Stats } from '@/lib/api/stats'
import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing'
import { LoadingOverlay } from '@ciphera-net/ui'
import SiteList from '@/components/sites/SiteList'
import { Button } from '@ciphera-net/ui'
import { BarChartIcon, LockIcon, ZapIcon, CheckCircleIcon, XIcon, GlobeIcon } from '@ciphera-net/ui'
import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@ciphera-net/ui'
import { getSitesLimitForPlan } from '@/lib/plans'
const MOCK_STATS = [
{ label: 'UNIQUE VISITORS', value: '12,847', trend: '+14%', up: true },
{ label: 'TOTAL PAGEVIEWS', value: '48,293', trend: '+8%', up: true },
{ label: 'BOUNCE RATE', value: '42%', trend: '-3%', up: true },
{ label: 'VISIT DURATION', value: '2m 34s', trend: '+11%', up: true },
]
const MOCK_CHART_POINTS = [
20, 35, 28, 45, 38, 52, 48, 65, 58, 72, 68, 85, 78, 92, 88, 105,
98, 110, 95, 115, 108, 125, 118, 130, 122, 138, 142, 155, 148, 160,
]
const MOCK_PAGES = [
{ path: '/', views: '8,421' },
{ path: '/pricing', views: '3,287' },
{ path: '/features', views: '2,104' },
{ path: '/blog/getting-started', views: '1,856' },
{ path: '/docs/installation', views: '1,203' },
]
const MOCK_REFERRERS = [
{ name: 'google.com', views: '5,832' },
{ name: 'twitter.com', views: '2,417' },
{ name: 'github.com', views: '1,894' },
{ name: 'reddit.com', views: '1,105' },
{ name: 'Direct / None', views: '987' },
]
function DashboardPreview() {
const chartMax = Math.max(...MOCK_CHART_POINTS)
const chartH = 180
const points = MOCK_CHART_POINTS.map((v, i) => {
const x = (i / (MOCK_CHART_POINTS.length - 1)) * 100
const y = chartH - (v / chartMax) * (chartH - 20)
return `${x},${y}`
}).join(' ')
const areaPoints = `0,${chartH} ${points} 100,${chartH}`
return (
{/* * Browser chrome */}
{/* * Header row */}
{/* * Stat cards */}
{MOCK_STATS.map((s, i) => (
{s.label}
{s.value}
{s.trend}
))}
{/* * Chart area */}
{/* * Two-column widgets */}
{/* * Top Pages */}
Top Pages
{MOCK_PAGES.map((p, i) => (
{p.path}
{p.views}
))}
{/* * Top Referrers */}
Top Referrers
{MOCK_REFERRERS.map((r, i) => (
{r.name}
{r.views}
))}
)
}
function ComparisonSection() {
return (
Why choose Pulse?
The lightweight, privacy-friendly alternative.
Feature
Pulse
Google Analytics
{[
{ feature: "Cookie Banner Required", pulse: false, ga: true },
{ feature: "GDPR Compliant", pulse: true, ga: "Complex" },
{ feature: "Script Size", pulse: "< 1 KB", ga: "45 KB+" },
{ feature: "Data Ownership", pulse: "Yours", ga: "Google's" },
].map((row, i) => (
{row.feature}
{row.pulse === true ? (
) : row.pulse === false ? (
No
) : (
{row.pulse}
)}
{row.ga === true ? (
Yes
) : (
{row.ga}
)}
))}
)
}
type SiteStatsMap = Record
export default function HomePage() {
const { user, loading: authLoading } = useAuth()
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)
useEffect(() => {
if (user?.org_id) {
loadSites()
loadSubscription()
}
}, [user])
useEffect(() => {
if (sites.length === 0) {
setSiteStats({})
return
}
let cancelled = false
const today = new Date().toISOString().split('T')[0]
const emptyStats: Stats = { pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 }
const load = async () => {
const results = await Promise.allSettled(
sites.map(async (site) => {
const statsRes = await getStats(site.id, today, today)
return { siteId: site.id, stats: statsRes }
})
)
if (cancelled) return
const map: SiteStatsMap = {}
results.forEach((r, i) => {
const site = sites[i]
if (r.status === 'fulfilled') {
map[site.id] = { stats: r.value.stats }
} else {
map[site.id] = { stats: emptyStats }
}
})
setSiteStats(map)
}
load()
return () => { cancelled = true }
}, [sites])
useEffect(() => {
if (typeof window === 'undefined') return
if (localStorage.getItem('pulse_welcome_completed') === 'true') setShowFinishSetupBanner(false)
}, [user?.org_id])
useEffect(() => {
if (typeof window === 'undefined') return
const params = new URLSearchParams(window.location.search)
if (params.get('trial_started') === '1') {
toast.success('Your trial is active. You can add sites and start tracking.')
params.delete('trial_started')
const newUrl = params.toString() ? `${window.location.pathname}?${params}` : window.location.pathname
window.history.replaceState({}, '', newUrl)
}
}, [])
const loadSites = async () => {
try {
setSitesLoading(true)
const data = await listSites()
setSites(Array.isArray(data) ? data : [])
} catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to load your sites')
setSites([])
} finally {
setSitesLoading(false)
}
}
const loadSubscription = async () => {
try {
setSubscriptionLoading(true)
const sub = await getSubscription()
setSubscription(sub)
} catch {
setSubscription(null)
} finally {
setSubscriptionLoading(false)
}
}
const handleDelete = async (id: string) => {
if (!confirm('Are you sure you want to delete this site? This action cannot be undone.')) {
return
}
try {
await deleteSite(id)
toast.success('Site deleted successfully')
loadSites()
} catch (error: unknown) {
toast.error(getAuthErrorMessage(error) || 'Failed to delete site')
}
}
if (authLoading) {
return
}
if (!user) {
return (
{/* * --- 1. ATMOSPHERE (Background) --- */}
{/* * Top-left Orange Glow */}
{/* * Bottom-right Neutral Glow */}
{/* * Grid Pattern with Radial Mask */}
{/* * --- 2. BADGE --- */}
Privacy-First Analytics
{/* * --- 3. HEADLINE --- */}
Simple analytics for
privacy-conscious
{/* * SVG Underline from Main Site */}
{' '}apps.
Respect your users' privacy while getting the insights you need.
No cookies, no IP tracking, fully GDPR compliant.
{/* * --- 4. CTAs --- */}
initiateOAuthFlow()} variant="primary" className="px-8 py-4 text-lg shadow-lg shadow-brand-orange/20">
Get Started
initiateSignupFlow()} variant="secondary" className="px-8 py-4 text-lg">
Create Account
{/* * NEW: DASHBOARD PREVIEW */}
{/* * --- 5. GLASS CARDS --- */}
{[
{ icon: LockIcon, title: "Privacy First", desc: "We don't track personal data. No IP addresses, no fingerprints, no cookies." },
{ icon: BarChartIcon, title: "Simple Insights", desc: "Get the metrics that matter without the clutter. Page views, visitors, and sources." },
{ icon: ZapIcon, title: "Lightweight", desc: "Our script is less than 1kb. It won't slow down your site or affect your SEO." }
].map((feature, i) => (
{feature.title}
{feature.desc}
))}
{/* * NEW: COMPARISON SECTION */}
{/* * NEW: CTA BOTTOM */}
Ready to switch?
initiateOAuthFlow()} variant="primary" className="px-8 py-4 text-lg shadow-lg shadow-brand-orange/20">
Start your free trial
No credit card required • Cancel anytime
)
}
// * Wait for organization context before rendering SiteList to avoid "Organization Required" flash
if (user && !user.org_id) {
return
}
return (
{showFinishSetupBanner && (
Finish setting up your workspace and add your first site.
Finish setup
{
if (typeof window !== 'undefined') localStorage.setItem('pulse_welcome_completed', 'true')
setShowFinishSetupBanner(false)
}}
className="text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-400 p-1 rounded"
aria-label="Dismiss"
>
)}
Your Sites
Manage your analytics sites and view insights.
{(() => {
const siteLimit = getSitesLimitForPlan(subscription?.plan_id)
const atLimit = siteLimit != null && sites.length >= siteLimit
return atLimit ? (
Limit reached ({sites.length}/{siteLimit})
Upgrade
) : null
})() ?? (
Add New Site
)}
{/* * 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.next_invoice_amount_due != null && subscription.next_invoice_currency && !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.next_invoice_amount_due != null && subscription.next_invoice_currency && !subscription.cancel_at_period_end && (subscription.subscription_status === 'active' || subscription.subscription_status === 'trialing') && (
Renews {(() => {
const ts = subscription.next_invoice_period_end ?? subscription.current_period_end
const d = ts ? new Date(typeof ts === 'number' ? ts * 1000 : ts) : null
const dateStr = d && !Number.isNaN(d.getTime()) && d.getTime() !== 0
? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
: null
const amount = (subscription.next_invoice_amount_due / 100).toLocaleString('en-US', {
style: 'currency',
currency: subscription.next_invoice_currency.toUpperCase(),
})
return dateStr ? `${dateStr} for ${amount}` : amount
})()}
)}
)}
{subscription.has_payment_method ? (
Manage billing
) : (
Upgrade
)}
>
) : (
Free Plan
)}
{!sitesLoading && sites.length === 0 && (
Add your first site
Connect a domain to start collecting privacy-friendly analytics. You can add more sites later from the dashboard.
Add your first site
)}
{(sitesLoading || sites.length > 0) && (
)}
)
}