fix: frontend consistency audit — 55 files cleaned up
Consistency fixes: - Extract getThisWeekRange/getThisMonthRange to shared lib/utils/dateRanges.ts (removed 4 identical copy-pasted definitions) - Add error boundaries for behavior, cdn, search, pagespeed pages (4 new error.tsx files — previously fell through to generic parent error) - Add "View setup guide" CTA to empty states on journeys and behavior pages (previously showed text with no actionable button) - Fix non-lazy useState initializer in funnel detail page - Fix Bot & Spam settings header from text-xl to text-2xl (matches all other sections) - Add useMinimumLoading to PageSpeed skeleton (consistent with all other pages) Cleanup: - Remove 438 redundant dark: class prefixes (app is dark-mode only) text-neutral-500 dark:text-neutral-400 → text-neutral-400 (206 occurrences) text-neutral-900 dark:text-white → text-white (232 occurrences) - Remove dead @stripe/react-stripe-js and @stripe/stripe-js packages (billing migrated to Polar, no code imports Stripe) - Remove duplicate motion package (framer-motion is the one actually used)
This commit is contained in:
@@ -28,8 +28,8 @@ export default function FilteredTrafficPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white">Filtered Traffic</h2>
|
<h2 className="text-xl font-semibold text-white">Filtered Traffic</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">
|
<p className="text-sm text-neutral-400 mt-1">
|
||||||
{totalBlocked.toLocaleString()} spam referrers blocked in the last {days} days
|
{totalBlocked.toLocaleString()} spam referrers blocked in the last {days} days
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,22 +52,22 @@ export default function FilteredTrafficPage() {
|
|||||||
|
|
||||||
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 shadow-sm overflow-hidden">
|
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 shadow-sm overflow-hidden">
|
||||||
{referrers.length === 0 ? (
|
{referrers.length === 0 ? (
|
||||||
<div className="p-12 text-center text-neutral-500 dark:text-neutral-400">
|
<div className="p-12 text-center text-neutral-400">
|
||||||
No filtered referrers in this period
|
No filtered referrers in this period
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="border-b border-neutral-200 dark:border-neutral-800">
|
<thead className="border-b border-neutral-200 dark:border-neutral-800">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Domain</th>
|
<th className="px-4 py-3 font-medium text-neutral-400">Domain</th>
|
||||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Reason</th>
|
<th className="px-4 py-3 font-medium text-neutral-400">Reason</th>
|
||||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400 text-right">Blocked</th>
|
<th className="px-4 py-3 font-medium text-neutral-400 text-right">Blocked</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-800">
|
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-800">
|
||||||
{referrers.map((r) => (
|
{referrers.map((r) => (
|
||||||
<tr key={`${r.domain}-${r.reason}`} className="hover:bg-neutral-50 dark:hover:bg-neutral-900/50">
|
<tr key={`${r.domain}-${r.reason}`} className="hover:bg-neutral-50 dark:hover:bg-neutral-900/50">
|
||||||
<td className="px-4 py-3 text-neutral-900 dark:text-white font-mono text-xs">{r.domain}</td>
|
<td className="px-4 py-3 text-white font-mono text-xs">{r.domain}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||||
r.reason === 'blocklist'
|
r.reason === 'blocklist'
|
||||||
@@ -77,7 +77,7 @@ export default function FilteredTrafficPage() {
|
|||||||
{r.reason}
|
{r.reason}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-right text-neutral-900 dark:text-white tabular-nums">
|
<td className="px-4 py-3 text-right text-white tabular-nums">
|
||||||
{r.count.toLocaleString()}
|
{r.count.toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
|||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
||||||
<div className="mb-8 flex items-center justify-between">
|
<div className="mb-8 flex items-center justify-between">
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">Pulse Admin</h1>
|
<h1 className="text-2xl font-bold text-white">Pulse Admin</h1>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function AdminOrgDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-4xl mx-auto">
|
<div className="space-y-6 max-w-4xl mx-auto">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
<h2 className="text-2xl font-bold text-white">
|
||||||
{org.business_name || 'Unnamed Organization'}
|
{org.business_name || 'Unnamed Organization'}
|
||||||
</h2>
|
</h2>
|
||||||
<span className="text-sm font-mono text-neutral-500">{org.organization_id}</span>
|
<span className="text-sm font-mono text-neutral-500">{org.organization_id}</span>
|
||||||
@@ -116,7 +116,7 @@ export default function AdminOrgDetailPage() {
|
|||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
{/* Current Status */}
|
{/* Current Status */}
|
||||||
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm">
|
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-4">Current Status</h3>
|
<h3 className="text-lg font-semibold text-white mb-4">Current Status</h3>
|
||||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
<span className="text-neutral-500">Plan:</span>
|
<span className="text-neutral-500">Plan:</span>
|
||||||
<span className="font-medium">{org.plan_id}</span>
|
<span className="font-medium">{org.plan_id}</span>
|
||||||
@@ -145,7 +145,7 @@ export default function AdminOrgDetailPage() {
|
|||||||
|
|
||||||
{/* Sites */}
|
{/* Sites */}
|
||||||
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm">
|
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-4">Sites ({org.sites.length})</h3>
|
<h3 className="text-lg font-semibold text-white mb-4">Sites ({org.sites.length})</h3>
|
||||||
<ul className="space-y-2 max-h-60 overflow-y-auto">
|
<ul className="space-y-2 max-h-60 overflow-y-auto">
|
||||||
{org.sites.map((site) => (
|
{org.sites.map((site) => (
|
||||||
<li key={site.id} className="flex justify-between items-center text-sm p-2 bg-neutral-50 dark:bg-neutral-900 rounded">
|
<li key={site.id} className="flex justify-between items-center text-sm p-2 bg-neutral-50 dark:bg-neutral-900 rounded">
|
||||||
@@ -160,7 +160,7 @@ export default function AdminOrgDetailPage() {
|
|||||||
|
|
||||||
{/* Grant Plan Form */}
|
{/* Grant Plan Form */}
|
||||||
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm">
|
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-4">Grant Plan (Manual Override)</h3>
|
<h3 className="text-lg font-semibold text-white mb-4">Grant Plan (Manual Override)</h3>
|
||||||
<form onSubmit={handleGrantPlan} className="space-y-4">
|
<form onSubmit={handleGrantPlan} className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -196,7 +196,7 @@ export default function AdminOrgDetailPage() {
|
|||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={periodEnd}
|
value={periodEnd}
|
||||||
onChange={(e) => setPeriodEnd(e.target.value)}
|
onChange={(e) => setPeriodEnd(e.target.value)}
|
||||||
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2"
|
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-white focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2 mt-1">
|
<div className="flex gap-2 mt-1">
|
||||||
|
|||||||
@@ -43,28 +43,28 @@ export default function AdminOrgsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white">Organizations</h2>
|
<h2 className="text-xl font-semibold text-white">Organizations</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm">
|
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-4">All Organizations</h3>
|
<h3 className="text-lg font-semibold text-white mb-4">All Organizations</h3>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="border-b border-neutral-200 dark:border-neutral-800">
|
<thead className="border-b border-neutral-200 dark:border-neutral-800">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Name</th>
|
<th className="px-4 py-3 font-medium text-neutral-400">Name</th>
|
||||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Org ID</th>
|
<th className="px-4 py-3 font-medium text-neutral-400">Org ID</th>
|
||||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Plan</th>
|
<th className="px-4 py-3 font-medium text-neutral-400">Plan</th>
|
||||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Status</th>
|
<th className="px-4 py-3 font-medium text-neutral-400">Status</th>
|
||||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Limit</th>
|
<th className="px-4 py-3 font-medium text-neutral-400">Limit</th>
|
||||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Updated</th>
|
<th className="px-4 py-3 font-medium text-neutral-400">Updated</th>
|
||||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Actions</th>
|
<th className="px-4 py-3 font-medium text-neutral-400">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-800">
|
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-800">
|
||||||
{orgs.map((org) => (
|
{orgs.map((org) => (
|
||||||
<tr key={org.organization_id} className="hover:bg-neutral-50 dark:hover:bg-neutral-900/50">
|
<tr key={org.organization_id} className="hover:bg-neutral-50 dark:hover:bg-neutral-900/50">
|
||||||
<td className="px-4 py-3 text-neutral-900 dark:text-white font-medium">
|
<td className="px-4 py-3 text-white font-medium">
|
||||||
{org.business_name || 'N/A'}
|
{org.business_name || 'N/A'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ export default function AdminDashboard() {
|
|||||||
href="/admin/orgs"
|
href="/admin/orgs"
|
||||||
className="block transition-transform hover:scale-[1.02] rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm"
|
className="block transition-transform hover:scale-[1.02] rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm"
|
||||||
>
|
>
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">Organizations</h3>
|
<h3 className="text-lg font-semibold text-white">Organizations</h3>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">Manage organization plans and limits</p>
|
<p className="text-sm text-neutral-400 mt-1">Manage organization plans and limits</p>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-4">
|
<p className="text-sm text-neutral-400 mt-4">
|
||||||
View all organizations, check billing status, and manually grant plans.
|
View all organizations, check billing status, and manually grant plans.
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -19,9 +19,9 @@ export default function AdminDashboard() {
|
|||||||
href="/admin/filtered-traffic"
|
href="/admin/filtered-traffic"
|
||||||
className="block transition-transform hover:scale-[1.02] rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm"
|
className="block transition-transform hover:scale-[1.02] rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm"
|
||||||
>
|
>
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">Filtered Traffic</h3>
|
<h3 className="text-lg font-semibold text-white">Filtered Traffic</h3>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">Monitor blocked referrer spam</p>
|
<p className="text-sm text-neutral-400 mt-1">Monitor blocked referrer spam</p>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-4">
|
<p className="text-sm text-neutral-400 mt-4">
|
||||||
View domains blocked by the spam filter and check for false positives.
|
View domains blocked by the spam filter and check for false positives.
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function ChangelogPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl px-4 sm:px-6 py-8">
|
<div className="mx-auto max-w-3xl px-4 sm:px-6 py-8">
|
||||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-neutral-900 dark:text-white mb-2">
|
<h1 className="text-4xl md:text-5xl font-bold tracking-tight text-white mb-2">
|
||||||
Changelog
|
Changelog
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-neutral-600 dark:text-neutral-400 mb-8 text-sm">
|
<p className="text-neutral-600 dark:text-neutral-400 mb-8 text-sm">
|
||||||
|
|||||||
@@ -122,8 +122,8 @@ export default function NotificationsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">Notifications</h1>
|
<h1 className="text-2xl font-bold text-white mb-2">Notifications</h1>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-6">
|
<p className="text-sm text-neutral-400 mb-6">
|
||||||
Manage which notifications you receive in{' '}
|
Manage which notifications you receive in{' '}
|
||||||
<Link href="/org-settings?tab=notifications" className="text-brand-orange hover:underline">
|
<Link href="/org-settings?tab=notifications" className="text-brand-orange hover:underline">
|
||||||
Organization Settings → Notifications
|
Organization Settings → Notifications
|
||||||
@@ -137,7 +137,7 @@ export default function NotificationsPage() {
|
|||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
) : notifications.length === 0 ? (
|
) : notifications.length === 0 ? (
|
||||||
<div className="p-6 text-center text-neutral-500 dark:text-neutral-400 rounded-2xl border border-neutral-200 dark:border-neutral-800">
|
<div className="p-6 text-center text-neutral-400 rounded-2xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<p>No notifications yet</p>
|
<p>No notifications yet</p>
|
||||||
<p className="text-sm mt-2">
|
<p className="text-sm mt-2">
|
||||||
Manage which notifications you receive in{' '}
|
Manage which notifications you receive in{' '}
|
||||||
@@ -159,11 +159,11 @@ export default function NotificationsPage() {
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{getTypeIcon(n.type)}
|
{getTypeIcon(n.type)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className={`text-sm ${!n.read ? 'font-medium' : ''} text-neutral-900 dark:text-white`}>
|
<p className={`text-sm ${!n.read ? 'font-medium' : ''} text-white`}>
|
||||||
{n.title}
|
{n.title}
|
||||||
</p>
|
</p>
|
||||||
{n.body && (
|
{n.body && (
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">{n.body}</p>
|
<p className="text-xs text-neutral-400 mt-0.5">{n.body}</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
|
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
|
||||||
{formatTimeAgo(n.created_at)}
|
{formatTimeAgo(n.created_at)}
|
||||||
@@ -182,11 +182,11 @@ export default function NotificationsPage() {
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{getTypeIcon(n.type)}
|
{getTypeIcon(n.type)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className={`text-sm ${!n.read ? 'font-medium' : ''} text-neutral-900 dark:text-white`}>
|
<p className={`text-sm ${!n.read ? 'font-medium' : ''} text-white`}>
|
||||||
{n.title}
|
{n.title}
|
||||||
</p>
|
</p>
|
||||||
{n.body && (
|
{n.body && (
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">{n.body}</p>
|
<p className="text-xs text-neutral-400 mt-0.5">{n.body}</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
|
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
|
||||||
{formatTimeAgo(n.created_at)}
|
{formatTimeAgo(n.created_at)}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function OnboardingPage() {
|
|||||||
<div className="min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-900 px-4">
|
<div className="min-h-screen flex items-center justify-center bg-neutral-50 dark:bg-neutral-900 px-4">
|
||||||
<div className="max-w-md w-full space-y-8">
|
<div className="max-w-md w-full space-y-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="mt-6 text-2xl font-bold text-neutral-900 dark:text-white">
|
<h2 className="mt-6 text-2xl font-bold text-white">
|
||||||
Welcome to Pulse
|
Welcome to Pulse
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ export default function PublicDashboardPage() {
|
|||||||
<div className="w-12 h-12 bg-brand-orange/10 rounded-xl flex items-center justify-center mx-auto mb-4 text-brand-orange">
|
<div className="w-12 h-12 bg-brand-orange/10 rounded-xl flex items-center justify-center mx-auto mb-4 text-brand-orange">
|
||||||
<ZapIcon className="w-6 h-6" />
|
<ZapIcon className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
<h1 className="text-2xl font-bold text-white mb-2">
|
||||||
Protected Dashboard
|
Protected Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
<p className="text-neutral-600 dark:text-neutral-400">
|
||||||
@@ -210,7 +210,7 @@ export default function PublicDashboardPage() {
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Enter password"
|
placeholder="Enter password"
|
||||||
className="w-full px-4 py-2 border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white focus:ring-2 focus:ring-brand-orange focus:border-transparent"
|
className="w-full px-4 py-2 border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-white focus:ring-2 focus:ring-brand-orange focus:border-transparent"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -270,7 +270,7 @@ export default function PublicDashboardPage() {
|
|||||||
<div className="w-2 h-2 rounded-full bg-brand-orange animate-pulse" />
|
<div className="w-2 h-2 rounded-full bg-brand-orange animate-pulse" />
|
||||||
<span className="text-sm font-medium text-brand-orange uppercase tracking-wider">Public Dashboard</span>
|
<span className="text-sm font-medium text-brand-orange uppercase tracking-wider">Public Dashboard</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white flex items-center gap-3">
|
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
|
||||||
<Image
|
<Image
|
||||||
src={`${FAVICON_SERVICE_URL}?domain=${site.domain}&sz=64`}
|
src={`${FAVICON_SERVICE_URL}?domain=${site.domain}&sz=64`}
|
||||||
alt={site.name}
|
alt={site.name}
|
||||||
|
|||||||
13
app/sites/[id]/behavior/error.tsx
Normal file
13
app/sites/[id]/behavior/error.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import ErrorDisplay from '@/components/ErrorDisplay'
|
||||||
|
|
||||||
|
export default function BehaviorError({ reset }: { error: Error; reset: () => void }) {
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
title="Behavior data failed to load"
|
||||||
|
message="We couldn't load the frustration signals. This might be a temporary issue — try again."
|
||||||
|
onRetry={reset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { getDateRange, formatDate } from '@ciphera-net/ui'
|
import { getDateRange, formatDate, getThisWeekRange, getThisMonthRange } from '@/lib/utils/dateRanges'
|
||||||
import { Select, DatePicker } from '@ciphera-net/ui'
|
import { Select, DatePicker } from '@ciphera-net/ui'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { getRageClicks, getDeadClicks } from '@/lib/api/stats'
|
import { getRageClicks, getDeadClicks } from '@/lib/api/stats'
|
||||||
@@ -15,20 +15,6 @@ import { BehaviorSkeleton, useMinimumLoading, useSkeletonFade } from '@/componen
|
|||||||
|
|
||||||
const ScrollDepth = dynamic(() => import('@/components/dashboard/ScrollDepth'))
|
const ScrollDepth = dynamic(() => import('@/components/dashboard/ScrollDepth'))
|
||||||
|
|
||||||
function getThisWeekRange(): { start: string; end: string } {
|
|
||||||
const today = new Date()
|
|
||||||
const dayOfWeek = today.getDay()
|
|
||||||
const monday = new Date(today)
|
|
||||||
monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1))
|
|
||||||
return { start: formatDate(monday), end: formatDate(today) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThisMonthRange(): { start: string; end: string } {
|
|
||||||
const today = new Date()
|
|
||||||
const firstOfMonth = new Date(today.getFullYear(), today.getMonth(), 1)
|
|
||||||
return { start: formatDate(firstOfMonth), end: formatDate(today) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BehaviorPage() {
|
export default function BehaviorPage() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const siteId = params.id as string
|
const siteId = params.id as string
|
||||||
@@ -74,10 +60,10 @@ export default function BehaviorPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">
|
<h1 className="text-2xl font-bold text-white mb-1">
|
||||||
Behavior
|
Behavior
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Frustration signals and user engagement patterns
|
Frustration signals and user engagement patterns
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
13
app/sites/[id]/cdn/error.tsx
Normal file
13
app/sites/[id]/cdn/error.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import ErrorDisplay from '@/components/ErrorDisplay'
|
||||||
|
|
||||||
|
export default function CDNError({ reset }: { error: Error; reset: () => void }) {
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
title="CDN data failed to load"
|
||||||
|
message="We couldn't load the BunnyCDN data. This might be a temporary issue — try again."
|
||||||
|
onRetry={reset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -177,10 +177,10 @@ export default function CDNPage() {
|
|||||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-5 mb-6">
|
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-5 mb-6">
|
||||||
<CloudArrowUp size={40} className="text-neutral-400 dark:text-neutral-500" />
|
<CloudArrowUp size={40} className="text-neutral-400 dark:text-neutral-500" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-2">
|
<h2 className="text-xl font-semibold text-white mb-2">
|
||||||
Connect BunnyCDN
|
Connect BunnyCDN
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-md mb-6">
|
<p className="text-sm text-neutral-400 max-w-md mb-6">
|
||||||
Monitor your CDN performance including bandwidth usage, cache hit rates, request volumes, and geographic distribution.
|
Monitor your CDN performance including bandwidth usage, cache hit rates, request volumes, and geographic distribution.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
@@ -212,10 +212,10 @@ export default function CDNPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">
|
<h1 className="text-2xl font-bold text-white mb-1">
|
||||||
CDN Analytics
|
CDN Analytics
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
BunnyCDN performance, bandwidth, and cache metrics
|
BunnyCDN performance, bandwidth, and cache metrics
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,7 +281,7 @@ export default function CDNPage() {
|
|||||||
|
|
||||||
{/* Bandwidth chart */}
|
{/* Bandwidth chart */}
|
||||||
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 mb-6">
|
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 mb-6">
|
||||||
<h2 className="text-sm font-semibold text-neutral-900 dark:text-white mb-4">Bandwidth</h2>
|
<h2 className="text-sm font-semibold text-white mb-4">Bandwidth</h2>
|
||||||
{daily.length > 0 ? (
|
{daily.length > 0 ? (
|
||||||
<ResponsiveContainer width="100%" height={280}>
|
<ResponsiveContainer width="100%" height={280}>
|
||||||
<AreaChart data={daily} margin={{ top: 4, right: 4, bottom: 0, left: 0 }}>
|
<AreaChart data={daily} margin={{ top: 4, right: 4, bottom: 0, left: 0 }}>
|
||||||
@@ -317,8 +317,8 @@ export default function CDNPage() {
|
|||||||
if (!active || !payload?.length) return null
|
if (!active || !payload?.length) return null
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 shadow-lg text-sm">
|
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 shadow-lg text-sm">
|
||||||
<p className="text-neutral-500 dark:text-neutral-400 mb-1">{formatDateShort(label)}</p>
|
<p className="text-neutral-400 mb-1">{formatDateShort(label)}</p>
|
||||||
<p className="text-neutral-900 dark:text-white font-medium">
|
<p className="text-white font-medium">
|
||||||
Total: {formatBytes(payload[0]?.value as number)}
|
Total: {formatBytes(payload[0]?.value as number)}
|
||||||
</p>
|
</p>
|
||||||
{payload[1] && (
|
{payload[1] && (
|
||||||
@@ -359,7 +359,7 @@ export default function CDNPage() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
{/* Requests chart */}
|
{/* Requests chart */}
|
||||||
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6">
|
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6">
|
||||||
<h2 className="text-sm font-semibold text-neutral-900 dark:text-white mb-4">Requests</h2>
|
<h2 className="text-sm font-semibold text-white mb-4">Requests</h2>
|
||||||
{daily.length > 0 ? (
|
{daily.length > 0 ? (
|
||||||
<ResponsiveContainer width="100%" height={220}>
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
<BarChart data={daily} margin={{ top: 4, right: 4, bottom: 0, left: 0 }}>
|
<BarChart data={daily} margin={{ top: 4, right: 4, bottom: 0, left: 0 }}>
|
||||||
@@ -385,8 +385,8 @@ export default function CDNPage() {
|
|||||||
if (!active || !payload?.length) return null
|
if (!active || !payload?.length) return null
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 shadow-lg text-sm">
|
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 shadow-lg text-sm">
|
||||||
<p className="text-neutral-500 dark:text-neutral-400 mb-1">{formatDateShort(label)}</p>
|
<p className="text-neutral-400 mb-1">{formatDateShort(label)}</p>
|
||||||
<p className="text-neutral-900 dark:text-white font-medium">
|
<p className="text-white font-medium">
|
||||||
{formatNumber(payload[0]?.value as number)} requests
|
{formatNumber(payload[0]?.value as number)} requests
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -405,7 +405,7 @@ export default function CDNPage() {
|
|||||||
|
|
||||||
{/* Errors chart */}
|
{/* Errors chart */}
|
||||||
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6">
|
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6">
|
||||||
<h2 className="text-sm font-semibold text-neutral-900 dark:text-white mb-4">Errors</h2>
|
<h2 className="text-sm font-semibold text-white mb-4">Errors</h2>
|
||||||
{daily.length > 0 ? (
|
{daily.length > 0 ? (
|
||||||
<ResponsiveContainer width="100%" height={220}>
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
<BarChart
|
<BarChart
|
||||||
@@ -439,7 +439,7 @@ export default function CDNPage() {
|
|||||||
if (!active || !payload?.length) return null
|
if (!active || !payload?.length) return null
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 shadow-lg text-sm">
|
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 shadow-lg text-sm">
|
||||||
<p className="text-neutral-500 dark:text-neutral-400 mb-1">{formatDateShort(label)}</p>
|
<p className="text-neutral-400 mb-1">{formatDateShort(label)}</p>
|
||||||
{payload.map((entry) => (
|
{payload.map((entry) => (
|
||||||
<p key={entry.name} style={{ color: entry.color }} className="font-medium">
|
<p key={entry.name} style={{ color: entry.color }} className="font-medium">
|
||||||
{entry.name}: {formatNumber(entry.value as number)}
|
{entry.name}: {formatNumber(entry.value as number)}
|
||||||
@@ -464,7 +464,7 @@ export default function CDNPage() {
|
|||||||
|
|
||||||
{/* Traffic Distribution */}
|
{/* Traffic Distribution */}
|
||||||
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6">
|
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6">
|
||||||
<h2 className="text-sm font-semibold text-neutral-900 dark:text-white mb-4">Traffic Distribution</h2>
|
<h2 className="text-sm font-semibold text-white mb-4">Traffic Distribution</h2>
|
||||||
{countries.length > 0 ? (
|
{countries.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="h-[360px] mb-8">
|
<div className="h-[360px] mb-8">
|
||||||
@@ -480,9 +480,9 @@ export default function CDNPage() {
|
|||||||
<div className="flex items-center gap-2.5 mb-2">
|
<div className="flex items-center gap-2.5 mb-2">
|
||||||
{cc && getFlagIcon(cc)}
|
{cc && getFlagIcon(cc)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<span className="text-sm font-medium text-neutral-900 dark:text-white truncate block">{city}</span>
|
<span className="text-sm font-medium text-white truncate block">{city}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm tabular-nums text-neutral-500 dark:text-neutral-400 shrink-0">
|
<span className="text-sm tabular-nums text-neutral-400 shrink-0">
|
||||||
{formatBytes(row.bandwidth)}
|
{formatBytes(row.bandwidth)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -530,13 +530,13 @@ function OverviewCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
|
<div className="p-4 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
|
||||||
<p className="text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-1">{label}</p>
|
<p className="text-xs font-medium text-neutral-400 mb-1">{label}</p>
|
||||||
<p className="text-2xl font-bold text-neutral-900 dark:text-white">{value}</p>
|
<p className="text-2xl font-bold text-white">{value}</p>
|
||||||
{changeLabel && (
|
{changeLabel && (
|
||||||
<p className={`text-xs mt-1 font-medium ${
|
<p className={`text-xs mt-1 font-medium ${
|
||||||
isGood ? 'text-green-600 dark:text-green-400' :
|
isGood ? 'text-green-600 dark:text-green-400' :
|
||||||
isBad ? 'text-red-600 dark:text-red-400' :
|
isBad ? 'text-red-600 dark:text-red-400' :
|
||||||
'text-neutral-500 dark:text-neutral-400'
|
'text-neutral-400'
|
||||||
}`}>
|
}`}>
|
||||||
{changeLabel} vs previous period
|
{changeLabel} vs previous period
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function FunnelReportPage() {
|
|||||||
const [funnel, setFunnel] = useState<Funnel | null>(null)
|
const [funnel, setFunnel] = useState<Funnel | null>(null)
|
||||||
const [stats, setStats] = useState<FunnelStats | null>(null)
|
const [stats, setStats] = useState<FunnelStats | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [dateRange, setDateRange] = useState(getDateRange(30))
|
const [dateRange, setDateRange] = useState(() => getDateRange(30))
|
||||||
const [datePreset, setDatePreset] = useState<'7' | '30' | 'custom'>('30')
|
const [datePreset, setDatePreset] = useState<'7' | '30' | 'custom'>('30')
|
||||||
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false)
|
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false)
|
||||||
const [loadError, setLoadError] = useState<'not_found' | 'forbidden' | 'error' | null>(null)
|
const [loadError, setLoadError] = useState<'not_found' | 'forbidden' | 'error' | null>(null)
|
||||||
@@ -154,7 +154,7 @@ export default function FunnelReportPage() {
|
|||||||
<ChevronLeftIcon className="w-5 h-5" />
|
<ChevronLeftIcon className="w-5 h-5" />
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-2xl font-bold text-white">
|
||||||
{funnel.name}
|
{funnel.name}
|
||||||
</h1>
|
</h1>
|
||||||
{funnel.description && (
|
{funnel.description && (
|
||||||
@@ -236,7 +236,7 @@ export default function FunnelReportPage() {
|
|||||||
{trends && trends.dates.length > 1 && (
|
{trends && trends.dates.length > 1 && (
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden shadow-sm p-6 mb-8">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden shadow-sm p-6 mb-8">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-white">
|
||||||
Conversion Trends
|
Conversion Trends
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -322,10 +322,10 @@ export default function FunnelReportPage() {
|
|||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead className="bg-neutral-50 dark:bg-neutral-800/50 border-b border-neutral-200 dark:border-neutral-800">
|
<thead className="bg-neutral-50 dark:bg-neutral-800/50 border-b border-neutral-200 dark:border-neutral-800">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-4 font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Step</th>
|
<th className="px-6 py-4 font-medium text-neutral-400 uppercase tracking-wider">Step</th>
|
||||||
<th className="px-6 py-4 font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider text-right">Visitors</th>
|
<th className="px-6 py-4 font-medium text-neutral-400 uppercase tracking-wider text-right">Visitors</th>
|
||||||
<th className="px-6 py-4 font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider text-right">Drop-off</th>
|
<th className="px-6 py-4 font-medium text-neutral-400 uppercase tracking-wider text-right">Drop-off</th>
|
||||||
<th className="px-6 py-4 font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider text-right">Conversion</th>
|
<th className="px-6 py-4 font-medium text-neutral-400 uppercase tracking-wider text-right">Conversion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-800">
|
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-800">
|
||||||
@@ -338,13 +338,13 @@ export default function FunnelReportPage() {
|
|||||||
{i + 1}
|
{i + 1}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-neutral-900 dark:text-white">{step.step.name}</p>
|
<p className="font-medium text-white">{step.step.name}</p>
|
||||||
<p className="text-neutral-500 dark:text-neutral-400 text-xs font-mono mt-0.5">{step.step.value}</p>
|
<p className="text-neutral-400 text-xs font-mono mt-0.5">{step.step.value}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-right">
|
<td className="px-6 py-4 text-right">
|
||||||
<span className="font-medium text-neutral-900 dark:text-white">
|
<span className="font-medium text-white">
|
||||||
{step.visitors.toLocaleString()}
|
{step.visitors.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default function FunnelsPage() {
|
|||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-2xl font-bold text-white">
|
||||||
Funnels
|
Funnels
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
<p className="text-neutral-600 dark:text-neutral-400">
|
||||||
@@ -65,7 +65,7 @@ export default function FunnelsPage() {
|
|||||||
className="mb-6"
|
className="mb-6"
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-2">
|
<h3 className="text-lg font-semibold text-white mb-2">
|
||||||
No funnels yet
|
No funnels yet
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-neutral-600 dark:text-neutral-400 mb-6 max-w-md mx-auto">
|
<p className="text-neutral-600 dark:text-neutral-400 mb-6 max-w-md mx-auto">
|
||||||
@@ -89,7 +89,7 @@ export default function FunnelsPage() {
|
|||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 hover:border-brand-orange/50 transition-colors">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 hover:border-brand-orange/50 transition-colors">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-neutral-900 dark:text-white group-hover:text-brand-orange transition-colors">
|
<h3 className="text-xl font-bold text-white group-hover:text-brand-orange transition-colors">
|
||||||
{funnel.name}
|
{funnel.name}
|
||||||
</h3>
|
</h3>
|
||||||
{funnel.description && (
|
{funnel.description && (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { getDateRange, formatDate } from '@ciphera-net/ui'
|
import { getDateRange, formatDate, getThisWeekRange, getThisMonthRange } from '@/lib/utils/dateRanges'
|
||||||
import { Select, DatePicker } from '@ciphera-net/ui'
|
import { Select, DatePicker } from '@ciphera-net/ui'
|
||||||
import ColumnJourney from '@/components/journeys/ColumnJourney'
|
import ColumnJourney from '@/components/journeys/ColumnJourney'
|
||||||
import SankeyJourney from '@/components/journeys/SankeyJourney'
|
import SankeyJourney from '@/components/journeys/SankeyJourney'
|
||||||
@@ -18,20 +18,6 @@ import {
|
|||||||
|
|
||||||
const DEFAULT_DEPTH = 4
|
const DEFAULT_DEPTH = 4
|
||||||
|
|
||||||
function getThisWeekRange(): { start: string; end: string } {
|
|
||||||
const today = new Date()
|
|
||||||
const dayOfWeek = today.getDay()
|
|
||||||
const monday = new Date(today)
|
|
||||||
monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1))
|
|
||||||
return { start: formatDate(monday), end: formatDate(today) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThisMonthRange(): { start: string; end: string } {
|
|
||||||
const today = new Date()
|
|
||||||
const firstOfMonth = new Date(today.getFullYear(), today.getMonth(), 1)
|
|
||||||
return { start: formatDate(firstOfMonth), end: formatDate(today) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JourneysPage() {
|
export default function JourneysPage() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const siteId = params.id as string
|
const siteId = params.id as string
|
||||||
@@ -91,10 +77,10 @@ export default function JourneysPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">
|
<h1 className="text-2xl font-bold text-white mb-1">
|
||||||
Journeys
|
Journeys
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
How visitors navigate through your site
|
How visitors navigate through your site
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +129,7 @@ export default function JourneysPage() {
|
|||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-6">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-6">
|
||||||
{/* Depth slider */}
|
{/* Depth slider */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex justify-between text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-3">
|
<div className="flex justify-between text-sm font-medium text-neutral-400 mb-3">
|
||||||
<span>2 steps</span>
|
<span>2 steps</span>
|
||||||
<span className="text-brand-orange font-bold">
|
<span className="text-brand-orange font-bold">
|
||||||
{depth} steps deep
|
{depth} steps deep
|
||||||
@@ -196,7 +182,7 @@ export default function JourneysPage() {
|
|||||||
aria-selected={viewMode === mode}
|
aria-selected={viewMode === mode}
|
||||||
className={`relative px-3 py-1 text-xs font-medium transition-colors capitalize focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded cursor-pointer ${
|
className={`relative px-3 py-1 text-xs font-medium transition-colors capitalize focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded cursor-pointer ${
|
||||||
viewMode === mode
|
viewMode === mode
|
||||||
? 'text-neutral-900 dark:text-white'
|
? 'text-white'
|
||||||
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -232,7 +218,7 @@ export default function JourneysPage() {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
{totalSessions > 0 && (
|
{totalSessions > 0 && (
|
||||||
<div className="px-6 pb-5 text-sm text-neutral-500 dark:text-neutral-400">
|
<div className="px-6 pb-5 text-sm text-neutral-400">
|
||||||
{totalSessions.toLocaleString()} sessions tracked
|
{totalSessions.toLocaleString()} sessions tracked
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
type Stats,
|
type Stats,
|
||||||
type DailyStat,
|
type DailyStat,
|
||||||
} from '@/lib/api/stats'
|
} from '@/lib/api/stats'
|
||||||
import { getDateRange, formatDate } from '@ciphera-net/ui'
|
import { getDateRange, formatDate, getThisWeekRange, getThisMonthRange } from '@/lib/utils/dateRanges'
|
||||||
import { toast } from '@ciphera-net/ui'
|
import { toast } from '@ciphera-net/ui'
|
||||||
import { Button } from '@ciphera-net/ui'
|
import { Button } from '@ciphera-net/ui'
|
||||||
import { Select, DatePicker, DownloadIcon } from '@ciphera-net/ui'
|
import { Select, DatePicker, DownloadIcon } from '@ciphera-net/ui'
|
||||||
@@ -63,19 +63,6 @@ function loadSavedSettings(): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getThisWeekRange(): { start: string; end: string } {
|
|
||||||
const today = new Date()
|
|
||||||
const dayOfWeek = today.getDay()
|
|
||||||
const monday = new Date(today)
|
|
||||||
monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1))
|
|
||||||
return { start: formatDate(monday), end: formatDate(today) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThisMonthRange(): { start: string; end: string } {
|
|
||||||
const today = new Date()
|
|
||||||
const firstOfMonth = new Date(today.getFullYear(), today.getMonth(), 1)
|
|
||||||
return { start: formatDate(firstOfMonth), end: formatDate(today) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitialDateRange(): { start: string; end: string } {
|
function getInitialDateRange(): { start: string; end: string } {
|
||||||
const settings = loadSavedSettings()
|
const settings = loadSavedSettings()
|
||||||
@@ -442,7 +429,7 @@ export default function SiteDashboardPage() {
|
|||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">
|
<h1 className="text-2xl font-bold text-white mb-1">
|
||||||
{site.name}
|
{site.name}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
<p className="text-neutral-600 dark:text-neutral-400">
|
||||||
|
|||||||
13
app/sites/[id]/pagespeed/error.tsx
Normal file
13
app/sites/[id]/pagespeed/error.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import ErrorDisplay from '@/components/ErrorDisplay'
|
||||||
|
|
||||||
|
export default function PageSpeedError({ reset }: { error: Error; reset: () => void }) {
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
title="PageSpeed data failed to load"
|
||||||
|
message="We couldn't load the PageSpeed data. This might be a temporary issue — try again."
|
||||||
|
onRetry={reset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { toast, Button } from '@ciphera-net/ui'
|
|||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import ScoreGauge from '@/components/pagespeed/ScoreGauge'
|
import ScoreGauge from '@/components/pagespeed/ScoreGauge'
|
||||||
import { AreaChart as VisxAreaChart, Area as VisxArea, Grid as VisxGrid, XAxis as VisxXAxis, YAxis as VisxYAxis, ChartTooltip as VisxChartTooltip } from '@/components/ui/area-chart'
|
import { AreaChart as VisxAreaChart, Area as VisxArea, Grid as VisxGrid, XAxis as VisxXAxis, YAxis as VisxYAxis, ChartTooltip as VisxChartTooltip } from '@/components/ui/area-chart'
|
||||||
|
import { useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
|
||||||
|
|
||||||
// * Metric status thresholds (Google's Core Web Vitals thresholds)
|
// * Metric status thresholds (Google's Core Web Vitals thresholds)
|
||||||
function getMetricStatus(metric: string, value: number | null): { label: string; color: string } {
|
function getMetricStatus(metric: string, value: number | null): { label: string; color: string } {
|
||||||
@@ -223,8 +224,10 @@ export default function PageSpeedPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// * Loading state
|
// * Loading state with minimum display time (consistent with other pages)
|
||||||
if (isLoading && !latestChecks) return <PageSpeedSkeleton />
|
const showSkeleton = useMinimumLoading(isLoading && !latestChecks)
|
||||||
|
const fadeClass = useSkeletonFade(showSkeleton)
|
||||||
|
if (showSkeleton) return <PageSpeedSkeleton />
|
||||||
if (!site) return <div className="p-8 text-neutral-500">Site not found</div>
|
if (!site) return <div className="p-8 text-neutral-500">Site not found</div>
|
||||||
|
|
||||||
const enabled = config?.enabled ?? false
|
const enabled = config?.enabled ?? false
|
||||||
@@ -235,10 +238,10 @@ export default function PageSpeedPage() {
|
|||||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
|
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">
|
<h1 className="text-2xl font-bold text-white mb-1">
|
||||||
PageSpeed
|
PageSpeed
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Monitor your site's performance and Core Web Vitals
|
Monitor your site's performance and Core Web Vitals
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,14 +249,14 @@ export default function PageSpeedPage() {
|
|||||||
{/* Empty state */}
|
{/* Empty state */}
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-12 text-center">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-12 text-center">
|
||||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
<svg className="w-8 h-8 text-neutral-500 dark:text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-8 h-8 text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-semibold text-neutral-900 dark:text-white mb-2">
|
<h3 className="font-semibold text-white mb-2">
|
||||||
PageSpeed monitoring is disabled
|
PageSpeed monitoring is disabled
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-6 max-w-md mx-auto">
|
<p className="text-sm text-neutral-400 mb-6 max-w-md mx-auto">
|
||||||
Enable PageSpeed monitoring to track your site's performance scores, Core Web Vitals, and get actionable improvement suggestions.
|
Enable PageSpeed monitoring to track your site's performance scores, Core Web Vitals, and get actionable improvement suggestions.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -263,7 +266,7 @@ export default function PageSpeedPage() {
|
|||||||
<select
|
<select
|
||||||
value={frequency}
|
value={frequency}
|
||||||
onChange={(e) => setFrequency(e.target.value)}
|
onChange={(e) => setFrequency(e.target.value)}
|
||||||
className="text-sm border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-neutral-900 dark:focus:ring-neutral-100"
|
className="text-sm border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-white rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-neutral-900 dark:focus:ring-neutral-100"
|
||||||
>
|
>
|
||||||
<option value="daily">Daily</option>
|
<option value="daily">Daily</option>
|
||||||
<option value="weekly">Weekly</option>
|
<option value="weekly">Weekly</option>
|
||||||
@@ -358,10 +361,10 @@ export default function PageSpeedPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">
|
<h1 className="text-2xl font-bold text-white mb-1">
|
||||||
PageSpeed
|
PageSpeed
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Performance scores and Core Web Vitals for {site.domain}
|
Performance scores and Core Web Vitals for {site.domain}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -443,7 +446,7 @@ export default function PageSpeedPage() {
|
|||||||
|
|
||||||
{/* Check navigator + frequency + legend */}
|
{/* Check navigator + frequency + legend */}
|
||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 mt-6 pt-4 border-t border-neutral-100 dark:border-neutral-800">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 mt-6 pt-4 border-t border-neutral-100 dark:border-neutral-800">
|
||||||
<div className="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
|
<div className="flex items-center gap-2 text-sm text-neutral-400">
|
||||||
{/* Prev/Next arrows */}
|
{/* Prev/Next arrows */}
|
||||||
{checkTimestamps.length > 1 && (
|
{checkTimestamps.length > 1 && (
|
||||||
<button
|
<button
|
||||||
@@ -497,7 +500,7 @@ export default function PageSpeedPage() {
|
|||||||
{/* Filmstrip — page load progression */}
|
{/* Filmstrip — page load progression */}
|
||||||
{currentCheck?.filmstrip && currentCheck.filmstrip.length > 0 && (
|
{currentCheck?.filmstrip && currentCheck.filmstrip.length > 0 && (
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 sm:p-8 mb-6 relative">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 sm:p-8 mb-6 relative">
|
||||||
<h3 className="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-4">
|
<h3 className="text-xs font-semibold text-neutral-400 uppercase tracking-wider mb-4">
|
||||||
Page Load Timeline
|
Page Load Timeline
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center overflow-x-auto gap-1 scrollbar-none">
|
<div className="flex items-center overflow-x-auto gap-1 scrollbar-none">
|
||||||
@@ -521,7 +524,7 @@ export default function PageSpeedPage() {
|
|||||||
|
|
||||||
{/* Section 2 — Metrics Card */}
|
{/* Section 2 — Metrics Card */}
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 sm:p-8 mb-6">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 sm:p-8 mb-6">
|
||||||
<h3 className="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-5">
|
<h3 className="text-xs font-semibold text-neutral-400 uppercase tracking-wider mb-5">
|
||||||
Metrics
|
Metrics
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-6">
|
||||||
@@ -529,10 +532,10 @@ export default function PageSpeedPage() {
|
|||||||
<div key={key} className="flex items-start gap-3">
|
<div key={key} className="flex items-start gap-3">
|
||||||
<span className={`mt-1.5 inline-block w-2.5 h-2.5 rounded-full flex-shrink-0 ${getMetricDotColor(key, value)}`} />
|
<span className={`mt-1.5 inline-block w-2.5 h-2.5 rounded-full flex-shrink-0 ${getMetricDotColor(key, value)}`} />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-neutral-500 dark:text-neutral-400">
|
<div className="text-sm text-neutral-400">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-semibold text-neutral-900 dark:text-white tabular-nums">
|
<div className="text-2xl font-semibold text-white tabular-nums">
|
||||||
{formatMetricValue(key, value)}
|
{formatMetricValue(key, value)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -544,7 +547,7 @@ export default function PageSpeedPage() {
|
|||||||
{/* Section 3 — Score Trend Chart (visx) */}
|
{/* Section 3 — Score Trend Chart (visx) */}
|
||||||
{chartData.length >= 2 && (
|
{chartData.length >= 2 && (
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 sm:p-8 mb-6 overflow-hidden">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 sm:p-8 mb-6 overflow-hidden">
|
||||||
<h3 className="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-4">
|
<h3 className="text-xs font-semibold text-neutral-400 uppercase tracking-wider mb-4">
|
||||||
Performance Score Trend
|
Performance Score Trend
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
@@ -597,10 +600,10 @@ export default function PageSpeedPage() {
|
|||||||
<div className="flex items-center gap-5 mb-6">
|
<div className="flex items-center gap-5 mb-6">
|
||||||
<ScoreGauge score={scoreByGroup[group.key]} label="" size={56} />
|
<ScoreGauge score={scoreByGroup[group.key]} label="" size={56} />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-white">
|
||||||
{group.label}
|
{group.label}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="text-xs text-neutral-400">
|
||||||
{(() => {
|
{(() => {
|
||||||
const realIssues = groupAudits.filter(a => a.score !== null && a.score !== undefined).length
|
const realIssues = groupAudits.filter(a => a.score !== null && a.score !== undefined).length
|
||||||
return realIssues === 0 ? 'No issues found' : `${realIssues} issue${realIssues !== 1 ? 's' : ''} found`
|
return realIssues === 0 ? 'No issues found' : `${realIssues} issue${realIssues !== 1 ? 's' : ''} found`
|
||||||
@@ -615,7 +618,7 @@ export default function PageSpeedPage() {
|
|||||||
|
|
||||||
{groupManual.length > 0 && (
|
{groupManual.length > 0 && (
|
||||||
<details className="mt-4">
|
<details className="mt-4">
|
||||||
<summary className="cursor-pointer text-sm font-medium text-neutral-500 dark:text-neutral-400 select-none hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors">
|
<summary className="cursor-pointer text-sm font-medium text-neutral-400 select-none hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors">
|
||||||
<span className="ml-1">Additional items to manually check ({groupManual.length})</span>
|
<span className="ml-1">Additional items to manually check ({groupManual.length})</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="mt-2 divide-y divide-neutral-100 dark:divide-neutral-800">
|
<div className="mt-2 divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||||
@@ -626,7 +629,7 @@ export default function PageSpeedPage() {
|
|||||||
|
|
||||||
{groupPassed.length > 0 && (
|
{groupPassed.length > 0 && (
|
||||||
<details className="mt-4">
|
<details className="mt-4">
|
||||||
<summary className="cursor-pointer text-sm font-medium text-neutral-500 dark:text-neutral-400 select-none hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors">
|
<summary className="cursor-pointer text-sm font-medium text-neutral-400 select-none hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors">
|
||||||
<span className="ml-1">{groupPassed.length} passed audit{groupPassed.length !== 1 ? 's' : ''}</span>
|
<span className="ml-1">{groupPassed.length} passed audit{groupPassed.length !== 1 ? 's' : ''}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="mt-2 divide-y divide-neutral-100 dark:divide-neutral-800">
|
<div className="mt-2 divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||||
@@ -738,7 +741,7 @@ function AuditRow({ audit }: { audit: AuditSummary }) {
|
|||||||
<details className="group">
|
<details className="group">
|
||||||
<summary className="flex items-center gap-3 py-3 px-2 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-800/50 cursor-pointer list-none">
|
<summary className="flex items-center gap-3 py-3 px-2 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-800/50 cursor-pointer list-none">
|
||||||
<AuditSeverityIcon score={audit.score} />
|
<AuditSeverityIcon score={audit.score} />
|
||||||
<span className="font-medium text-sm text-neutral-900 dark:text-white flex-1 min-w-0 truncate">{audit.title}</span>
|
<span className="font-medium text-sm text-white flex-1 min-w-0 truncate">{audit.title}</span>
|
||||||
{audit.display_value && (
|
{audit.display_value && (
|
||||||
<span className="text-xs text-neutral-500 dark:text-neutral-500 flex-shrink-0 tabular-nums">{audit.display_value}</span>
|
<span className="text-xs text-neutral-500 dark:text-neutral-500 flex-shrink-0 tabular-nums">{audit.display_value}</span>
|
||||||
)}
|
)}
|
||||||
@@ -754,7 +757,7 @@ function AuditRow({ audit }: { audit: AuditSummary }) {
|
|||||||
<div className="pl-8 pr-2 pb-3 pt-1">
|
<div className="pl-8 pr-2 pb-3 pt-1">
|
||||||
{/* Description with parsed markdown links */}
|
{/* Description with parsed markdown links */}
|
||||||
{audit.description && (
|
{audit.description && (
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-3 leading-relaxed">
|
<p className="text-xs text-neutral-400 mb-3 leading-relaxed">
|
||||||
<AuditDescription text={audit.description} />
|
<AuditDescription text={audit.description} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -822,15 +825,15 @@ function AuditItem({ item }: { item: Record<string, any> }) {
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
{label && (
|
{label && (
|
||||||
<div className="font-medium text-neutral-900 dark:text-white text-xs mb-0.5">
|
<div className="font-medium text-white text-xs mb-0.5">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{url && (
|
{url && (
|
||||||
<div className="font-mono text-xs text-neutral-500 dark:text-neutral-400 break-all">{url}</div>
|
<div className="font-mono text-xs text-neutral-400 break-all">{url}</div>
|
||||||
)}
|
)}
|
||||||
{text && (
|
{text && (
|
||||||
<div className="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">{text}</div>
|
<div className="text-xs text-neutral-400 mt-0.5">{text}</div>
|
||||||
)}
|
)}
|
||||||
{item.node?.snippet && (
|
{item.node?.snippet && (
|
||||||
<code className="text-xs bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded break-all mt-1 inline-block">{item.node.snippet}</code>
|
<code className="text-xs bg-neutral-100 dark:bg-neutral-800 px-1.5 py-0.5 rounded break-all mt-1 inline-block">{item.node.snippet}</code>
|
||||||
|
|||||||
13
app/sites/[id]/search/error.tsx
Normal file
13
app/sites/[id]/search/error.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import ErrorDisplay from '@/components/ErrorDisplay'
|
||||||
|
|
||||||
|
export default function SearchError({ reset }: { error: Error; reset: () => void }) {
|
||||||
|
return (
|
||||||
|
<ErrorDisplay
|
||||||
|
title="Search Console data failed to load"
|
||||||
|
message="We couldn't load the Google Search Console data. This might be a temporary issue — try again."
|
||||||
|
onRetry={reset}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { getDateRange, formatDate, Select, DatePicker } from '@ciphera-net/ui'
|
import { Select, DatePicker } from '@ciphera-net/ui'
|
||||||
|
import { getDateRange, formatDate, getThisWeekRange, getThisMonthRange } from '@/lib/utils/dateRanges'
|
||||||
import { CaretDown, CaretUp, MagnifyingGlass, ArrowSquareOut } from '@phosphor-icons/react'
|
import { CaretDown, CaretUp, MagnifyingGlass, ArrowSquareOut } from '@phosphor-icons/react'
|
||||||
import { useDashboard, useGSCStatus, useGSCOverview, useGSCTopQueries, useGSCTopPages, useGSCNewQueries } from '@/lib/swr/dashboard'
|
import { useDashboard, useGSCStatus, useGSCOverview, useGSCTopQueries, useGSCTopPages, useGSCNewQueries } from '@/lib/swr/dashboard'
|
||||||
import { getGSCQueryPages, getGSCPageQueries } from '@/lib/api/gsc'
|
import { getGSCQueryPages, getGSCPageQueries } from '@/lib/api/gsc'
|
||||||
@@ -13,20 +14,6 @@ import ClicksImpressionsChart from '@/components/search/ClicksImpressionsChart'
|
|||||||
|
|
||||||
// ─── Helpers ────────────────────────────────────────────────────
|
// ─── Helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
function getThisWeekRange(): { start: string; end: string } {
|
|
||||||
const today = new Date()
|
|
||||||
const dayOfWeek = today.getDay()
|
|
||||||
const monday = new Date(today)
|
|
||||||
monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1))
|
|
||||||
return { start: formatDate(monday), end: formatDate(today) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThisMonthRange(): { start: string; end: string } {
|
|
||||||
const today = new Date()
|
|
||||||
const firstOfMonth = new Date(today.getFullYear(), today.getMonth(), 1)
|
|
||||||
return { start: formatDate(firstOfMonth), end: formatDate(today) }
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatPosition = (pos: number) => pos.toFixed(1)
|
const formatPosition = (pos: number) => pos.toFixed(1)
|
||||||
const formatCTR = (ctr: number) => (ctr * 100).toFixed(1) + '%'
|
const formatCTR = (ctr: number) => (ctr * 100).toFixed(1) + '%'
|
||||||
|
|
||||||
@@ -179,10 +166,10 @@ export default function SearchConsolePage() {
|
|||||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-5 mb-6">
|
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-5 mb-6">
|
||||||
<MagnifyingGlass size={40} className="text-neutral-400 dark:text-neutral-500" />
|
<MagnifyingGlass size={40} className="text-neutral-400 dark:text-neutral-500" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-2">
|
<h2 className="text-xl font-semibold text-white mb-2">
|
||||||
Connect Google Search Console
|
Connect Google Search Console
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-md mb-6">
|
<p className="text-sm text-neutral-400 max-w-md mb-6">
|
||||||
See how your site performs in Google Search. View top queries, pages, click-through rates, and average position data.
|
See how your site performs in Google Search. View top queries, pages, click-through rates, and average position data.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
@@ -215,10 +202,10 @@ export default function SearchConsolePage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">
|
<h1 className="text-2xl font-bold text-white mb-1">
|
||||||
Search Console
|
Search Console
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Google Search performance, queries, and page rankings
|
Google Search performance, queries, and page rankings
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,9 +283,9 @@ export default function SearchConsolePage() {
|
|||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3 mb-6">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3 mb-6">
|
||||||
{topQueries.queries.slice(0, 5).map((q) => (
|
{topQueries.queries.slice(0, 5).map((q) => (
|
||||||
<div key={q.query} className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-3">
|
<div key={q.query} className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-3">
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 truncate mb-1">{q.query}</p>
|
<p className="text-xs text-neutral-400 truncate mb-1">{q.query}</p>
|
||||||
<div className="flex items-baseline gap-1.5">
|
<div className="flex items-baseline gap-1.5">
|
||||||
<p className="text-lg font-semibold text-neutral-900 dark:text-white">{q.position.toFixed(1)}</p>
|
<p className="text-lg font-semibold text-white">{q.position.toFixed(1)}</p>
|
||||||
<p className="text-xs text-neutral-400">pos</p>
|
<p className="text-xs text-neutral-400">pos</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-neutral-500 mt-0.5">{q.clicks} {q.clicks === 1 ? 'click' : 'clicks'}</p>
|
<p className="text-xs text-neutral-500 mt-0.5">{q.clicks} {q.clicks === 1 ? 'click' : 'clicks'}</p>
|
||||||
@@ -322,8 +309,8 @@ export default function SearchConsolePage() {
|
|||||||
onClick={() => { setActiveView('queries'); setExpandedQuery(null); setExpandedData([]) }}
|
onClick={() => { setActiveView('queries'); setExpandedQuery(null); setExpandedData([]) }}
|
||||||
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-all cursor-pointer ${
|
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-all cursor-pointer ${
|
||||||
activeView === 'queries'
|
activeView === 'queries'
|
||||||
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
|
? 'bg-white dark:bg-neutral-700 text-white shadow-sm'
|
||||||
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
|
: 'text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Top Queries
|
Top Queries
|
||||||
@@ -332,8 +319,8 @@ export default function SearchConsolePage() {
|
|||||||
onClick={() => { setActiveView('pages'); setExpandedPage(null); setExpandedData([]) }}
|
onClick={() => { setActiveView('pages'); setExpandedPage(null); setExpandedData([]) }}
|
||||||
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-all cursor-pointer ${
|
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-all cursor-pointer ${
|
||||||
activeView === 'pages'
|
activeView === 'pages'
|
||||||
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
|
? 'bg-white dark:bg-neutral-700 text-white shadow-sm'
|
||||||
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
|
: 'text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Top Pages
|
Top Pages
|
||||||
@@ -347,12 +334,12 @@ export default function SearchConsolePage() {
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-neutral-200 dark:border-neutral-800">
|
<tr className="border-b border-neutral-200 dark:border-neutral-800">
|
||||||
<th className="text-left px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400 w-8" />
|
<th className="text-left px-4 py-3 font-medium text-neutral-400 w-8" />
|
||||||
<th className="text-left px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Query</th>
|
<th className="text-left px-4 py-3 font-medium text-neutral-400">Query</th>
|
||||||
<th className="text-right px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Clicks</th>
|
<th className="text-right px-4 py-3 font-medium text-neutral-400">Clicks</th>
|
||||||
<th className="text-right px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Impressions</th>
|
<th className="text-right px-4 py-3 font-medium text-neutral-400">Impressions</th>
|
||||||
<th className="text-right px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">CTR</th>
|
<th className="text-right px-4 py-3 font-medium text-neutral-400">CTR</th>
|
||||||
<th className="text-right px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Position</th>
|
<th className="text-right px-4 py-3 font-medium text-neutral-400">Position</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -369,7 +356,7 @@ export default function SearchConsolePage() {
|
|||||||
))
|
))
|
||||||
) : queries.length === 0 ? (
|
) : queries.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="px-4 py-12 text-center text-neutral-500 dark:text-neutral-400">
|
<td colSpan={6} className="px-4 py-12 text-center text-neutral-400">
|
||||||
No query data available for this period.
|
No query data available for this period.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -391,7 +378,7 @@ export default function SearchConsolePage() {
|
|||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{queriesTotal > PAGE_SIZE && (
|
{queriesTotal > PAGE_SIZE && (
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-t border-neutral-200 dark:border-neutral-800">
|
<div className="flex items-center justify-between px-4 py-3 border-t border-neutral-200 dark:border-neutral-800">
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Showing {queryPage * PAGE_SIZE + 1}-{Math.min((queryPage + 1) * PAGE_SIZE, queriesTotal)} of {queriesTotal.toLocaleString()}
|
Showing {queryPage * PAGE_SIZE + 1}-{Math.min((queryPage + 1) * PAGE_SIZE, queriesTotal)} of {queriesTotal.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -421,12 +408,12 @@ export default function SearchConsolePage() {
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-neutral-200 dark:border-neutral-800">
|
<tr className="border-b border-neutral-200 dark:border-neutral-800">
|
||||||
<th className="text-left px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400 w-8" />
|
<th className="text-left px-4 py-3 font-medium text-neutral-400 w-8" />
|
||||||
<th className="text-left px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Page</th>
|
<th className="text-left px-4 py-3 font-medium text-neutral-400">Page</th>
|
||||||
<th className="text-right px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Clicks</th>
|
<th className="text-right px-4 py-3 font-medium text-neutral-400">Clicks</th>
|
||||||
<th className="text-right px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Impressions</th>
|
<th className="text-right px-4 py-3 font-medium text-neutral-400">Impressions</th>
|
||||||
<th className="text-right px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">CTR</th>
|
<th className="text-right px-4 py-3 font-medium text-neutral-400">CTR</th>
|
||||||
<th className="text-right px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Position</th>
|
<th className="text-right px-4 py-3 font-medium text-neutral-400">Position</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -443,7 +430,7 @@ export default function SearchConsolePage() {
|
|||||||
))
|
))
|
||||||
) : pages.length === 0 ? (
|
) : pages.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="px-4 py-12 text-center text-neutral-500 dark:text-neutral-400">
|
<td colSpan={6} className="px-4 py-12 text-center text-neutral-400">
|
||||||
No page data available for this period.
|
No page data available for this period.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -465,7 +452,7 @@ export default function SearchConsolePage() {
|
|||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{pagesTotal > PAGE_SIZE && (
|
{pagesTotal > PAGE_SIZE && (
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-t border-neutral-200 dark:border-neutral-800">
|
<div className="flex items-center justify-between px-4 py-3 border-t border-neutral-200 dark:border-neutral-800">
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Showing {pagePage * PAGE_SIZE + 1}-{Math.min((pagePage + 1) * PAGE_SIZE, pagesTotal)} of {pagesTotal.toLocaleString()}
|
Showing {pagePage * PAGE_SIZE + 1}-{Math.min((pagePage + 1) * PAGE_SIZE, pagesTotal)} of {pagesTotal.toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -522,13 +509,13 @@ function OverviewCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
|
<div className="p-4 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
|
||||||
<p className="text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-1">{label}</p>
|
<p className="text-xs font-medium text-neutral-400 mb-1">{label}</p>
|
||||||
<p className="text-2xl font-bold text-neutral-900 dark:text-white">{value}</p>
|
<p className="text-2xl font-bold text-white">{value}</p>
|
||||||
{change && (
|
{change && (
|
||||||
<p className={`text-xs mt-1 font-medium ${
|
<p className={`text-xs mt-1 font-medium ${
|
||||||
isPositive ? 'text-green-600 dark:text-green-400' :
|
isPositive ? 'text-green-600 dark:text-green-400' :
|
||||||
isNegative ? 'text-red-600 dark:text-red-400' :
|
isNegative ? 'text-red-600 dark:text-red-400' :
|
||||||
'text-neutral-500 dark:text-neutral-400'
|
'text-neutral-400'
|
||||||
}`}>
|
}`}>
|
||||||
{change.label} vs previous period
|
{change.label} vs previous period
|
||||||
</p>
|
</p>
|
||||||
@@ -560,7 +547,7 @@ function QueryRow({
|
|||||||
<td className="px-4 py-3 text-neutral-400 dark:text-neutral-500">
|
<td className="px-4 py-3 text-neutral-400 dark:text-neutral-500">
|
||||||
<Caret size={14} />
|
<Caret size={14} />
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-neutral-900 dark:text-white font-medium">{row.query}</td>
|
<td className="px-4 py-3 text-white font-medium">{row.query}</td>
|
||||||
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{row.clicks.toLocaleString()}</td>
|
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{row.clicks.toLocaleString()}</td>
|
||||||
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{row.impressions.toLocaleString()}</td>
|
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{row.impressions.toLocaleString()}</td>
|
||||||
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{formatCTR(row.ctr)}</td>
|
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{formatCTR(row.ctr)}</td>
|
||||||
@@ -576,7 +563,7 @@ function QueryRow({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : expandedData.length === 0 ? (
|
) : expandedData.length === 0 ? (
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 py-1">No pages found for this query.</p>
|
<p className="text-sm text-neutral-400 py-1">No pages found for this query.</p>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -631,7 +618,7 @@ function PageRow({
|
|||||||
<td className="px-4 py-3 text-neutral-400 dark:text-neutral-500">
|
<td className="px-4 py-3 text-neutral-400 dark:text-neutral-500">
|
||||||
<Caret size={14} />
|
<Caret size={14} />
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-neutral-900 dark:text-white font-medium max-w-md truncate" title={row.page}>{row.page}</td>
|
<td className="px-4 py-3 text-white font-medium max-w-md truncate" title={row.page}>{row.page}</td>
|
||||||
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{row.clicks.toLocaleString()}</td>
|
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{row.clicks.toLocaleString()}</td>
|
||||||
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{row.impressions.toLocaleString()}</td>
|
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{row.impressions.toLocaleString()}</td>
|
||||||
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{formatCTR(row.ctr)}</td>
|
<td className="px-4 py-3 text-right text-neutral-700 dark:text-neutral-300 tabular-nums">{formatCTR(row.ctr)}</td>
|
||||||
@@ -647,7 +634,7 @@ function PageRow({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : expandedData.length === 0 ? (
|
) : expandedData.length === 0 ? (
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 py-1">No queries found for this page.</p>
|
<p className="text-sm text-neutral-400 py-1">No queries found for this page.</p>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -738,9 +738,9 @@ export default function SiteSettingsPage() {
|
|||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">Site Settings</h1>
|
<h1 className="text-2xl font-bold text-white">Site Settings</h1>
|
||||||
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
||||||
Manage settings for <span className="font-medium text-neutral-900 dark:text-white">{site.domain}</span>
|
Manage settings for <span className="font-medium text-white">{site.domain}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -860,8 +860,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">General Configuration</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">General Configuration</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Update your site details and tracking script.</p>
|
<p className="text-sm text-neutral-400">Update your site details and tracking script.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -907,17 +907,17 @@ export default function SiteSettingsPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={site.domain}
|
value={site.domain}
|
||||||
disabled
|
disabled
|
||||||
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-neutral-100 dark:bg-neutral-800/50 text-neutral-500 dark:text-neutral-400 cursor-not-allowed"
|
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-neutral-100 dark:bg-neutral-800/50 text-neutral-400 cursor-not-allowed"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="text-xs text-neutral-400">
|
||||||
Domain cannot be changed after creation
|
Domain cannot be changed after creation
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 border-t border-neutral-100 dark:border-neutral-800">
|
<div className="pt-4 border-t border-neutral-100 dark:border-neutral-800">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-2">Tracking Script</h3>
|
<h3 className="text-lg font-semibold text-white mb-2">Tracking Script</h3>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
Add this script to your website to start tracking visitors. Choose your framework for setup instructions.
|
Add this script to your website to start tracking visitors. Choose your framework for setup instructions.
|
||||||
</p>
|
</p>
|
||||||
<ScriptSetupBlock
|
<ScriptSetupBlock
|
||||||
@@ -945,7 +945,7 @@ export default function SiteSettingsPage() {
|
|||||||
{site.is_verified ? <CheckIcon className="w-4 h-4" /> : <ZapIcon className="w-4 h-4" />}
|
{site.is_verified ? <CheckIcon className="w-4 h-4" /> : <ZapIcon className="w-4 h-4" />}
|
||||||
{site.is_verified ? 'Verified' : 'Verify Installation'}
|
{site.is_verified ? 'Verified' : 'Verify Installation'}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="text-xs text-neutral-400">
|
||||||
{site.is_verified ? 'Your site is sending data correctly.' : 'Check if your site is sending data correctly.'}
|
{site.is_verified ? 'Your site is sending data correctly.' : 'Check if your site is sending data correctly.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -964,7 +964,7 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-red-600 dark:text-red-500 mb-1">Danger Zone</h2>
|
<h2 className="text-2xl font-bold text-red-600 dark:text-red-500 mb-1">Danger Zone</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Irreversible actions for your site.</p>
|
<p className="text-sm text-neutral-400">Irreversible actions for your site.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -1003,8 +1003,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Visibility Settings</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">Visibility Settings</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Manage who can view your dashboard.</p>
|
<p className="text-sm text-neutral-400">Manage who can view your dashboard.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
||||||
@@ -1014,8 +1014,8 @@ export default function SiteSettingsPage() {
|
|||||||
<GlobeIcon className="w-6 h-6" />
|
<GlobeIcon className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-neutral-900 dark:text-white">Public Dashboard</h3>
|
<h3 className="font-medium text-white">Public Dashboard</h3>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Allow anyone with the link to view this dashboard
|
Allow anyone with the link to view this dashboard
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1041,7 +1041,7 @@ export default function SiteSettingsPage() {
|
|||||||
className="mt-6 pt-6 border-t border-neutral-200 dark:border-neutral-800 overflow-hidden space-y-6"
|
className="mt-6 pt-6 border-t border-neutral-200 dark:border-neutral-800 overflow-hidden space-y-6"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-2 text-neutral-900 dark:text-white">
|
<label className="block text-sm font-medium mb-2 text-white">
|
||||||
Public Link
|
Public Link
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -1054,12 +1054,12 @@ export default function SiteSettingsPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={copyLink}
|
onClick={copyLink}
|
||||||
className="px-4 py-2 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 text-neutral-900 dark:text-white rounded-xl font-medium hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2"
|
className="px-4 py-2 bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 text-white rounded-xl font-medium hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{linkCopied ? 'Copied!' : 'Copy Link'}
|
{linkCopied ? 'Copied!' : 'Copy Link'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="mt-2 text-xs text-neutral-400">
|
||||||
Share this link with others to view the dashboard.
|
Share this link with others to view the dashboard.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1154,8 +1154,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Data & Privacy</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">Data & Privacy</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Control what visitor data is collected. Less data = more privacy.</p>
|
<p className="text-sm text-neutral-400">Control what visitor data is collected. Less data = more privacy.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data Collection Controls */}
|
{/* Data Collection Controls */}
|
||||||
@@ -1166,8 +1166,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-neutral-900 dark:text-white">Page Paths</h4>
|
<h4 className="font-medium text-white">Page Paths</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
<p className="text-sm text-neutral-400 mt-0.5">
|
||||||
Track which pages visitors view
|
Track which pages visitors view
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1187,8 +1187,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-neutral-900 dark:text-white">Referrers</h4>
|
<h4 className="font-medium text-white">Referrers</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
<p className="text-sm text-neutral-400 mt-0.5">
|
||||||
Track where visitors come from
|
Track where visitors come from
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1208,8 +1208,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-neutral-900 dark:text-white">Device Info</h4>
|
<h4 className="font-medium text-white">Device Info</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
<p className="text-sm text-neutral-400 mt-0.5">
|
||||||
Track browser, OS, and device type
|
Track browser, OS, and device type
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1229,8 +1229,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-neutral-900 dark:text-white">Geographic Data</h4>
|
<h4 className="font-medium text-white">Geographic Data</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
<p className="text-sm text-neutral-400 mt-0.5">
|
||||||
Control location tracking granularity
|
Control location tracking granularity
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1253,8 +1253,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-neutral-900 dark:text-white">Screen Resolution</h4>
|
<h4 className="font-medium text-white">Screen Resolution</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
<p className="text-sm text-neutral-400 mt-0.5">
|
||||||
Track visitor screen sizes
|
Track visitor screen sizes
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1277,8 +1277,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-neutral-900 dark:text-white">Hide unknown locations</h4>
|
<h4 className="font-medium text-white">Hide unknown locations</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
<p className="text-sm text-neutral-400 mt-0.5">
|
||||||
Exclude entries where geographic data could not be resolved from location stats
|
Exclude entries where geographic data could not be resolved from location stats
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1315,8 +1315,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-neutral-900 dark:text-white">Keep raw event data for</h4>
|
<h4 className="font-medium text-white">Keep raw event data for</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
<p className="text-sm text-neutral-400 mt-0.5">
|
||||||
Events older than this are automatically deleted. Aggregated daily stats are kept permanently.
|
Events older than this are automatically deleted. Aggregated daily stats are kept permanently.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1353,8 +1353,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
<div className="p-6 bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-100 dark:border-neutral-800">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-medium text-neutral-900 dark:text-white">Check frequency</h4>
|
<h4 className="font-medium text-white">Check frequency</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
<p className="text-sm text-neutral-400 mt-0.5">
|
||||||
How often PageSpeed Insights runs automated checks on your site.
|
How often PageSpeed Insights runs automated checks on your site.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1406,7 +1406,7 @@ export default function SiteSettingsPage() {
|
|||||||
focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white font-mono text-sm"
|
focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-2">
|
<p className="text-sm text-neutral-400 mt-2">
|
||||||
Enter paths to exclude from tracking (one per line). Supports wildcards (e.g., /admin/*).
|
Enter paths to exclude from tracking (one per line). Supports wildcards (e.g., /admin/*).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1417,7 +1417,7 @@ export default function SiteSettingsPage() {
|
|||||||
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
For your privacy policy
|
For your privacy policy
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Copy the text below into your site's Privacy Policy to describe your use of Pulse.
|
Copy the text below into your site's Privacy Policy to describe your use of Pulse.
|
||||||
It updates automatically based on your saved settings above.
|
It updates automatically based on your saved settings above.
|
||||||
</p>
|
</p>
|
||||||
@@ -1445,7 +1445,7 @@ export default function SiteSettingsPage() {
|
|||||||
{snippetCopied ? (
|
{snippetCopied ? (
|
||||||
<CheckIcon className="w-4 h-4 text-green-600" />
|
<CheckIcon className="w-4 h-4 text-green-600" />
|
||||||
) : (
|
) : (
|
||||||
<svg className="w-4 h-4 text-neutral-500 dark:text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="w-4 h-4 text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -1468,7 +1468,7 @@ export default function SiteSettingsPage() {
|
|||||||
{activeTab === 'bot' && (
|
{activeTab === 'bot' && (
|
||||||
<div className="flex-1 space-y-6">
|
<div className="flex-1 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-white mb-1">Bot & Spam</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">Bot & Spam</h2>
|
||||||
<p className="text-neutral-400 text-sm">Manage automated and manual bot filtering.</p>
|
<p className="text-neutral-400 text-sm">Manage automated and manual bot filtering.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1627,8 +1627,8 @@ export default function SiteSettingsPage() {
|
|||||||
{activeTab === 'goals' && (
|
{activeTab === 'goals' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Goals & Events</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">Goals & Events</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Define goals to label custom events (e.g. signup, purchase). Track with <code className="px-1.5 py-0.5 rounded bg-neutral-200 dark:bg-neutral-700 text-xs">pulse.track('event_name')</code> in your snippet.
|
Define goals to label custom events (e.g. signup, purchase). Track with <code className="px-1.5 py-0.5 rounded bg-neutral-200 dark:bg-neutral-700 text-xs">pulse.track('event_name')</code> in your snippet.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1643,7 +1643,7 @@ export default function SiteSettingsPage() {
|
|||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{goals.length === 0 ? (
|
{goals.length === 0 ? (
|
||||||
<div className="p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 text-center text-neutral-500 dark:text-neutral-400 text-sm">
|
<div className="p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 text-center text-neutral-400 text-sm">
|
||||||
No goals yet. Add a goal to give custom events a display name in the dashboard.
|
No goals yet. Add a goal to give custom events a display name in the dashboard.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -1653,8 +1653,8 @@ export default function SiteSettingsPage() {
|
|||||||
className="flex items-center justify-between py-3 px-4 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50/50 dark:bg-neutral-900/50"
|
className="flex items-center justify-between py-3 px-4 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50/50 dark:bg-neutral-900/50"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-neutral-900 dark:text-white">{goal.name}</span>
|
<span className="font-medium text-white">{goal.name}</span>
|
||||||
<span className="text-neutral-500 dark:text-neutral-400 text-sm ml-2">({goal.event_name})</span>
|
<span className="text-neutral-400 text-sm ml-2">({goal.event_name})</span>
|
||||||
</div>
|
</div>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -1686,16 +1686,16 @@ export default function SiteSettingsPage() {
|
|||||||
{activeTab === 'notifications' && (
|
{activeTab === 'notifications' && (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Notifications</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">Notifications</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Configure how you receive reports and alerts.</p>
|
<p className="text-sm text-neutral-400">Configure how you receive reports and alerts.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reports subsection */}
|
{/* Reports subsection */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-medium text-neutral-900 dark:text-white">Reports</h3>
|
<h3 className="text-base font-medium text-white">Reports</h3>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">Automatically deliver analytics reports via email or webhooks.</p>
|
<p className="text-sm text-neutral-400 mt-0.5">Automatically deliver analytics reports via email or webhooks.</p>
|
||||||
</div>
|
</div>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Button onClick={() => { setEditingSchedule(null); resetReportForm(); setReportModalOpen(true) }}>
|
<Button onClick={() => { setEditingSchedule(null); resetReportForm(); setReportModalOpen(true) }}>
|
||||||
@@ -1711,7 +1711,7 @@ export default function SiteSettingsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : reportSchedules.length === 0 ? (
|
) : reportSchedules.length === 0 ? (
|
||||||
<div className="p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 text-center text-neutral-500 dark:text-neutral-400 text-sm">
|
<div className="p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 text-center text-neutral-400 text-sm">
|
||||||
No scheduled reports yet. Add a report to automatically receive analytics summaries.
|
No scheduled reports yet. Add a report to automatically receive analytics summaries.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -1732,7 +1732,7 @@ export default function SiteSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="font-medium text-neutral-900 dark:text-white">
|
<span className="font-medium text-white">
|
||||||
{getChannelLabel(schedule.channel)}
|
{getChannelLabel(schedule.channel)}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-brand-orange/10 text-brand-orange">
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-brand-orange/10 text-brand-orange">
|
||||||
@@ -1742,7 +1742,7 @@ export default function SiteSettingsPage() {
|
|||||||
{getReportTypeLabel(schedule.report_type)}
|
{getReportTypeLabel(schedule.report_type)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1 truncate">
|
<p className="text-sm text-neutral-400 mt-1 truncate">
|
||||||
{schedule.channel === 'email'
|
{schedule.channel === 'email'
|
||||||
? (schedule.channel_config as EmailConfig).recipients.join(', ')
|
? (schedule.channel_config as EmailConfig).recipients.join(', ')
|
||||||
: (schedule.channel_config as WebhookConfig).url}
|
: (schedule.channel_config as WebhookConfig).url}
|
||||||
@@ -1821,8 +1821,8 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-medium text-neutral-900 dark:text-white">Alerts</h3>
|
<h3 className="text-base font-medium text-white">Alerts</h3>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">Get notified when your site goes down or recovers.</p>
|
<p className="text-sm text-neutral-400 mt-0.5">Get notified when your site goes down or recovers.</p>
|
||||||
</div>
|
</div>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Button onClick={() => { setEditingAlert(null); resetAlertForm(); setAlertModalOpen(true) }}>
|
<Button onClick={() => { setEditingAlert(null); resetAlertForm(); setAlertModalOpen(true) }}>
|
||||||
@@ -1838,7 +1838,7 @@ export default function SiteSettingsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : alertSchedules.length === 0 ? (
|
) : alertSchedules.length === 0 ? (
|
||||||
<div className="p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 text-center text-neutral-500 dark:text-neutral-400 text-sm">
|
<div className="p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 text-center text-neutral-400 text-sm">
|
||||||
No alert channels configured. Add a channel to receive uptime alerts when your site goes down or recovers.
|
No alert channels configured. Add a channel to receive uptime alerts when your site goes down or recovers.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -1859,14 +1859,14 @@ export default function SiteSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="font-medium text-neutral-900 dark:text-white">
|
<span className="font-medium text-white">
|
||||||
{getChannelLabel(schedule.channel)}
|
{getChannelLabel(schedule.channel)}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-500">
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-500">
|
||||||
Uptime Alert
|
Uptime Alert
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1 truncate">
|
<p className="text-sm text-neutral-400 mt-1 truncate">
|
||||||
{schedule.channel === 'email'
|
{schedule.channel === 'email'
|
||||||
? (schedule.channel_config as EmailConfig).recipients.join(', ')
|
? (schedule.channel_config as EmailConfig).recipients.join(', ')
|
||||||
: (schedule.channel_config as WebhookConfig).url}
|
: (schedule.channel_config as WebhookConfig).url}
|
||||||
@@ -1940,8 +1940,8 @@ export default function SiteSettingsPage() {
|
|||||||
{activeTab === 'integrations' && (
|
{activeTab === 'integrations' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Integrations</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">Integrations</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Connect external services to enrich your analytics data.</p>
|
<p className="text-sm text-neutral-400">Connect external services to enrich your analytics data.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Google Search Console */}
|
{/* Google Search Console */}
|
||||||
@@ -1958,7 +1958,7 @@ export default function SiteSettingsPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">Google Search Console</h3>
|
<h3 className="text-lg font-semibold text-white">Google Search Console</h3>
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">
|
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">
|
||||||
See which search queries bring visitors to your site, with impressions, clicks, CTR, and ranking position.
|
See which search queries bring visitors to your site, with impressions, clicks, CTR, and ranking position.
|
||||||
</p>
|
</p>
|
||||||
@@ -1968,7 +1968,7 @@ export default function SiteSettingsPage() {
|
|||||||
<svg className="w-4 h-4 text-neutral-400 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
<svg className="w-4 h-4 text-neutral-400 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="text-xs text-neutral-400">
|
||||||
Pulse only requests read-only access. Your tokens are encrypted at rest and all data can be fully removed at any time.
|
Pulse only requests read-only access. Your tokens are encrypted at rest and all data can be fully removed at any time.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -2005,7 +2005,7 @@ export default function SiteSettingsPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">Google Search Console</h3>
|
<h3 className="text-lg font-semibold text-white">Google Search Console</h3>
|
||||||
<div className="flex items-center gap-2 mt-1.5">
|
<div className="flex items-center gap-2 mt-1.5">
|
||||||
<span className={`inline-flex items-center gap-1.5 text-xs font-medium ${
|
<span className={`inline-flex items-center gap-1.5 text-xs font-medium ${
|
||||||
gscStatus.status === 'active'
|
gscStatus.status === 'active'
|
||||||
@@ -2031,28 +2031,28 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{gscStatus.google_email && (
|
{gscStatus.google_email && (
|
||||||
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">Google Account</p>
|
<p className="text-xs text-neutral-400">Google Account</p>
|
||||||
<p className="text-sm font-medium text-neutral-900 dark:text-white mt-0.5 truncate">{gscStatus.google_email}</p>
|
<p className="text-sm font-medium text-white mt-0.5 truncate">{gscStatus.google_email}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{gscStatus.gsc_property && (
|
{gscStatus.gsc_property && (
|
||||||
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">Property</p>
|
<p className="text-xs text-neutral-400">Property</p>
|
||||||
<p className="text-sm font-medium text-neutral-900 dark:text-white mt-0.5 truncate">{gscStatus.gsc_property}</p>
|
<p className="text-sm font-medium text-white mt-0.5 truncate">{gscStatus.gsc_property}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{gscStatus.last_synced_at && (
|
{gscStatus.last_synced_at && (
|
||||||
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">Last Synced</p>
|
<p className="text-xs text-neutral-400">Last Synced</p>
|
||||||
<p className="text-sm font-medium text-neutral-900 dark:text-white mt-0.5">
|
<p className="text-sm font-medium text-white mt-0.5">
|
||||||
{new Date(gscStatus.last_synced_at).toLocaleString('en-GB')}
|
{new Date(gscStatus.last_synced_at).toLocaleString('en-GB')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{gscStatus.created_at && (
|
{gscStatus.created_at && (
|
||||||
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">Connected Since</p>
|
<p className="text-xs text-neutral-400">Connected Since</p>
|
||||||
<p className="text-sm font-medium text-neutral-900 dark:text-white mt-0.5">
|
<p className="text-sm font-medium text-white mt-0.5">
|
||||||
{new Date(gscStatus.created_at).toLocaleString('en-GB')}
|
{new Date(gscStatus.created_at).toLocaleString('en-GB')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -2121,7 +2121,7 @@ export default function SiteSettingsPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">BunnyCDN</h3>
|
<h3 className="text-lg font-semibold text-white">BunnyCDN</h3>
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">
|
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">
|
||||||
Monitor CDN performance with bandwidth usage, cache hit rates, response times, and geographic distribution.
|
Monitor CDN performance with bandwidth usage, cache hit rates, response times, and geographic distribution.
|
||||||
</p>
|
</p>
|
||||||
@@ -2131,7 +2131,7 @@ export default function SiteSettingsPage() {
|
|||||||
<svg className="w-4 h-4 text-neutral-400 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
<svg className="w-4 h-4 text-neutral-400 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="text-xs text-neutral-400">
|
||||||
Your API key is encrypted at rest and only used to fetch read-only statistics. You can disconnect at any time.
|
Your API key is encrypted at rest and only used to fetch read-only statistics. You can disconnect at any time.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -2147,7 +2147,7 @@ export default function SiteSettingsPage() {
|
|||||||
setBunnySelectedZone(null)
|
setBunnySelectedZone(null)
|
||||||
}}
|
}}
|
||||||
placeholder="BunnyCDN API key"
|
placeholder="BunnyCDN API key"
|
||||||
className="flex-1 px-4 py-2.5 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white text-sm placeholder:text-neutral-400"
|
className="flex-1 px-4 py-2.5 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-white dark:bg-neutral-900 text-white text-sm placeholder:text-neutral-400"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -2190,7 +2190,7 @@ export default function SiteSettingsPage() {
|
|||||||
const zone = bunnyPullZones.find(z => z.id === Number(e.target.value))
|
const zone = bunnyPullZones.find(z => z.id === Number(e.target.value))
|
||||||
setBunnySelectedZone(zone || null)
|
setBunnySelectedZone(zone || null)
|
||||||
}}
|
}}
|
||||||
className="w-full px-4 py-2.5 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white text-sm"
|
className="w-full px-4 py-2.5 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-white dark:bg-neutral-900 text-white text-sm"
|
||||||
>
|
>
|
||||||
{bunnyPullZones.map((zone) => (
|
{bunnyPullZones.map((zone) => (
|
||||||
<option key={zone.id} value={zone.id}>{zone.name}</option>
|
<option key={zone.id} value={zone.id}>{zone.name}</option>
|
||||||
@@ -2252,7 +2252,7 @@ export default function SiteSettingsPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">BunnyCDN</h3>
|
<h3 className="text-lg font-semibold text-white">BunnyCDN</h3>
|
||||||
<div className="flex items-center gap-2 mt-1.5">
|
<div className="flex items-center gap-2 mt-1.5">
|
||||||
<span className={`inline-flex items-center gap-1.5 text-xs font-medium ${
|
<span className={`inline-flex items-center gap-1.5 text-xs font-medium ${
|
||||||
bunnyStatus.status === 'active'
|
bunnyStatus.status === 'active'
|
||||||
@@ -2278,22 +2278,22 @@ export default function SiteSettingsPage() {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{bunnyStatus.pull_zone_name && (
|
{bunnyStatus.pull_zone_name && (
|
||||||
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">Pull Zone</p>
|
<p className="text-xs text-neutral-400">Pull Zone</p>
|
||||||
<p className="text-sm font-medium text-neutral-900 dark:text-white mt-0.5 truncate">{bunnyStatus.pull_zone_name}</p>
|
<p className="text-sm font-medium text-white mt-0.5 truncate">{bunnyStatus.pull_zone_name}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{bunnyStatus.last_synced_at && (
|
{bunnyStatus.last_synced_at && (
|
||||||
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">Last Synced</p>
|
<p className="text-xs text-neutral-400">Last Synced</p>
|
||||||
<p className="text-sm font-medium text-neutral-900 dark:text-white mt-0.5">
|
<p className="text-sm font-medium text-white mt-0.5">
|
||||||
{new Date(bunnyStatus.last_synced_at).toLocaleString('en-GB')}
|
{new Date(bunnyStatus.last_synced_at).toLocaleString('en-GB')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{bunnyStatus.created_at && (
|
{bunnyStatus.created_at && (
|
||||||
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
<div className="p-3 bg-white dark:bg-neutral-800/50 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">Connected Since</p>
|
<p className="text-xs text-neutral-400">Connected Since</p>
|
||||||
<p className="text-sm font-medium text-neutral-900 dark:text-white mt-0.5">
|
<p className="text-sm font-medium text-white mt-0.5">
|
||||||
{new Date(bunnyStatus.created_at).toLocaleString('en-GB')}
|
{new Date(bunnyStatus.created_at).toLocaleString('en-GB')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -2354,7 +2354,7 @@ export default function SiteSettingsPage() {
|
|||||||
onChange={(e) => setGoalForm({ ...goalForm, name: e.target.value })}
|
onChange={(e) => setGoalForm({ ...goalForm, name: e.target.value })}
|
||||||
placeholder="e.g. Signups"
|
placeholder="e.g. Signups"
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white"
|
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-white"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -2366,11 +2366,11 @@ export default function SiteSettingsPage() {
|
|||||||
onChange={(e) => setGoalForm({ ...goalForm, event_name: e.target.value })}
|
onChange={(e) => setGoalForm({ ...goalForm, event_name: e.target.value })}
|
||||||
placeholder="e.g. signup_click (letters, numbers, underscores only)"
|
placeholder="e.g. signup_click (letters, numbers, underscores only)"
|
||||||
maxLength={64}
|
maxLength={64}
|
||||||
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white"
|
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-white"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between mt-1">
|
<div className="flex justify-between mt-1">
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">Letters, numbers, and underscores only. Spaces become underscores.</p>
|
<p className="text-xs text-neutral-400">Letters, numbers, and underscores only. Spaces become underscores.</p>
|
||||||
<span className={`text-xs tabular-nums ${goalForm.event_name.length > 56 ? 'text-amber-500' : 'text-neutral-400'}`}>{goalForm.event_name.length}/64</span>
|
<span className={`text-xs tabular-nums ${goalForm.event_name.length > 56 ? 'text-amber-500' : 'text-neutral-400'}`}>{goalForm.event_name.length}/64</span>
|
||||||
</div>
|
</div>
|
||||||
{editingGoal && goalForm.event_name.trim().toLowerCase().replace(/\s+/g, '_') !== editingGoal.event_name && (
|
{editingGoal && goalForm.event_name.trim().toLowerCase().replace(/\s+/g, '_') !== editingGoal.event_name && (
|
||||||
@@ -2423,10 +2423,10 @@ export default function SiteSettingsPage() {
|
|||||||
value={reportForm.recipients}
|
value={reportForm.recipients}
|
||||||
onChange={(e) => setReportForm({ ...reportForm, recipients: e.target.value })}
|
onChange={(e) => setReportForm({ ...reportForm, recipients: e.target.value })}
|
||||||
placeholder="email1@example.com, email2@example.com"
|
placeholder="email1@example.com, email2@example.com"
|
||||||
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white"
|
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-white"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Comma-separated email addresses.</p>
|
<p className="text-xs text-neutral-400 mt-1">Comma-separated email addresses.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
@@ -2438,7 +2438,7 @@ export default function SiteSettingsPage() {
|
|||||||
value={reportForm.webhookUrl}
|
value={reportForm.webhookUrl}
|
||||||
onChange={(e) => setReportForm({ ...reportForm, webhookUrl: e.target.value })}
|
onChange={(e) => setReportForm({ ...reportForm, webhookUrl: e.target.value })}
|
||||||
placeholder="https://hooks.example.com/..."
|
placeholder="https://hooks.example.com/..."
|
||||||
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white"
|
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-white"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -2582,10 +2582,10 @@ export default function SiteSettingsPage() {
|
|||||||
value={alertForm.recipients}
|
value={alertForm.recipients}
|
||||||
onChange={(e) => setAlertForm({ ...alertForm, recipients: e.target.value })}
|
onChange={(e) => setAlertForm({ ...alertForm, recipients: e.target.value })}
|
||||||
placeholder="email1@example.com, email2@example.com"
|
placeholder="email1@example.com, email2@example.com"
|
||||||
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white"
|
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-white"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">Comma-separated email addresses.</p>
|
<p className="text-xs text-neutral-400 mt-1">Comma-separated email addresses.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
@@ -2597,14 +2597,14 @@ export default function SiteSettingsPage() {
|
|||||||
value={alertForm.webhookUrl}
|
value={alertForm.webhookUrl}
|
||||||
onChange={(e) => setAlertForm({ ...alertForm, webhookUrl: e.target.value })}
|
onChange={(e) => setAlertForm({ ...alertForm, webhookUrl: e.target.value })}
|
||||||
placeholder="https://hooks.example.com/..."
|
placeholder="https://hooks.example.com/..."
|
||||||
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white"
|
className="w-full px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 text-white"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="p-3 rounded-lg bg-neutral-50 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700">
|
<div className="p-3 rounded-lg bg-neutral-50 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700">
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="text-xs text-neutral-400">
|
||||||
Alerts are sent automatically when your site goes down or recovers. No schedule configuration needed.
|
Alerts are sent automatically when your site goes down or recovers. No schedule configuration needed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ function getOverallStatusTextColor(status: string): string {
|
|||||||
case 'down':
|
case 'down':
|
||||||
return 'text-red-600 dark:text-red-400'
|
return 'text-red-600 dark:text-red-400'
|
||||||
default:
|
default:
|
||||||
return 'text-neutral-500 dark:text-neutral-400'
|
return 'text-neutral-400'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,22 +168,22 @@ function StatusBarTooltip({
|
|||||||
style={{ left: position.x, top: position.y - 10, transform: 'translate(-50%, -100%)' }}
|
style={{ left: position.x, top: position.y - 10, transform: 'translate(-50%, -100%)' }}
|
||||||
>
|
>
|
||||||
<div className="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-lg transition-shadow duration-300 px-3 py-2.5 text-xs min-w-40">
|
<div className="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-lg transition-shadow duration-300 px-3 py-2.5 text-xs min-w-40">
|
||||||
<div className="font-semibold text-neutral-900 dark:text-white mb-1.5">{formattedDate}</div>
|
<div className="font-semibold text-white mb-1.5">{formattedDate}</div>
|
||||||
{stat && stat.total_checks > 0 ? (
|
{stat && stat.total_checks > 0 ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex justify-between gap-4">
|
<div className="flex justify-between gap-4">
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">Uptime</span>
|
<span className="text-neutral-400">Uptime</span>
|
||||||
<span className="font-medium text-neutral-900 dark:text-white">
|
<span className="font-medium text-white">
|
||||||
{formatUptime(stat.uptime_percentage)}
|
{formatUptime(stat.uptime_percentage)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between gap-4">
|
<div className="flex justify-between gap-4">
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">Checks</span>
|
<span className="text-neutral-400">Checks</span>
|
||||||
<span className="font-medium text-neutral-900 dark:text-white">{stat.total_checks}</span>
|
<span className="font-medium text-white">{stat.total_checks}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between gap-4">
|
<div className="flex justify-between gap-4">
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">Avg Response</span>
|
<span className="text-neutral-400">Avg Response</span>
|
||||||
<span className="font-medium text-neutral-900 dark:text-white">
|
<span className="font-medium text-white">
|
||||||
{formatMs(Math.round(stat.avg_response_time_ms))}
|
{formatMs(Math.round(stat.avg_response_time_ms))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,7 +275,7 @@ function ResponseTimeChart({ checks }: { checks: UptimeCheck[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h4 className="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-3">
|
<h4 className="text-xs font-semibold text-neutral-400 uppercase tracking-wider mb-3">
|
||||||
Response Time
|
Response Time
|
||||||
</h4>
|
</h4>
|
||||||
<ChartContainer config={responseTimeChartConfig} className="h-40">
|
<ChartContainer config={responseTimeChartConfig} className="h-40">
|
||||||
@@ -406,10 +406,10 @@ export default function UptimePage() {
|
|||||||
<div className={`w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8 ${fadeClass}`}>
|
<div className={`w-full max-w-6xl mx-auto px-4 sm:px-6 pb-8 ${fadeClass}`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">
|
<h1 className="text-2xl font-bold text-white mb-1">
|
||||||
Uptime
|
Uptime
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Monitor your site's availability and response time
|
Monitor your site's availability and response time
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -417,14 +417,14 @@ export default function UptimePage() {
|
|||||||
{/* Empty state */}
|
{/* Empty state */}
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-12 text-center">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-12 text-center">
|
||||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4 w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
<svg className="w-8 h-8 text-neutral-500 dark:text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-8 h-8 text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-semibold text-neutral-900 dark:text-white mb-2">
|
<h3 className="font-semibold text-white mb-2">
|
||||||
Uptime monitoring is disabled
|
Uptime monitoring is disabled
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-6 max-w-md mx-auto">
|
<p className="text-sm text-neutral-400 mb-6 max-w-md mx-auto">
|
||||||
Enable uptime monitoring to track your site's availability and response time around the clock.
|
Enable uptime monitoring to track your site's availability and response time around the clock.
|
||||||
</p>
|
</p>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
@@ -446,10 +446,10 @@ export default function UptimePage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">
|
<h1 className="text-2xl font-bold text-white mb-1">
|
||||||
Uptime
|
Uptime
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Monitor your site's availability and response time
|
Monitor your site's availability and response time
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -471,7 +471,7 @@ export default function UptimePage() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`w-3.5 h-3.5 rounded-full ${getStatusDotColor(overallStatus)}`} />
|
<div className={`w-3.5 h-3.5 rounded-full ${getStatusDotColor(overallStatus)}`} />
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold text-neutral-900 dark:text-white text-lg">
|
<span className="font-semibold text-white text-lg">
|
||||||
{site.name}
|
{site.name}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-sm font-medium ml-3 ${getOverallStatusTextColor(overallStatus)}`}>
|
<span className={`text-sm font-medium ml-3 ${getOverallStatusTextColor(overallStatus)}`}>
|
||||||
@@ -480,11 +480,11 @@ export default function UptimePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<span className="text-sm font-semibold text-neutral-900 dark:text-white">
|
<span className="text-sm font-semibold text-white">
|
||||||
{formatUptime(overallUptime)} uptime
|
{formatUptime(overallUptime)} uptime
|
||||||
</span>
|
</span>
|
||||||
{monitor && (
|
{monitor && (
|
||||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="text-xs text-neutral-400">
|
||||||
Last checked {formatTimeAgo(monitor.monitor.last_checked_at)}
|
Last checked {formatTimeAgo(monitor.monitor.last_checked_at)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -495,7 +495,7 @@ export default function UptimePage() {
|
|||||||
{/* 90-day uptime bar */}
|
{/* 90-day uptime bar */}
|
||||||
{monitor && (
|
{monitor && (
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-5 mb-6">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-5 mb-6">
|
||||||
<h3 className="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-3">
|
<h3 className="text-xs font-semibold text-neutral-400 uppercase tracking-wider mb-3">
|
||||||
90-Day Availability
|
90-Day Availability
|
||||||
</h3>
|
</h3>
|
||||||
<UptimeStatusBar dailyStats={monitor.daily_stats} />
|
<UptimeStatusBar dailyStats={monitor.daily_stats} />
|
||||||
@@ -512,39 +512,39 @@ export default function UptimePage() {
|
|||||||
{/* Monitor details grid */}
|
{/* Monitor details grid */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-5">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-5">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-1">
|
<div className="text-xs font-medium text-neutral-400 uppercase tracking-wider mb-1">
|
||||||
Status
|
Status
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className={`w-2 h-2 rounded-full ${getStatusDotColor(monitor.monitor.last_status)}`} />
|
<div className={`w-2 h-2 rounded-full ${getStatusDotColor(monitor.monitor.last_status)}`} />
|
||||||
<span className="text-sm font-medium text-neutral-900 dark:text-white">
|
<span className="text-sm font-medium text-white">
|
||||||
{getStatusLabel(monitor.monitor.last_status)}
|
{getStatusLabel(monitor.monitor.last_status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-1">
|
<div className="text-xs font-medium text-neutral-400 uppercase tracking-wider mb-1">
|
||||||
Response Time
|
Response Time
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-neutral-900 dark:text-white">
|
<span className="text-sm font-medium text-white">
|
||||||
{formatMs(monitor.monitor.last_response_time_ms)}
|
{formatMs(monitor.monitor.last_response_time_ms)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-1">
|
<div className="text-xs font-medium text-neutral-400 uppercase tracking-wider mb-1">
|
||||||
Check Interval
|
Check Interval
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-neutral-900 dark:text-white">
|
<span className="text-sm font-medium text-white">
|
||||||
{monitor.monitor.check_interval_seconds >= 60
|
{monitor.monitor.check_interval_seconds >= 60
|
||||||
? `${Math.floor(monitor.monitor.check_interval_seconds / 60)}m`
|
? `${Math.floor(monitor.monitor.check_interval_seconds / 60)}m`
|
||||||
: `${monitor.monitor.check_interval_seconds}s`}
|
: `${monitor.monitor.check_interval_seconds}s`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-1">
|
<div className="text-xs font-medium text-neutral-400 uppercase tracking-wider mb-1">
|
||||||
Overall Uptime
|
Overall Uptime
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-neutral-900 dark:text-white">
|
<span className="text-sm font-medium text-white">
|
||||||
{formatUptime(monitor.overall_uptime)}
|
{formatUptime(monitor.overall_uptime)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -559,7 +559,7 @@ export default function UptimePage() {
|
|||||||
|
|
||||||
{/* Recent checks */}
|
{/* Recent checks */}
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<h4 className="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-3">
|
<h4 className="text-xs font-semibold text-neutral-400 uppercase tracking-wider mb-3">
|
||||||
Recent Checks
|
Recent Checks
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-1.5 max-h-48 overflow-y-auto">
|
<div className="space-y-1.5 max-h-48 overflow-y-auto">
|
||||||
@@ -576,7 +576,7 @@ export default function UptimePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{check.status_code && (
|
{check.status_code && (
|
||||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">
|
<span className="text-xs text-neutral-400">
|
||||||
{check.status_code}
|
{check.status_code}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export default function NewSitePage() {
|
|||||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-green-500/10 text-green-600 dark:text-green-400 mb-6">
|
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-green-500/10 text-green-600 dark:text-green-400 mb-6">
|
||||||
<CheckCircleIcon className="h-7 w-7" />
|
<CheckCircleIcon className="h-7 w-7" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
<h2 className="text-2xl font-bold text-white">
|
||||||
Site created
|
Site created
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
||||||
@@ -137,7 +137,7 @@ export default function NewSitePage() {
|
|||||||
>
|
>
|
||||||
<span className="text-brand-orange">Verify installation</span>
|
<span className="text-brand-orange">Verify installation</span>
|
||||||
</button>
|
</button>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="text-xs text-neutral-400">
|
||||||
Check if your site is sending data correctly.
|
Check if your site is sending data correctly.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,7 +146,7 @@ export default function NewSitePage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleBackToForm}
|
onClick={handleBackToForm}
|
||||||
className="text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 underline"
|
className="text-sm text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 underline"
|
||||||
>
|
>
|
||||||
Edit site details
|
Edit site details
|
||||||
</button>
|
</button>
|
||||||
@@ -174,7 +174,7 @@ export default function NewSitePage() {
|
|||||||
// * Step 1: Name & domain form
|
// * Step 1: Name & domain form
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-2xl mx-auto px-4 sm:px-6 py-8">
|
<div className="w-full max-w-2xl mx-auto px-4 sm:px-6 py-8">
|
||||||
<h1 className="text-2xl font-bold mb-8 text-neutral-900 dark:text-white">
|
<h1 className="text-2xl font-bold mb-8 text-white">
|
||||||
Create New Site
|
Create New Site
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ export default function NewSitePage() {
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
<form onSubmit={handleSubmit} className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="name" className="block text-sm font-medium mb-2 text-neutral-900 dark:text-white">
|
<label htmlFor="name" className="block text-sm font-medium mb-2 text-white">
|
||||||
Site Name
|
Site Name
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -201,7 +201,7 @@ export default function NewSitePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label htmlFor="domain" className="block text-sm font-medium mb-2 text-neutral-900 dark:text-white">
|
<label htmlFor="domain" className="block text-sm font-medium mb-2 text-white">
|
||||||
Domain
|
Domain
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -378,10 +378,10 @@ function WelcomeContent() {
|
|||||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-brand-orange/20 to-brand-orange/5 text-brand-orange mb-5 shadow-sm">
|
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-brand-orange/20 to-brand-orange/5 text-brand-orange mb-5 shadow-sm">
|
||||||
<BarChartIcon className="h-8 w-8" />
|
<BarChartIcon className="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold tracking-tight text-neutral-900 dark:text-white">
|
<h2 className="text-2xl font-bold tracking-tight text-white">
|
||||||
Choose your organization
|
Choose your organization
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm text-neutral-500 dark:text-neutral-400 max-w-sm mx-auto">
|
<p className="mt-2 text-sm text-neutral-400 max-w-sm mx-auto">
|
||||||
Continue with an existing one or create a new organization.
|
Continue with an existing one or create a new organization.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -413,7 +413,7 @@ function WelcomeContent() {
|
|||||||
>
|
>
|
||||||
{initial}
|
{initial}
|
||||||
</div>
|
</div>
|
||||||
<span className="flex-1 font-medium text-neutral-900 dark:text-white truncate">
|
<span className="flex-1 font-medium text-white truncate">
|
||||||
{org.organization_name || 'Organization'}
|
{org.organization_name || 'Organization'}
|
||||||
</span>
|
</span>
|
||||||
{isCurrent && (
|
{isCurrent && (
|
||||||
@@ -443,7 +443,7 @@ function WelcomeContent() {
|
|||||||
alt="Welcome to Pulse"
|
alt="Welcome to Pulse"
|
||||||
className="w-48 h-auto mx-auto mb-6"
|
className="w-48 h-auto mx-auto mb-6"
|
||||||
/>
|
/>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-2xl font-bold text-white">
|
||||||
Welcome to Pulse
|
Welcome to Pulse
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
||||||
@@ -475,7 +475,7 @@ function WelcomeContent() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setStep(1)}
|
onClick={() => setStep(1)}
|
||||||
className="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded"
|
className="flex items-center gap-2 text-sm text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded"
|
||||||
aria-label="Back to welcome"
|
aria-label="Back to welcome"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-4 w-4" />
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
@@ -485,7 +485,7 @@ function WelcomeContent() {
|
|||||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-brand-orange/10 text-brand-orange mb-4">
|
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-brand-orange/10 text-brand-orange mb-4">
|
||||||
<BarChartIcon className="h-7 w-7" />
|
<BarChartIcon className="h-7 w-7" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-2xl font-bold text-white">
|
||||||
Name your organization
|
Name your organization
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
@@ -520,7 +520,7 @@ function WelcomeContent() {
|
|||||||
onChange={(e) => setOrgSlug(e.target.value)}
|
onChange={(e) => setOrgSlug(e.target.value)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="mt-1 text-xs text-neutral-400">
|
||||||
Used in your organization URL.
|
Used in your organization URL.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -546,7 +546,7 @@ function WelcomeContent() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setStep(2)}
|
onClick={() => setStep(2)}
|
||||||
className="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded"
|
className="flex items-center gap-2 text-sm text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded"
|
||||||
aria-label="Back to organization"
|
aria-label="Back to organization"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-4 w-4" />
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
@@ -556,7 +556,7 @@ function WelcomeContent() {
|
|||||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-green-500/10 text-green-600 dark:text-green-400 mb-4">
|
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-green-500/10 text-green-600 dark:text-green-400 mb-4">
|
||||||
<CheckCircleIcon className="h-7 w-7" />
|
<CheckCircleIcon className="h-7 w-7" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-2xl font-bold text-white">
|
||||||
{showPendingCheckoutInStep3 ? 'Complete your plan' : "You're on the free plan"}
|
{showPendingCheckoutInStep3 ? 'Complete your plan' : "You're on the free plan"}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
@@ -631,7 +631,7 @@ function WelcomeContent() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setStep(3)}
|
onClick={() => setStep(3)}
|
||||||
className="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded"
|
className="flex items-center gap-2 text-sm text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 mb-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded"
|
||||||
aria-label="Back to plan"
|
aria-label="Back to plan"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon className="h-4 w-4" />
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
@@ -643,7 +643,7 @@ function WelcomeContent() {
|
|||||||
alt="Add your first site"
|
alt="Add your first site"
|
||||||
className="w-44 h-auto mx-auto mb-4"
|
className="w-44 h-auto mx-auto mb-4"
|
||||||
/>
|
/>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-2xl font-bold text-white">
|
||||||
Add your first site
|
Add your first site
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
@@ -730,7 +730,7 @@ function WelcomeContent() {
|
|||||||
alt="All set"
|
alt="All set"
|
||||||
className="w-44 h-auto mx-auto mb-6"
|
className="w-44 h-auto mx-auto mb-6"
|
||||||
/>
|
/>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
<h1 className="text-2xl font-bold text-white">
|
||||||
You're all set
|
You're all set
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
||||||
@@ -758,7 +758,7 @@ function WelcomeContent() {
|
|||||||
>
|
>
|
||||||
<span className="text-brand-orange">Verify installation</span>
|
<span className="text-brand-orange">Verify installation</span>
|
||||||
</button>
|
</button>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="text-xs text-neutral-400">
|
||||||
Check if your site is sending data correctly.
|
Check if your site is sending data correctly.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function ErrorDisplay({
|
|||||||
className="w-56 h-auto mx-auto mb-8"
|
className="w-56 h-auto mx-auto mb-8"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-4">
|
<h2 className="text-2xl font-bold text-white mb-4">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-neutral-600 dark:text-neutral-400 max-w-md mx-auto mb-10 leading-relaxed">
|
<p className="text-lg text-neutral-600 dark:text-neutral-400 max-w-md mx-auto mb-10 leading-relaxed">
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
|||||||
<footer className="w-full py-8 mt-auto border-t border-neutral-100 dark:border-neutral-800 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm">
|
<footer className="w-full py-8 mt-auto border-t border-neutral-100 dark:border-neutral-800 bg-white/50 dark:bg-neutral-900/50 backdrop-blur-sm">
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
<div className="text-sm text-neutral-500 dark:text-neutral-400">
|
<div className="text-sm text-neutral-400">
|
||||||
© 2024-{year} Ciphera. All rights reserved.
|
© 2024-{year} Ciphera. All rights reserved.
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-6 text-sm font-medium text-neutral-600 dark:text-neutral-300">
|
<div className="flex gap-6 text-sm font-medium text-neutral-600 dark:text-neutral-300">
|
||||||
@@ -88,7 +88,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
className="w-9 h-9 object-contain group-hover:scale-105 transition-transform duration-300"
|
className="w-9 h-9 object-contain group-hover:scale-105 transition-transform duration-300"
|
||||||
/>
|
/>
|
||||||
<span className="text-xl font-bold text-neutral-900 dark:text-white group-hover:text-brand-orange transition-colors duration-300">
|
<span className="text-xl font-bold text-white group-hover:text-brand-orange transition-colors duration-300">
|
||||||
Pulse
|
Pulse
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -125,7 +125,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
|||||||
|
|
||||||
{/* * Products */}
|
{/* * Products */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white mb-4">Products</h4>
|
<h4 className="font-semibold text-white mb-4">Products</h4>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{footerLinks.products.map((link) => (
|
{footerLinks.products.map((link) => (
|
||||||
<li key={link.name}>
|
<li key={link.name}>
|
||||||
@@ -153,7 +153,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
|||||||
|
|
||||||
{/* * Company */}
|
{/* * Company */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white mb-4">Company</h4>
|
<h4 className="font-semibold text-white mb-4">Company</h4>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{footerLinks.company.map((link) => (
|
{footerLinks.company.map((link) => (
|
||||||
<li key={link.name}>
|
<li key={link.name}>
|
||||||
@@ -181,7 +181,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
|||||||
|
|
||||||
{/* * Resources */}
|
{/* * Resources */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white mb-4">Resources</h4>
|
<h4 className="font-semibold text-white mb-4">Resources</h4>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{footerLinks.resources.map((link) => (
|
{footerLinks.resources.map((link) => (
|
||||||
<li key={link.name}>
|
<li key={link.name}>
|
||||||
@@ -209,7 +209,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
|||||||
|
|
||||||
{/* * Legal */}
|
{/* * Legal */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white mb-4">Legal</h4>
|
<h4 className="font-semibold text-white mb-4">Legal</h4>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{footerLinks.legal.map((link) => (
|
{footerLinks.legal.map((link) => (
|
||||||
<li key={link.name}>
|
<li key={link.name}>
|
||||||
@@ -232,10 +232,10 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
|||||||
|
|
||||||
{/* * Bottom bar */}
|
{/* * Bottom bar */}
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
© 2024-{year} Ciphera. All rights reserved.
|
© 2024-{year} Ciphera. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Where Privacy Still Exists
|
Where Privacy Still Exists
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ export default function FrustrationByPageTable({ pages, loading }: FrustrationBy
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 mb-8">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 mb-8">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-white">
|
||||||
Frustration by Page
|
Frustration by Page
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
Pages with the most frustration signals
|
Pages with the most frustration signals
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ export default function FrustrationByPageTable({ pages, loading }: FrustrationBy
|
|||||||
style={{ width: `${barWidth}%` }}
|
style={{ width: `${barWidth}%` }}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="relative text-sm text-neutral-900 dark:text-white truncate max-w-[200px] sm:max-w-[300px]"
|
className="relative text-sm text-white truncate max-w-[200px] sm:max-w-[300px]"
|
||||||
title={page.page_path}
|
title={page.page_path}
|
||||||
>
|
>
|
||||||
{page.page_path}
|
{page.page_path}
|
||||||
@@ -84,7 +84,7 @@ export default function FrustrationByPageTable({ pages, loading }: FrustrationBy
|
|||||||
<span className="w-12 text-right text-sm tabular-nums text-neutral-600 dark:text-neutral-400">
|
<span className="w-12 text-right text-sm tabular-nums text-neutral-600 dark:text-neutral-400">
|
||||||
{formatNumber(page.dead_clicks)}
|
{formatNumber(page.dead_clicks)}
|
||||||
</span>
|
</span>
|
||||||
<span className="w-12 text-right text-sm font-semibold tabular-nums text-neutral-900 dark:text-white">
|
<span className="w-12 text-right text-sm font-semibold tabular-nums text-white">
|
||||||
{formatNumber(page.total)}
|
{formatNumber(page.total)}
|
||||||
</span>
|
</span>
|
||||||
<span className="w-16 text-right text-sm tabular-nums text-neutral-600 dark:text-neutral-400">
|
<span className="w-16 text-right text-sm tabular-nums text-neutral-600 dark:text-neutral-400">
|
||||||
@@ -99,14 +99,17 @@ export default function FrustrationByPageTable({ pages, loading }: FrustrationBy
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center text-center px-6 py-8 gap-4 min-h-[200px]">
|
<div className="flex flex-col items-center justify-center text-center px-6 py-8 gap-4 min-h-[200px]">
|
||||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||||
<Files className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
<Files className="w-8 h-8 text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
<h4 className="font-semibold text-white">
|
||||||
No frustration signals detected
|
No frustration signals detected
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-md">
|
<p className="text-sm text-neutral-400 max-w-md">
|
||||||
Page-level frustration data will appear here once rage clicks or dead clicks are detected on your site.
|
Page-level frustration data will appear here once rage clicks or dead clicks are detected on your site.
|
||||||
</p>
|
</p>
|
||||||
|
<a href="/installation" target="_blank" rel="noopener noreferrer" className="mt-1 text-sm font-medium text-brand-orange hover:underline">
|
||||||
|
View setup guide
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function ChangeIndicator({ change }: { change: ReturnType<typeof pctChange> }) {
|
|||||||
? 'text-red-600 dark:text-red-400'
|
? 'text-red-600 dark:text-red-400'
|
||||||
: isDown
|
: isDown
|
||||||
? 'text-green-600 dark:text-green-400'
|
? 'text-green-600 dark:text-green-400'
|
||||||
: 'text-neutral-500 dark:text-neutral-400'
|
: 'text-neutral-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isUp ? '+' : ''}{change.value}%
|
{isUp ? '+' : ''}{change.value}%
|
||||||
@@ -71,11 +71,11 @@ export default function FrustrationSummaryCards({ data, loading }: FrustrationSu
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
|
||||||
{/* Rage Clicks */}
|
{/* Rage Clicks */}
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
||||||
<p className="text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-1">
|
<p className="text-sm font-medium text-neutral-400 mb-1">
|
||||||
Rage Clicks
|
Rage Clicks
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<span className="text-2xl font-bold text-neutral-900 dark:text-white tabular-nums">
|
<span className="text-2xl font-bold text-white tabular-nums">
|
||||||
{data.rage_clicks.toLocaleString()}
|
{data.rage_clicks.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<ChangeIndicator change={rageChange} />
|
<ChangeIndicator change={rageChange} />
|
||||||
@@ -87,11 +87,11 @@ export default function FrustrationSummaryCards({ data, loading }: FrustrationSu
|
|||||||
|
|
||||||
{/* Dead Clicks */}
|
{/* Dead Clicks */}
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
||||||
<p className="text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-1">
|
<p className="text-sm font-medium text-neutral-400 mb-1">
|
||||||
Dead Clicks
|
Dead Clicks
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<span className="text-2xl font-bold text-neutral-900 dark:text-white tabular-nums">
|
<span className="text-2xl font-bold text-white tabular-nums">
|
||||||
{data.dead_clicks.toLocaleString()}
|
{data.dead_clicks.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<ChangeIndicator change={deadChange} />
|
<ChangeIndicator change={deadChange} />
|
||||||
@@ -103,10 +103,10 @@ export default function FrustrationSummaryCards({ data, loading }: FrustrationSu
|
|||||||
|
|
||||||
{/* Total Frustration Signals */}
|
{/* Total Frustration Signals */}
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
||||||
<p className="text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-1">
|
<p className="text-sm font-medium text-neutral-400 mb-1">
|
||||||
Total Signals
|
Total Signals
|
||||||
</p>
|
</p>
|
||||||
<span className="text-2xl font-bold text-neutral-900 dark:text-white tabular-nums">
|
<span className="text-2xl font-bold text-white tabular-nums">
|
||||||
{totalSignals.toLocaleString()}
|
{totalSignals.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
{topPage ? (
|
{topPage ? (
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function SelectorCell({ selector }: { selector: string }) {
|
|||||||
className="flex items-center gap-1 min-w-0 group/copy cursor-pointer"
|
className="flex items-center gap-1 min-w-0 group/copy cursor-pointer"
|
||||||
title={selector}
|
title={selector}
|
||||||
>
|
>
|
||||||
<span className="text-sm font-mono text-neutral-900 dark:text-white truncate">
|
<span className="text-sm font-mono text-white truncate">
|
||||||
{selector}
|
{selector}
|
||||||
</span>
|
</span>
|
||||||
<span className="opacity-0 group-hover/copy:opacity-100 transition-opacity shrink-0">
|
<span className="opacity-0 group-hover/copy:opacity-100 transition-opacity shrink-0">
|
||||||
@@ -145,7 +145,7 @@ export default function FrustrationTable({
|
|||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-white">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
{showViewAll && (
|
{showViewAll && (
|
||||||
@@ -159,7 +159,7 @@ export default function FrustrationTable({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -182,15 +182,18 @@ export default function FrustrationTable({
|
|||||||
alt="No frustration signals"
|
alt="No frustration signals"
|
||||||
className="w-44 h-auto mb-1"
|
className="w-44 h-auto mb-1"
|
||||||
/>
|
/>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
<h4 className="font-semibold text-white">
|
||||||
No {title.toLowerCase()} detected
|
No {title.toLowerCase()} detected
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-md">
|
<p className="text-sm text-neutral-400 max-w-md">
|
||||||
Frustration tracking requires the add-on script. Add it after your core Pulse script:
|
Frustration tracking requires the add-on script. Add it after your core Pulse script:
|
||||||
</p>
|
</p>
|
||||||
<code className="text-xs bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 px-3 py-2 rounded-lg font-mono break-all">
|
<code className="text-xs bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 px-3 py-2 rounded-lg font-mono break-all">
|
||||||
{'<script defer src="https://pulse.ciphera.net/script.frustration.js"></script>'}
|
{'<script defer src="https://pulse.ciphera.net/script.frustration.js"></script>'}
|
||||||
</code>
|
</code>
|
||||||
|
<a href="/installation" target="_blank" rel="noopener noreferrer" className="mt-1 text-sm font-medium text-brand-orange hover:underline">
|
||||||
|
View setup guide
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +217,7 @@ export default function FrustrationTable({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 py-8 text-center">
|
<p className="text-sm text-neutral-400 py-8 text-center">
|
||||||
No data available
|
No data available
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
|
|||||||
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
||||||
style={{ backgroundColor: item.fill }}
|
style={{ backgroundColor: item.fill }}
|
||||||
/>
|
/>
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">
|
<span className="text-neutral-400">
|
||||||
{LABELS[item.type] ?? item.type}
|
{LABELS[item.type] ?? item.type}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono font-medium tabular-nums text-neutral-900 dark:text-neutral-50">
|
<span className="font-mono font-medium tabular-nums text-neutral-900 dark:text-neutral-50">
|
||||||
@@ -93,21 +93,21 @@ export default function FrustrationTrend({ summary, loading }: FrustrationTrendP
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-white">
|
||||||
Frustration Trend
|
Frustration Trend
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
Rage vs. dead click breakdown
|
Rage vs. dead click breakdown
|
||||||
</p>
|
</p>
|
||||||
<div className="flex-1 min-h-[270px] flex flex-col items-center justify-center text-center px-6 py-8 gap-4">
|
<div className="flex-1 min-h-[270px] flex flex-col items-center justify-center text-center px-6 py-8 gap-4">
|
||||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||||
<TrendUp className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
<TrendUp className="w-8 h-8 text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
<h4 className="font-semibold text-white">
|
||||||
No trend data yet
|
No trend data yet
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-md">
|
<p className="text-sm text-neutral-400 max-w-md">
|
||||||
Frustration trend data will appear here once rage clicks or dead clicks are detected on your site.
|
Frustration trend data will appear here once rage clicks or dead clicks are detected on your site.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,11 +118,11 @@ export default function FrustrationTrend({ summary, loading }: FrustrationTrendP
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-white">
|
||||||
Frustration Trend
|
Frustration Trend
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
{hasPrevious
|
{hasPrevious
|
||||||
? 'Rage and dead clicks split across current and previous period'
|
? 'Rage and dead clicks split across current and previous period'
|
||||||
: 'Rage vs. dead click breakdown'}
|
: 'Rage vs. dead click breakdown'}
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ export default function Chart({
|
|||||||
>
|
>
|
||||||
<div className={cn('text-[10px] font-semibold uppercase tracking-widest mb-2', metric === m.key ? 'text-brand-orange' : 'text-neutral-400 dark:text-neutral-500')}>{m.label}</div>
|
<div className={cn('text-[10px] font-semibold uppercase tracking-widest mb-2', metric === m.key ? 'text-brand-orange' : 'text-neutral-400 dark:text-neutral-500')}>{m.label}</div>
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<AnimatedNumber value={m.value} format={m.format} className="text-2xl font-bold text-neutral-900 dark:text-white" />
|
<AnimatedNumber value={m.value} format={m.format} className="text-2xl font-bold text-white" />
|
||||||
{m.change !== null && (
|
{m.change !== null && (
|
||||||
<span className={cn('flex items-center gap-0.5 text-sm font-semibold', m.isPositive ? 'text-[#10B981]' : 'text-[#EF4444]')}>
|
<span className={cn('flex items-center gap-0.5 text-sm font-semibold', m.isPositive ? 'text-[#10B981]' : 'text-[#EF4444]')}>
|
||||||
{m.isPositive ? <ArrowUpRight weight="bold" className="size-3.5" /> : <ArrowDownRight weight="bold" className="size-3.5" />}
|
{m.isPositive ? <ArrowUpRight weight="bold" className="size-3.5" /> : <ArrowDownRight weight="bold" className="size-3.5" />}
|
||||||
@@ -357,7 +357,7 @@ export default function Chart({
|
|||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="flex items-center justify-between gap-3 mb-4 px-2">
|
<div className="flex items-center justify-between gap-3 mb-4 px-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
<span className="text-xs font-medium text-neutral-400">
|
||||||
{METRIC_CONFIGS.find((m) => m.key === metric)?.label}
|
{METRIC_CONFIGS.find((m) => m.key === metric)?.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -526,7 +526,7 @@ export default function Chart({
|
|||||||
<span className="font-medium text-neutral-400 dark:text-neutral-500">
|
<span className="font-medium text-neutral-400 dark:text-neutral-500">
|
||||||
{ANNOTATION_LABELS[a.category] || 'Note'} · {formatEU(a.date)}{a.time ? ` at ${a.time}` : ''}
|
{ANNOTATION_LABELS[a.category] || 'Note'} · {formatEU(a.date)}{a.time ? ` at ${a.time}` : ''}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-neutral-900 dark:text-white">{a.text}</p>
|
<p className="text-white">{a.text}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -593,16 +593,16 @@ export default function Chart({
|
|||||||
{annotationForm.visible && (
|
{annotationForm.visible && (
|
||||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/20 dark:bg-black/40 rounded-2xl">
|
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/20 dark:bg-black/40 rounded-2xl">
|
||||||
<div className="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl p-5 w-[340px] max-w-[90%]">
|
<div className="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl p-5 w-[340px] max-w-[90%]">
|
||||||
<h3 className="text-sm font-semibold text-neutral-900 dark:text-white mb-3">
|
<h3 className="text-sm font-semibold text-white mb-3">
|
||||||
{annotationForm.editingId ? 'Edit annotation' : 'Add annotation'}
|
{annotationForm.editingId ? 'Edit annotation' : 'Add annotation'}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-1">Date</label>
|
<label className="block text-xs font-medium text-neutral-400 mb-1">Date</label>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCalendarOpen(true)}
|
onClick={() => setCalendarOpen(true)}
|
||||||
className="w-full px-3 py-1.5 text-sm rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-orange/30 text-left flex items-center justify-between"
|
className="w-full px-3 py-1.5 text-sm rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-white focus:outline-none focus:ring-2 focus:ring-brand-orange/30 text-left flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<span>{annotationForm.date ? formatEU(annotationForm.date) : 'Select date'}</span>
|
<span>{annotationForm.date ? formatEU(annotationForm.date) : 'Select date'}</span>
|
||||||
<svg className="w-4 h-4 text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="w-4 h-4 text-neutral-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@@ -611,7 +611,7 @@ export default function Chart({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-1">
|
<label className="block text-xs font-medium text-neutral-400 mb-1">
|
||||||
Time <span className="text-neutral-400 dark:text-neutral-500">(optional)</span>
|
Time <span className="text-neutral-400 dark:text-neutral-500">(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -619,7 +619,7 @@ export default function Chart({
|
|||||||
type="time"
|
type="time"
|
||||||
value={annotationForm.time}
|
value={annotationForm.time}
|
||||||
onChange={(e) => setAnnotationForm((f) => ({ ...f, time: e.target.value }))}
|
onChange={(e) => setAnnotationForm((f) => ({ ...f, time: e.target.value }))}
|
||||||
className="flex-1 px-3 py-1.5 text-sm rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-orange/30"
|
className="flex-1 px-3 py-1.5 text-sm rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-white focus:outline-none focus:ring-2 focus:ring-brand-orange/30"
|
||||||
/>
|
/>
|
||||||
{annotationForm.time && (
|
{annotationForm.time && (
|
||||||
<button
|
<button
|
||||||
@@ -634,20 +634,20 @@ export default function Chart({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-1">Note</label>
|
<label className="block text-xs font-medium text-neutral-400 mb-1">Note</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={annotationForm.text}
|
value={annotationForm.text}
|
||||||
onChange={(e) => setAnnotationForm((f) => ({ ...f, text: e.target.value.slice(0, 200) }))}
|
onChange={(e) => setAnnotationForm((f) => ({ ...f, text: e.target.value.slice(0, 200) }))}
|
||||||
placeholder="e.g. Launched new homepage"
|
placeholder="e.g. Launched new homepage"
|
||||||
maxLength={200}
|
maxLength={200}
|
||||||
className="w-full px-3 py-1.5 text-sm rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-orange/30"
|
className="w-full px-3 py-1.5 text-sm rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-white focus:outline-none focus:ring-2 focus:ring-brand-orange/30"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<span className="text-[10px] text-neutral-400 mt-0.5 block text-right">{annotationForm.text.length}/200</span>
|
<span className="text-[10px] text-neutral-400 mt-0.5 block text-right">{annotationForm.text.length}/200</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-1">Category</label>
|
<label className="block text-xs font-medium text-neutral-400 mb-1">Category</label>
|
||||||
<Select
|
<Select
|
||||||
value={annotationForm.category}
|
value={annotationForm.category}
|
||||||
onChange={(v) => setAnnotationForm((f) => ({ ...f, category: v }))}
|
onChange={(v) => setAnnotationForm((f) => ({ ...f, category: v }))}
|
||||||
@@ -675,7 +675,7 @@ export default function Chart({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAnnotationForm({ visible: false, date: '', time: '', text: '', category: 'other' })}
|
onClick={() => setAnnotationForm({ visible: false, date: '', time: '', text: '', category: 'other' })}
|
||||||
className="px-3 py-1.5 text-xs font-medium text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 cursor-pointer"
|
className="px-3 py-1.5 text-xs font-medium text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 cursor-pointer"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
{/* Progress Bar */}
|
{/* Progress Bar */}
|
||||||
{(isExporting || exportDone) && (
|
{(isExporting || exportDone) && (
|
||||||
<div className="space-y-2 pt-2">
|
<div className="space-y-2 pt-2">
|
||||||
<div className="flex items-center justify-between text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="flex items-center justify-between text-xs text-neutral-400">
|
||||||
<span>{exportDone ? 'Export complete' : exportProgress.label}</span>
|
<span>{exportDone ? 'Export complete' : exportProgress.label}</span>
|
||||||
<span>{exportDone ? '100%' : `${Math.round((exportProgress.step / exportProgress.total) * 100)}%`}</span>
|
<span>{exportDone ? '100%' : `${Math.round((exportProgress.step / exportProgress.total) * 100)}%`}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function FilterBar({ filters, onRemove, onClear }: FilterBarProps
|
|||||||
{filters.length > 1 && (
|
{filters.length > 1 && (
|
||||||
<button
|
<button
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
className="px-2 py-1.5 text-xs font-medium text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors cursor-pointer"
|
className="px-2 py-1.5 text-xs font-medium text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
Clear all
|
Clear all
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function SiteNav({ siteId }: SiteNavProps) {
|
|||||||
tabIndex={isActive(tab.href) ? 0 : -1}
|
tabIndex={isActive(tab.href) ? 0 : -1}
|
||||||
className={`relative shrink-0 whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded-t cursor-pointer -mb-px ${
|
className={`relative shrink-0 whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded-t cursor-pointer -mb-px ${
|
||||||
isActive(tab.href)
|
isActive(tab.href)
|
||||||
? 'text-neutral-900 dark:text-white'
|
? 'text-white'
|
||||||
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ArrowSquareOut className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" />
|
<ArrowSquareOut className="w-5 h-5 text-neutral-400 dark:text-neutral-500" weight="bold" />
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-white">
|
||||||
Referrers
|
Referrers
|
||||||
</h3>
|
</h3>
|
||||||
{showViewAll && (
|
{showViewAll && (
|
||||||
@@ -115,7 +115,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
<div className="space-y-2 flex-1 min-h-[270px]">
|
<div className="space-y-2 flex-1 min-h-[270px]">
|
||||||
{!collectReferrers ? (
|
{!collectReferrers ? (
|
||||||
<div className="h-full flex flex-col items-center justify-center text-center px-4">
|
<div className="h-full flex flex-col items-center justify-center text-center px-4">
|
||||||
<p className="text-neutral-500 dark:text-neutral-400 text-sm">Referrer tracking is disabled in site settings</p>
|
<p className="text-neutral-400 text-sm">Referrer tracking is disabled in site settings</p>
|
||||||
</div>
|
</div>
|
||||||
) : hasData ? (
|
) : hasData ? (
|
||||||
<>
|
<>
|
||||||
@@ -132,7 +132,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all"
|
className="absolute inset-y-0.5 left-0.5 bg-brand-orange/15 dark:bg-brand-orange/40 rounded-md transition-all"
|
||||||
style={{ width: `${barWidth}%` }}
|
style={{ width: `${barWidth}%` }}
|
||||||
/>
|
/>
|
||||||
<div className="relative flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
<div className="relative flex-1 truncate text-white flex items-center gap-3">
|
||||||
{renderReferrerIcon(ref.referrer)}
|
{renderReferrerIcon(ref.referrer)}
|
||||||
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>
|
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,12 +154,12 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
) : (
|
) : (
|
||||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||||
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
<GlobeIcon className="w-8 h-8 text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
<h4 className="font-semibold text-white">
|
||||||
No referrers yet
|
No referrers yet
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
<p className="text-sm text-neutral-400 max-w-xs">
|
||||||
Traffic sources will appear here when visitors come from external sites.
|
Traffic sources will appear here when visitors come from external sites.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
@@ -186,7 +186,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
value={modalSearch}
|
value={modalSearch}
|
||||||
onChange={(e) => setModalSearch(e.target.value)}
|
onChange={(e) => setModalSearch(e.target.value)}
|
||||||
placeholder="Search referrers..."
|
placeholder="Search referrers..."
|
||||||
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[80vh]">
|
<div className="max-h-[80vh]">
|
||||||
@@ -208,7 +208,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
onClick={() => { if (onFilter) { onFilter({ dimension: 'referrer', operator: 'is', values: [ref.referrer] }); setIsModalOpen(false) } }}
|
onClick={() => { if (onFilter) { onFilter({ dimension: 'referrer', operator: 'is', values: [ref.referrer] }); setIsModalOpen(false) } }}
|
||||||
className={`flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
|
className={`flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
<div className="flex-1 truncate text-white flex items-center gap-3">
|
||||||
{renderReferrerIcon(ref.referrer)}
|
{renderReferrerIcon(ref.referrer)}
|
||||||
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>
|
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default function BreakdownDrawer({ siteId, funnelId, stepIndex, stepName,
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-800">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-800">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-neutral-900 dark:text-white">Step Breakdown</h3>
|
<h3 className="font-semibold text-white">Step Breakdown</h3>
|
||||||
<p className="text-sm text-neutral-500">{stepName}</p>
|
<p className="text-sm text-neutral-500">{stepName}</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} className="p-2 text-neutral-400 hover:text-neutral-600 rounded-lg">
|
<button onClick={onClose} className="p-2 text-neutral-400 hover:text-neutral-600 rounded-lg">
|
||||||
@@ -91,7 +91,7 @@ export default function BreakdownDrawer({ siteId, funnelId, stepIndex, stepName,
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{breakdown.entries.map(entry => (
|
{breakdown.entries.map(entry => (
|
||||||
<div key={entry.value} className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-800/50">
|
<div key={entry.value} className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-800/50">
|
||||||
<span className="text-sm text-neutral-900 dark:text-white truncate mr-4">
|
<span className="text-sm text-white truncate mr-4">
|
||||||
{entry.value || '(unknown)'}
|
{entry.value || '(unknown)'}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-4 text-sm shrink-0">
|
<div className="flex items-center gap-4 text-sm shrink-0">
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export default function FunnelForm({ siteId, initialData, onSubmit, submitLabel,
|
|||||||
Back to Funnels
|
Back to Funnels
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
<h1 className="text-2xl font-bold text-white mb-2">
|
||||||
{initialData ? 'Edit Funnel' : 'Create New Funnel'}
|
{initialData ? 'Edit Funnel' : 'Create New Funnel'}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-neutral-600 dark:text-neutral-400">
|
<p className="text-neutral-600 dark:text-neutral-400">
|
||||||
@@ -252,7 +252,7 @@ export default function FunnelForm({ siteId, initialData, onSubmit, submitLabel,
|
|||||||
{/* Steps */}
|
{/* Steps */}
|
||||||
<div className="space-y-4 mb-6">
|
<div className="space-y-4 mb-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-white">
|
||||||
Funnel Steps
|
Funnel Steps
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ function ColumnHeader({
|
|||||||
{column.index === 0 ? 'Entry' : `Step ${column.index}`}
|
{column.index === 0 ? 'Entry' : `Step ${column.index}`}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-baseline gap-1.5">
|
<div className="flex items-baseline gap-1.5">
|
||||||
<span className="text-sm font-semibold text-neutral-900 dark:text-white tabular-nums">
|
<span className="text-sm font-semibold text-white tabular-nums">
|
||||||
{column.totalSessions.toLocaleString()} visitors
|
{column.totalSessions.toLocaleString()} visitors
|
||||||
</span>
|
</span>
|
||||||
{column.dropOffPercent !== 0 && (
|
{column.dropOffPercent !== 0 && (
|
||||||
@@ -235,10 +235,10 @@ function PageRow({
|
|||||||
<span
|
<span
|
||||||
className={`relative flex-1 truncate text-sm ${
|
className={`relative flex-1 truncate text-sm ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'text-neutral-900 dark:text-white font-medium'
|
? 'text-white font-medium'
|
||||||
: isOther
|
: isOther
|
||||||
? 'italic text-neutral-400 dark:text-neutral-500'
|
? 'italic text-neutral-400 dark:text-neutral-500'
|
||||||
: 'text-neutral-900 dark:text-white'
|
: 'text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isOther ? page.path : smartLabel(page.path)}
|
{isOther ? page.path : smartLabel(page.path)}
|
||||||
@@ -561,12 +561,15 @@ export default function ColumnJourney({
|
|||||||
alt="No journey data"
|
alt="No journey data"
|
||||||
className="w-52 h-auto mb-2"
|
className="w-52 h-auto mb-2"
|
||||||
/>
|
/>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
<h4 className="font-semibold text-white">
|
||||||
No journey data yet
|
No journey data yet
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
<p className="text-sm text-neutral-400 max-w-xs">
|
||||||
Navigation flows will appear here as visitors browse through your site.
|
Navigation flows will appear here as visitors browse through your site.
|
||||||
</p>
|
</p>
|
||||||
|
<a href="/installation" target="_blank" rel="noopener noreferrer" className="mt-2 text-sm font-medium text-brand-orange hover:underline">
|
||||||
|
View setup guide
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -510,14 +510,17 @@ export default function SankeyJourney({
|
|||||||
return (
|
return (
|
||||||
<div className="h-[400px] flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
<div className="h-[400px] flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||||
<TreeStructure className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
<TreeStructure className="w-8 h-8 text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
<h4 className="font-semibold text-white">
|
||||||
No journey data yet
|
No journey data yet
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
<p className="text-sm text-neutral-400 max-w-xs">
|
||||||
Navigation flows will appear here as visitors browse through your site.
|
Navigation flows will appear here as visitors browse through your site.
|
||||||
</p>
|
</p>
|
||||||
|
<a href="/installation" target="_blank" rel="noopener noreferrer" className="mt-2 text-sm font-medium text-brand-orange hover:underline">
|
||||||
|
View setup guide
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -528,7 +531,7 @@ export default function SankeyJourney({
|
|||||||
<div className="flex items-center gap-2 mb-3 px-3 py-2 rounded-lg bg-brand-orange/10 text-sm">
|
<div className="flex items-center gap-2 mb-3 px-3 py-2 rounded-lg bg-brand-orange/10 text-sm">
|
||||||
<span className="text-neutral-700 dark:text-neutral-300">
|
<span className="text-neutral-700 dark:text-neutral-300">
|
||||||
Showing flows through{' '}
|
Showing flows through{' '}
|
||||||
<span className="font-medium text-neutral-900 dark:text-white">
|
<span className="font-medium text-white">
|
||||||
{filterPath}
|
{filterPath}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ export default function TopPathsTable({ paths, loading }: TopPathsTableProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-white">
|
||||||
Top Paths
|
Top Paths
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-5">
|
<p className="text-sm text-neutral-400 mb-5">
|
||||||
Most common navigation paths across sessions
|
Most common navigation paths across sessions
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ export default function TopPathsTable({ paths, loading }: TopPathsTableProps) {
|
|||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
className="text-sm text-neutral-900 dark:text-white truncate"
|
className="text-sm text-white truncate"
|
||||||
title={page}
|
title={page}
|
||||||
>
|
>
|
||||||
{smartLabel(page)}
|
{smartLabel(page)}
|
||||||
@@ -113,12 +113,12 @@ export default function TopPathsTable({ paths, loading }: TopPathsTableProps) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
<div className="flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||||
<Path className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
<Path className="w-8 h-8 text-neutral-400" />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
<h4 className="font-semibold text-white">
|
||||||
No path data yet
|
No path data yet
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
<p className="text-sm text-neutral-400 max-w-xs">
|
||||||
Common navigation paths will appear here as visitors browse your site.
|
Common navigation paths will appear here as visitors browse your site.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
|
|||||||
style={anchor === 'right' && fixedPos ? { left: fixedPos.left, top: fixedPos.top, bottom: fixedPos.bottom } : undefined}
|
style={anchor === 'right' && fixedPos ? { left: fixedPos.left, top: fixedPos.top, bottom: fixedPos.bottom } : undefined}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
|
||||||
<h3 className="font-semibold text-neutral-900 dark:text-white">Notifications</h3>
|
<h3 className="font-semibold text-white">Notifications</h3>
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -243,7 +243,7 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
|
|||||||
<div className="p-6 text-center text-red-500 text-sm">{error}</div>
|
<div className="p-6 text-center text-red-500 text-sm">{error}</div>
|
||||||
)}
|
)}
|
||||||
{!loading && !error && (notifications?.length ?? 0) === 0 && (
|
{!loading && !error && (notifications?.length ?? 0) === 0 && (
|
||||||
<div className="p-6 text-center text-neutral-500 dark:text-neutral-400 text-sm">
|
<div className="p-6 text-center text-neutral-400 text-sm">
|
||||||
No notifications yet
|
No notifications yet
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -260,11 +260,11 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{getTypeIcon(n.type)}
|
{getTypeIcon(n.type)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className={`text-sm ${!n.read ? 'font-medium' : ''} text-neutral-900 dark:text-white`}>
|
<p className={`text-sm ${!n.read ? 'font-medium' : ''} text-white`}>
|
||||||
{n.title}
|
{n.title}
|
||||||
</p>
|
</p>
|
||||||
{n.body && (
|
{n.body && (
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5 line-clamp-2">
|
<p className="text-xs text-neutral-400 mt-0.5 line-clamp-2">
|
||||||
{n.body}
|
{n.body}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -283,11 +283,11 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{getTypeIcon(n.type)}
|
{getTypeIcon(n.type)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className={`text-sm ${!n.read ? 'font-medium' : ''} text-neutral-900 dark:text-white`}>
|
<p className={`text-sm ${!n.read ? 'font-medium' : ''} text-white`}>
|
||||||
{n.title}
|
{n.title}
|
||||||
</p>
|
</p>
|
||||||
{n.body && (
|
{n.body && (
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5 line-clamp-2">
|
<p className="text-xs text-neutral-400 mt-0.5 line-clamp-2">
|
||||||
{n.body}
|
{n.body}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -315,7 +315,7 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
|
|||||||
<Link
|
<Link
|
||||||
href="/org-settings?tab=notifications"
|
href="/org-settings?tab=notifications"
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
className="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
|
className="flex items-center gap-2 text-sm text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
|
||||||
>
|
>
|
||||||
<SettingsIcon className="w-4 h-4" aria-hidden="true" />
|
<SettingsIcon className="w-4 h-4" aria-hidden="true" />
|
||||||
Manage settings
|
Manage settings
|
||||||
|
|||||||
@@ -31,19 +31,19 @@ function CustomTooltip({ active, payload, label }: TooltipProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 p-3 shadow-sm shadow-black/5 min-w-[140px]">
|
<div className="rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 p-3 shadow-sm shadow-black/5 min-w-[140px]">
|
||||||
<div className="text-xs text-neutral-500 dark:text-neutral-400 mb-1.5">{label}</div>
|
<div className="text-xs text-neutral-400 mb-1.5">{label}</div>
|
||||||
{clicks && (
|
{clicks && (
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<div className="size-1.5 rounded-full" style={{ backgroundColor: '#FD5E0F' }} />
|
<div className="size-1.5 rounded-full" style={{ backgroundColor: '#FD5E0F' }} />
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">Clicks:</span>
|
<span className="text-neutral-400">Clicks:</span>
|
||||||
<span className="font-semibold text-neutral-900 dark:text-white">{clicks.value.toLocaleString()}</span>
|
<span className="font-semibold text-white">{clicks.value.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{impressions && (
|
{impressions && (
|
||||||
<div className="flex items-center gap-2 text-sm mt-1">
|
<div className="flex items-center gap-2 text-sm mt-1">
|
||||||
<div className="size-1.5 rounded-full" style={{ backgroundColor: '#9CA3AF' }} />
|
<div className="size-1.5 rounded-full" style={{ backgroundColor: '#9CA3AF' }} />
|
||||||
<span className="text-neutral-500 dark:text-neutral-400">Impressions:</span>
|
<span className="text-neutral-400">Impressions:</span>
|
||||||
<span className="font-semibold text-neutral-900 dark:text-white">{impressions.value.toLocaleString()}</span>
|
<span className="font-semibold text-white">{impressions.value.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ export default function OrganizationSettings() {
|
|||||||
// If no org ID, we are in personal organization context, so don't show org settings
|
// If no org ID, we are in personal organization context, so don't show org settings
|
||||||
if (!currentOrgId) {
|
if (!currentOrgId) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 text-center text-neutral-500 dark:text-neutral-400">
|
<div className="p-6 text-center text-neutral-400">
|
||||||
<p>You are in your personal context. Switch to an Organization to manage its settings.</p>
|
<p>You are in your personal context. Switch to an Organization to manage its settings.</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -490,7 +490,7 @@ export default function OrganizationSettings() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">Organization Settings</h1>
|
<h1 className="text-2xl font-bold text-white">Organization Settings</h1>
|
||||||
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
<p className="mt-2 text-neutral-600 dark:text-neutral-400">
|
||||||
Manage your organization workspace and members.
|
Manage your organization workspace and members.
|
||||||
</p>
|
</p>
|
||||||
@@ -580,8 +580,8 @@ export default function OrganizationSettings() {
|
|||||||
{activeTab === 'general' && (
|
{activeTab === 'general' && (
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">General Information</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">General Information</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Basic details about your organization.</p>
|
<p className="text-sm text-neutral-400">Basic details about your organization.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleUpdateOrg} className="space-y-4">
|
<form onSubmit={handleUpdateOrg} className="space-y-4">
|
||||||
@@ -597,7 +597,7 @@ export default function OrganizationSettings() {
|
|||||||
minLength={2}
|
minLength={2}
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className={`bg-white dark:bg-neutral-900 ${!isEditing ? 'text-neutral-500 dark:text-neutral-400' : ''}`}
|
className={`bg-white dark:bg-neutral-900 ${!isEditing ? 'text-neutral-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -606,7 +606,7 @@ export default function OrganizationSettings() {
|
|||||||
Organization Slug
|
Organization Slug
|
||||||
</label>
|
</label>
|
||||||
<div className="flex rounded-xl shadow-sm">
|
<div className="flex rounded-xl shadow-sm">
|
||||||
<span className="inline-flex items-center px-3 rounded-l-xl border border-r-0 border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900 text-neutral-500 dark:text-neutral-400 text-sm">
|
<span className="inline-flex items-center px-3 rounded-l-xl border border-r-0 border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900 text-neutral-400 text-sm">
|
||||||
pulse.ciphera.net/
|
pulse.ciphera.net/
|
||||||
</span>
|
</span>
|
||||||
<Input
|
<Input
|
||||||
@@ -617,10 +617,10 @@ export default function OrganizationSettings() {
|
|||||||
minLength={3}
|
minLength={3}
|
||||||
maxLength={30}
|
maxLength={30}
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
className={`rounded-l-none bg-white dark:bg-neutral-900 ${!isEditing ? 'text-neutral-500 dark:text-neutral-400' : ''}`}
|
className={`rounded-l-none bg-white dark:bg-neutral-900 ${!isEditing ? 'text-neutral-400' : ''}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
<p className="text-xs text-neutral-400">
|
||||||
Changing the slug will change your organization's URL.
|
Changing the slug will change your organization's URL.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -658,7 +658,7 @@ export default function OrganizationSettings() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-red-600 dark:text-red-500 mb-1">Danger Zone</h2>
|
<h2 className="text-2xl font-bold text-red-600 dark:text-red-500 mb-1">Danger Zone</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Irreversible actions for this organization.</p>
|
<p className="text-sm text-neutral-400">Irreversible actions for this organization.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 border border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-900/10 rounded-2xl flex items-center justify-between">
|
<div className="p-6 border border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-900/10 rounded-2xl flex items-center justify-between">
|
||||||
@@ -696,11 +696,11 @@ export default function OrganizationSettings() {
|
|||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
{/* Invite Section */}
|
{/* Invite Section */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Organization Members</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">Organization Members</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-6">Manage who has access to this organization.</p>
|
<p className="text-sm text-neutral-400 mb-6">Manage who has access to this organization.</p>
|
||||||
|
|
||||||
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-4">
|
<div className="bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-4">
|
||||||
<h3 className="text-sm font-medium text-neutral-900 dark:text-white mb-3">Invite New Member</h3>
|
<h3 className="text-sm font-medium text-white mb-3">Invite New Member</h3>
|
||||||
<form onSubmit={handleSendInvite} className="flex gap-3 items-end">
|
<form onSubmit={handleSendInvite} className="flex gap-3 items-end">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Input
|
<Input
|
||||||
@@ -744,12 +744,12 @@ export default function OrganizationSettings() {
|
|||||||
|
|
||||||
{/* Members List */}
|
{/* Members List */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Active Members</h3>
|
<h3 className="text-sm font-medium text-neutral-400 uppercase tracking-wider">Active Members</h3>
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
|
||||||
{isLoadingMembers ? (
|
{isLoadingMembers ? (
|
||||||
<MembersListSkeleton />
|
<MembersListSkeleton />
|
||||||
) : members.length === 0 ? (
|
) : members.length === 0 ? (
|
||||||
<div className="p-8 text-center text-neutral-500 dark:text-neutral-400">No members found.</div>
|
<div className="p-8 text-center text-neutral-400">No members found.</div>
|
||||||
) : (
|
) : (
|
||||||
members.map((member) => (
|
members.map((member) => (
|
||||||
<div key={member.user_id} className="p-4 flex items-center justify-between hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
|
<div key={member.user_id} className="p-4 flex items-center justify-between hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
|
||||||
@@ -758,10 +758,10 @@ export default function OrganizationSettings() {
|
|||||||
{member.user_email?.[0].toUpperCase() || '?'}
|
{member.user_email?.[0].toUpperCase() || '?'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-neutral-900 dark:text-white">
|
<div className="text-sm font-medium text-white">
|
||||||
{member.user_email || 'Unknown User'}
|
{member.user_email || 'Unknown User'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="text-xs text-neutral-400">
|
||||||
Joined {formatDate(new Date(member.joined_at))}
|
Joined {formatDate(new Date(member.joined_at))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -786,7 +786,7 @@ export default function OrganizationSettings() {
|
|||||||
{/* Pending Invitations */}
|
{/* Pending Invitations */}
|
||||||
{invitations.length > 0 && (
|
{invitations.length > 0 && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Pending Invitations</h3>
|
<h3 className="text-sm font-medium text-neutral-400 uppercase tracking-wider">Pending Invitations</h3>
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
|
||||||
{invitations.map((invite) => (
|
{invitations.map((invite) => (
|
||||||
<div key={invite.id} className="p-4 flex items-center justify-between">
|
<div key={invite.id} className="p-4 flex items-center justify-between">
|
||||||
@@ -795,10 +795,10 @@ export default function OrganizationSettings() {
|
|||||||
<div className="w-2 h-2 rounded-full bg-neutral-400 animate-pulse"></div>
|
<div className="w-2 h-2 rounded-full bg-neutral-400 animate-pulse"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-neutral-900 dark:text-white">
|
<div className="text-sm font-medium text-white">
|
||||||
{invite.email}
|
{invite.email}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="text-xs text-neutral-400">
|
||||||
Invited as <span className="capitalize font-medium">{invite.role}</span> • Expires {formatDate(new Date(invite.expires_at))}
|
Invited as <span className="capitalize font-medium">{invite.role}</span> • Expires {formatDate(new Date(invite.expires_at))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -821,8 +821,8 @@ export default function OrganizationSettings() {
|
|||||||
{activeTab === 'billing' && (
|
{activeTab === 'billing' && (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Billing & Subscription</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">Billing & Subscription</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Manage your plan, usage, and payment details.</p>
|
<p className="text-sm text-neutral-400">Manage your plan, usage, and payment details.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoadingSubscription ? (
|
{isLoadingSubscription ? (
|
||||||
@@ -832,7 +832,7 @@ export default function OrganizationSettings() {
|
|||||||
</div>
|
</div>
|
||||||
) : !subscription ? (
|
) : !subscription ? (
|
||||||
<div className="p-6 text-center bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-200 dark:border-neutral-800">
|
<div className="p-6 text-center bg-neutral-50 dark:bg-neutral-900/50 rounded-2xl border border-neutral-200 dark:border-neutral-800">
|
||||||
<p className="text-neutral-500 dark:text-neutral-400">Could not load subscription details.</p>
|
<p className="text-neutral-400">Could not load subscription details.</p>
|
||||||
<Button variant="ghost" onClick={loadSubscription} className="mt-4">Retry</Button>
|
<Button variant="ghost" onClick={loadSubscription} className="mt-4">Retry</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -915,7 +915,7 @@ export default function OrganizationSettings() {
|
|||||||
{/* Plan header */}
|
{/* Plan header */}
|
||||||
<div className="p-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="p-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xl font-bold text-neutral-900 dark:text-white capitalize">
|
<span className="text-xl font-bold text-white capitalize">
|
||||||
{subscription.plan_id?.startsWith('price_') ? 'Pro' : (subscription.plan_id === 'free' || !subscription.plan_id ? 'Free' : subscription.plan_id)} Plan
|
{subscription.plan_id?.startsWith('price_') ? 'Pro' : (subscription.plan_id === 'free' || !subscription.plan_id ? 'Free' : subscription.plan_id)} Plan
|
||||||
</span>
|
</span>
|
||||||
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
|
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
|
||||||
@@ -940,7 +940,7 @@ export default function OrganizationSettings() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{(subscription.business_name || subscription.tax_id) && (
|
{(subscription.business_name || subscription.tax_id) && (
|
||||||
<div className="px-6 pb-2 -mt-2 space-y-1 text-sm text-neutral-500 dark:text-neutral-400">
|
<div className="px-6 pb-2 -mt-2 space-y-1 text-sm text-neutral-400">
|
||||||
{subscription.business_name && (
|
{subscription.business_name && (
|
||||||
<div>Billing for: {subscription.business_name}</div>
|
<div>Billing for: {subscription.business_name}</div>
|
||||||
)}
|
)}
|
||||||
@@ -956,7 +956,7 @@ export default function OrganizationSettings() {
|
|||||||
<div className="border-t border-neutral-200 dark:border-neutral-800 p-6 grid grid-cols-2 md:grid-cols-4 gap-y-4 gap-x-6">
|
<div className="border-t border-neutral-200 dark:border-neutral-800 p-6 grid grid-cols-2 md:grid-cols-4 gap-y-4 gap-x-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Sites</div>
|
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Sites</div>
|
||||||
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<div className="text-lg font-semibold text-white">
|
||||||
{typeof subscription.sites_count === 'number'
|
{typeof subscription.sites_count === 'number'
|
||||||
? (() => {
|
? (() => {
|
||||||
const limit = getSitesLimitForPlan(subscription.plan_id)
|
const limit = getSitesLimitForPlan(subscription.plan_id)
|
||||||
@@ -967,7 +967,7 @@ export default function OrganizationSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Pageviews</div>
|
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Pageviews</div>
|
||||||
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<div className="text-lg font-semibold text-white">
|
||||||
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number'
|
{subscription.pageview_limit > 0 && typeof subscription.pageview_usage === 'number'
|
||||||
? `${subscription.pageview_usage.toLocaleString()} / ${subscription.pageview_limit.toLocaleString()}`
|
? `${subscription.pageview_usage.toLocaleString()} / ${subscription.pageview_limit.toLocaleString()}`
|
||||||
: '—'}
|
: '—'}
|
||||||
@@ -993,7 +993,7 @@ export default function OrganizationSettings() {
|
|||||||
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">
|
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">
|
||||||
{subscription.subscription_status === 'trialing' ? 'Trial ends' : (subscription.cancel_at_period_end ? 'Access until' : 'Renews')}
|
{subscription.subscription_status === 'trialing' ? 'Trial ends' : (subscription.cancel_at_period_end ? 'Access until' : 'Renews')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<div className="text-lg font-semibold text-white">
|
||||||
{(() => {
|
{(() => {
|
||||||
const ts = subscription.current_period_end
|
const ts = subscription.current_period_end
|
||||||
const d = ts ? new Date(ts) : null
|
const d = ts ? new Date(ts) : null
|
||||||
@@ -1005,7 +1005,7 @@ export default function OrganizationSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Limit</div>
|
<div className="text-xs text-neutral-500 uppercase tracking-wider mb-1">Limit</div>
|
||||||
<div className="text-lg font-semibold text-neutral-900 dark:text-white">
|
<div className="text-lg font-semibold text-white">
|
||||||
{subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / mo` : 'Unlimited'}
|
{subscription.pageview_limit > 0 ? `${subscription.pageview_limit.toLocaleString()} / mo` : 'Unlimited'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1038,19 +1038,19 @@ export default function OrganizationSettings() {
|
|||||||
|
|
||||||
{/* Order History */}
|
{/* Order History */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white mb-3">Recent orders</h3>
|
<h3 className="text-lg font-semibold text-white mb-3">Recent orders</h3>
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
|
||||||
{isLoadingInvoices ? (
|
{isLoadingInvoices ? (
|
||||||
<InvoicesListSkeleton />
|
<InvoicesListSkeleton />
|
||||||
) : orders.length === 0 ? (
|
) : orders.length === 0 ? (
|
||||||
<div className="p-8 text-center text-neutral-500 dark:text-neutral-400">No orders found.</div>
|
<div className="p-8 text-center text-neutral-400">No orders found.</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{orders.map((order) => (
|
{orders.map((order) => (
|
||||||
<div key={order.id} className="px-4 py-3 flex items-center justify-between hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
|
<div key={order.id} className="px-4 py-3 flex items-center justify-between hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-sm text-neutral-900 dark:text-white">
|
<span className="font-medium text-sm text-white">
|
||||||
{(order.total_amount / 100).toLocaleString('en-US', { style: 'currency', currency: order.currency.toUpperCase() })}
|
{(order.total_amount / 100).toLocaleString('en-US', { style: 'currency', currency: order.currency.toUpperCase() })}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-neutral-500 ml-2">
|
<span className="text-xs text-neutral-500 ml-2">
|
||||||
@@ -1084,8 +1084,8 @@ export default function OrganizationSettings() {
|
|||||||
{activeTab === 'notifications' && (
|
{activeTab === 'notifications' && (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Notification Settings</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">Notification Settings</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-6">
|
<p className="text-sm text-neutral-400 mb-6">
|
||||||
Choose which notification types you want to receive. These apply to the notification center for owners and admins.
|
Choose which notification types you want to receive. These apply to the notification center for owners and admins.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1094,7 +1094,7 @@ export default function OrganizationSettings() {
|
|||||||
<SettingsFormSkeleton />
|
<SettingsFormSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Notification categories</h3>
|
<h3 className="text-sm font-medium text-neutral-400 uppercase tracking-wider">Notification categories</h3>
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl overflow-hidden divide-y divide-neutral-200 dark:divide-neutral-800">
|
||||||
{notificationCategories.map((cat) => (
|
{notificationCategories.map((cat) => (
|
||||||
<div
|
<div
|
||||||
@@ -1102,8 +1102,8 @@ export default function OrganizationSettings() {
|
|||||||
className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors"
|
className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-medium text-neutral-900 dark:text-white">{cat.label}</p>
|
<p className="text-sm font-medium text-white">{cat.label}</p>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">{cat.description}</p>
|
<p className="text-sm text-neutral-400 mt-0.5">{cat.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center shrink-0">
|
<div className="flex items-center shrink-0">
|
||||||
<button
|
<button
|
||||||
@@ -1149,8 +1149,8 @@ export default function OrganizationSettings() {
|
|||||||
{activeTab === 'audit' && (
|
{activeTab === 'audit' && (
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Audit log</h2>
|
<h2 className="text-2xl font-bold text-white mb-1">Audit log</h2>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Who did what and when for this organization.</p>
|
<p className="text-sm text-neutral-400">Who did what and when for this organization.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Advanced Filters */}
|
{/* Advanced Filters */}
|
||||||
@@ -1163,7 +1163,7 @@ export default function OrganizationSettings() {
|
|||||||
placeholder="e.g. 8a2b3c"
|
placeholder="e.g. 8a2b3c"
|
||||||
value={auditLogIdFilter}
|
value={auditLogIdFilter}
|
||||||
onChange={(e) => setAuditLogIdFilter(e.target.value)}
|
onChange={(e) => setAuditLogIdFilter(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-neutral-900 dark:text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all"
|
className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -1173,7 +1173,7 @@ export default function OrganizationSettings() {
|
|||||||
placeholder="e.g. site_created"
|
placeholder="e.g. site_created"
|
||||||
value={auditActionFilter}
|
value={auditActionFilter}
|
||||||
onChange={(e) => setAuditActionFilter(e.target.value)}
|
onChange={(e) => setAuditActionFilter(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-neutral-900 dark:text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all"
|
className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -1182,7 +1182,7 @@ export default function OrganizationSettings() {
|
|||||||
type="date"
|
type="date"
|
||||||
value={auditStartDate}
|
value={auditStartDate}
|
||||||
onChange={(e) => setAuditStartDate(e.target.value)}
|
onChange={(e) => setAuditStartDate(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-neutral-900 dark:text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all"
|
className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -1191,7 +1191,7 @@ export default function OrganizationSettings() {
|
|||||||
type="date"
|
type="date"
|
||||||
value={auditEndDate}
|
value={auditEndDate}
|
||||||
onChange={(e) => setAuditEndDate(e.target.value)}
|
onChange={(e) => setAuditEndDate(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-neutral-900 dark:text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all"
|
className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-sm text-white focus:ring-2 focus:ring-brand-orange outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1240,10 +1240,10 @@ export default function OrganizationSettings() {
|
|||||||
<td className="px-4 py-3 text-neutral-600 dark:text-neutral-400 whitespace-nowrap">
|
<td className="px-4 py-3 text-neutral-600 dark:text-neutral-400 whitespace-nowrap">
|
||||||
{formatDateTime(new Date(entry.occurred_at))}
|
{formatDateTime(new Date(entry.occurred_at))}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-neutral-900 dark:text-white whitespace-nowrap" title={entry.actor_email || entry.actor_id || 'System'}>
|
<td className="px-4 py-3 text-white whitespace-nowrap" title={entry.actor_email || entry.actor_id || 'System'}>
|
||||||
{entry.actor_email || entry.actor_id || 'System'}
|
{entry.actor_email || entry.actor_id || 'System'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 font-medium text-neutral-900 dark:text-white whitespace-nowrap" title={entry.action}>{entry.action}</td>
|
<td className="px-4 py-3 font-medium text-white whitespace-nowrap" title={entry.action}>{entry.action}</td>
|
||||||
<td className="px-4 py-3 text-neutral-600 dark:text-neutral-400">{entry.resource_type}</td>
|
<td className="px-4 py-3 text-neutral-600 dark:text-neutral-400">{entry.resource_type}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -1255,7 +1255,7 @@ export default function OrganizationSettings() {
|
|||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{auditTotal > auditPageSize && (
|
{auditTotal > auditPageSize && (
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-t border-neutral-200 dark:border-neutral-800">
|
<div className="flex items-center justify-between px-4 py-3 border-t border-neutral-200 dark:border-neutral-800">
|
||||||
<span className="text-sm text-neutral-500 dark:text-neutral-400">
|
<span className="text-sm text-neutral-400">
|
||||||
{auditPage * auditPageSize + 1}–{Math.min((auditPage + 1) * auditPageSize, auditTotal)} of {auditTotal}
|
{auditPage * auditPageSize + 1}–{Math.min((auditPage + 1) * auditPageSize, auditTotal)} of {auditTotal}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -1361,7 +1361,7 @@ export default function OrganizationSettings() {
|
|||||||
value={deleteConfirm}
|
value={deleteConfirm}
|
||||||
onChange={(e) => setDeleteConfirm(e.target.value)}
|
onChange={(e) => setDeleteConfirm(e.target.value)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="w-full px-3 py-2 text-sm border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400"
|
className="w-full px-3 py-2 text-sm border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400"
|
||||||
placeholder="DELETE"
|
placeholder="DELETE"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1410,7 +1410,7 @@ export default function OrganizationSettings() {
|
|||||||
className="bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl max-w-md w-full p-6 border border-neutral-200 dark:border-neutral-800"
|
className="bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl max-w-md w-full p-6 border border-neutral-200 dark:border-neutral-800"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">Cancel subscription?</h3>
|
<h3 className="text-lg font-semibold text-white">Cancel subscription?</h3>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCancelPrompt(false)}
|
onClick={() => setShowCancelPrompt(false)}
|
||||||
className="text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-400"
|
className="text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-400"
|
||||||
@@ -1464,7 +1464,7 @@ export default function OrganizationSettings() {
|
|||||||
className="bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl max-w-md w-full p-6 border border-neutral-200 dark:border-neutral-800"
|
className="bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl max-w-md w-full p-6 border border-neutral-200 dark:border-neutral-800"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">Change plan</h3>
|
<h3 className="text-lg font-semibold text-white">Change plan</h3>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowChangePlanModal(false)}
|
onClick={() => setShowChangePlanModal(false)}
|
||||||
@@ -1500,7 +1500,7 @@ export default function OrganizationSettings() {
|
|||||||
: 'border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600'
|
: 'border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className={`block text-sm font-semibold ${isSelected ? 'text-brand-orange' : 'text-neutral-900 dark:text-white'}`}>
|
<span className={`block text-sm font-semibold ${isSelected ? 'text-brand-orange' : 'text-white'}`}>
|
||||||
{plan.name}
|
{plan.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="block text-xs text-neutral-500 mt-0.5">{plan.sites}</span>
|
<span className="block text-xs text-neutral-500 mt-0.5">{plan.sites}</span>
|
||||||
@@ -1519,7 +1519,7 @@ export default function OrganizationSettings() {
|
|||||||
<select
|
<select
|
||||||
value={changePlanTierIndex}
|
value={changePlanTierIndex}
|
||||||
onChange={(e) => setChangePlanTierIndex(Number(e.target.value))}
|
onChange={(e) => setChangePlanTierIndex(Number(e.target.value))}
|
||||||
className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white focus:ring-2 focus:ring-brand-orange outline-none"
|
className="w-full px-3 py-2 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 text-white focus:ring-2 focus:ring-brand-orange outline-none"
|
||||||
>
|
>
|
||||||
{TRAFFIC_TIERS.map((tier, idx) => (
|
{TRAFFIC_TIERS.map((tier, idx) => (
|
||||||
<option key={tier.value} value={idx}>
|
<option key={tier.value} value={idx}>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function getEventColor(eventType: string, outcome: string): string {
|
|||||||
if (eventType === '2fa_disabled') return 'text-amber-500 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30'
|
if (eventType === '2fa_disabled') return 'text-amber-500 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30'
|
||||||
if (eventType === 'account_deleted') return 'text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-950/30'
|
if (eventType === 'account_deleted') return 'text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-950/30'
|
||||||
if (eventType === 'recovery_codes_regenerated') return 'text-amber-500 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30'
|
if (eventType === 'recovery_codes_regenerated') return 'text-amber-500 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30'
|
||||||
return 'text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-neutral-800'
|
return 'text-neutral-400 bg-neutral-100 dark:bg-neutral-800'
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMethodLabel(entry: AuditLogEntry): string | null {
|
function getMethodLabel(entry: AuditLogEntry): string | null {
|
||||||
@@ -120,8 +120,8 @@ export default function SecurityActivityCard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-1">Security Activity</h2>
|
<h2 className="text-xl font-semibold text-white mb-1">Security Activity</h2>
|
||||||
<p className="text-neutral-500 dark:text-neutral-400 text-sm mb-6">
|
<p className="text-neutral-400 text-sm mb-6">
|
||||||
Recent security events on your account{totalCount > 0 ? ` (${totalCount})` : ''}
|
Recent security events on your account{totalCount > 0 ? ` (${totalCount})` : ''}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ export default function SecurityActivityCard() {
|
|||||||
<svg className="w-12 h-12 mx-auto mb-3 text-neutral-300 dark:text-neutral-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-12 h-12 mx-auto mb-3 text-neutral-300 dark:text-neutral-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-neutral-500 dark:text-neutral-400">No activity recorded yet.</p>
|
<p className="text-neutral-400">No activity recorded yet.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -165,11 +165,11 @@ export default function SecurityActivityCard() {
|
|||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="font-medium text-neutral-900 dark:text-white text-sm">
|
<span className="font-medium text-white text-sm">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{method && (
|
{method && (
|
||||||
<span className="text-xs px-1.5 py-0.5 rounded bg-neutral-100 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-400">
|
<span className="text-xs px-1.5 py-0.5 rounded bg-neutral-100 dark:bg-neutral-800 text-neutral-400">
|
||||||
{method}
|
{method}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -179,7 +179,7 @@ export default function SecurityActivityCard() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-0.5 text-xs text-neutral-500 dark:text-neutral-400 flex-wrap">
|
<div className="flex items-center gap-2 mt-0.5 text-xs text-neutral-400 flex-wrap">
|
||||||
{reason && <span>{reason}</span>}
|
{reason && <span>{reason}</span>}
|
||||||
{reason && (deviceStr || entry.ip_address) && <span>·</span>}
|
{reason && (deviceStr || entry.ip_address) && <span>·</span>}
|
||||||
{deviceStr && <span>{deviceStr}</span>}
|
{deviceStr && <span>{deviceStr}</span>}
|
||||||
@@ -189,7 +189,7 @@ export default function SecurityActivityCard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0 text-right">
|
<div className="flex-shrink-0 text-right">
|
||||||
<span className="text-xs text-neutral-500 dark:text-neutral-400" title={formatDateTimeFull(new Date(entry.created_at))}>
|
<span className="text-xs text-neutral-400" title={formatDateTimeFull(new Date(entry.created_at))}>
|
||||||
{formatRelativeTime(entry.created_at)}
|
{formatRelativeTime(entry.created_at)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function NotificationCenterPlaceholder() {
|
|||||||
return (
|
return (
|
||||||
<div className="text-center max-w-md mx-auto py-8">
|
<div className="text-center max-w-md mx-auto py-8">
|
||||||
<BellIcon className="w-12 h-12 text-neutral-300 mx-auto mb-4" />
|
<BellIcon className="w-12 h-12 text-neutral-300 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-medium text-neutral-900 dark:text-white mb-2">Notification Center</h3>
|
<h3 className="text-lg font-medium text-white mb-2">Notification Center</h3>
|
||||||
<p className="text-sm text-neutral-500 mb-4">View and manage all your notifications in one place.</p>
|
<p className="text-sm text-neutral-500 mb-4">View and manage all your notifications in one place.</p>
|
||||||
<Link href="/notifications" className="inline-flex items-center gap-2 px-4 py-2 bg-brand-orange text-white rounded-lg hover:bg-brand-orange/90 transition-colors">
|
<Link href="/notifications" className="inline-flex items-center gap-2 px-4 py-2 bg-brand-orange text-white rounded-lg hover:bg-brand-orange/90 transition-colors">
|
||||||
Open Notification Center
|
Open Notification Center
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ export default function TrustedDevicesCard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-1">Trusted Devices</h2>
|
<h2 className="text-xl font-semibold text-white mb-1">Trusted Devices</h2>
|
||||||
<p className="text-neutral-500 dark:text-neutral-400 text-sm mb-6">
|
<p className="text-neutral-400 text-sm mb-6">
|
||||||
Devices that have signed in to your account. Removing a device means the next sign-in from it will trigger a new device alert.
|
Devices that have signed in to your account. Removing a device means the next sign-in from it will trigger a new device alert.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ export default function TrustedDevicesCard() {
|
|||||||
<svg className="w-12 h-12 mx-auto mb-3 text-neutral-300 dark:text-neutral-600" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24">
|
<svg className="w-12 h-12 mx-auto mb-3 text-neutral-300 dark:text-neutral-600" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25A2.25 2.25 0 015.25 3h13.5A2.25 2.25 0 0121 5.25z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25A2.25 2.25 0 015.25 3h13.5A2.25 2.25 0 0121 5.25z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-neutral-500 dark:text-neutral-400">No trusted devices yet. They appear after you sign in.</p>
|
<p className="text-neutral-400">No trusted devices yet. They appear after you sign in.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -83,7 +83,7 @@ export default function TrustedDevicesCard() {
|
|||||||
key={device.id}
|
key={device.id}
|
||||||
className="flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-4 py-3"
|
className="flex items-center gap-3 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-4 py-3"
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0 w-9 h-9 rounded-lg flex items-center justify-center bg-neutral-100 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-400">
|
<div className="flex-shrink-0 w-9 h-9 rounded-lg flex items-center justify-center bg-neutral-100 dark:bg-neutral-800 text-neutral-400">
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d={getDeviceIcon(device.display_hint)} />
|
<path strokeLinecap="round" strokeLinejoin="round" d={getDeviceIcon(device.display_hint)} />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -91,7 +91,7 @@ export default function TrustedDevicesCard() {
|
|||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-neutral-900 dark:text-white text-sm truncate">
|
<span className="font-medium text-white text-sm truncate">
|
||||||
{device.display_hint || 'Unknown device'}
|
{device.display_hint || 'Unknown device'}
|
||||||
</span>
|
</span>
|
||||||
{device.is_current && (
|
{device.is_current && (
|
||||||
@@ -100,7 +100,7 @@ export default function TrustedDevicesCard() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-0.5 text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="flex items-center gap-2 mt-0.5 text-xs text-neutral-400">
|
||||||
<span title={formatDateTimeFull(new Date(device.first_seen_at))}>
|
<span title={formatDateTimeFull(new Date(device.first_seen_at))}>
|
||||||
First seen {formatRelativeTime(device.first_seen_at)}
|
First seen {formatRelativeTime(device.first_seen_at)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export default function DeleteSiteModal({ open, onClose, onDeleted, siteName, si
|
|||||||
value={deleteConfirm}
|
value={deleteConfirm}
|
||||||
onChange={(e) => setDeleteConfirm(e.target.value)}
|
onChange={(e) => setDeleteConfirm(e.target.value)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="w-full px-3 py-2 text-sm border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400"
|
className="w-full px-3 py-2 text-sm border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400"
|
||||||
placeholder="DELETE"
|
placeholder="DELETE"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,7 +187,7 @@ export default function DeleteSiteModal({ open, onClose, onDeleted, siteName, si
|
|||||||
value={permanentConfirm}
|
value={permanentConfirm}
|
||||||
onChange={(e) => setPermanentConfirm(e.target.value)}
|
onChange={(e) => setPermanentConfirm(e.target.value)}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="w-full px-3 py-2 text-sm border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400"
|
className="w-full px-3 py-2 text-sm border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400"
|
||||||
placeholder={siteDomain}
|
placeholder={siteDomain}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export default function ScriptSetupBlock({
|
|||||||
|
|
||||||
{/* ── Feature toggles ─────────────────────────────────────────────── */}
|
{/* ── Feature toggles ─────────────────────────────────────────────── */}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h4 className="text-sm font-semibold text-neutral-900 dark:text-white mb-3">
|
<h4 className="text-sm font-semibold text-white mb-3">
|
||||||
Features
|
Features
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
@@ -177,10 +177,10 @@ export default function ScriptSetupBlock({
|
|||||||
className="flex items-center justify-between rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-4 py-3"
|
className="flex items-center justify-between rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-4 py-3"
|
||||||
>
|
>
|
||||||
<div className="min-w-0 mr-3">
|
<div className="min-w-0 mr-3">
|
||||||
<span className="text-sm font-medium text-neutral-900 dark:text-white block">
|
<span className="text-sm font-medium text-white block">
|
||||||
{f.label}
|
{f.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">
|
<span className="text-xs text-neutral-400">
|
||||||
{f.description}
|
{f.description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,10 +191,10 @@ export default function ScriptSetupBlock({
|
|||||||
{/* * Frustration — full-width, visually distinct as add-on */}
|
{/* * Frustration — full-width, visually distinct as add-on */}
|
||||||
<div className="mt-3 flex items-center justify-between rounded-xl border border-dashed border-neutral-300 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-900/50 px-4 py-3">
|
<div className="mt-3 flex items-center justify-between rounded-xl border border-dashed border-neutral-300 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-900/50 px-4 py-3">
|
||||||
<div className="min-w-0 mr-3">
|
<div className="min-w-0 mr-3">
|
||||||
<span className="text-sm font-medium text-neutral-900 dark:text-white block">
|
<span className="text-sm font-medium text-white block">
|
||||||
Frustration tracking
|
Frustration tracking
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">
|
<span className="text-xs text-neutral-400">
|
||||||
Rage clicks & dead clicks · Loads separate add-on script
|
Rage clicks & dead clicks · Loads separate add-on script
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,15 +204,15 @@ export default function ScriptSetupBlock({
|
|||||||
|
|
||||||
{/* ── Storage + TTL ───────────────────────────────────────────────── */}
|
{/* ── Storage + TTL ───────────────────────────────────────────────── */}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h4 className="text-sm font-semibold text-neutral-900 dark:text-white mb-1">
|
<h4 className="text-sm font-semibold text-white mb-1">
|
||||||
Visitor identity
|
Visitor identity
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-3">
|
<p className="text-xs text-neutral-400 mb-3">
|
||||||
How returning visitors are recognized. Stricter settings increase privacy but may raise unique visitor counts.
|
How returning visitors are recognized. Stricter settings increase privacy but may raise unique visitor counts.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-end gap-3">
|
<div className="flex items-end gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<label className="text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-1 block">
|
<label className="text-xs font-medium text-neutral-400 mb-1 block">
|
||||||
Recognition
|
Recognition
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
@@ -224,7 +224,7 @@ export default function ScriptSetupBlock({
|
|||||||
</div>
|
</div>
|
||||||
{storage === 'local' && (
|
{storage === 'local' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-1 block">
|
<label className="text-xs font-medium text-neutral-400 mb-1 block">
|
||||||
Reset after
|
Reset after
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
@@ -242,14 +242,14 @@ export default function ScriptSetupBlock({
|
|||||||
{showFrameworkPicker && (
|
{showFrameworkPicker && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h4 className="text-sm font-semibold text-neutral-900 dark:text-white">
|
<h4 className="text-sm font-semibold text-white">
|
||||||
Setup guide
|
Setup guide
|
||||||
</h4>
|
</h4>
|
||||||
<Link
|
<Link
|
||||||
href="/integrations"
|
href="/integrations"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-xs font-medium text-neutral-500 dark:text-neutral-400 hover:text-brand-orange transition-colors"
|
className="text-xs font-medium text-neutral-400 hover:text-brand-orange transition-colors"
|
||||||
>
|
>
|
||||||
All integrations →
|
All integrations →
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ function SiteCard({ site, stats, statsLoading, onDelete, canDelete }: SiteCardPr
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-neutral-900 dark:text-white">{site.name}</h3>
|
<h3 className="font-semibold text-white">{site.name}</h3>
|
||||||
<div className="flex items-center gap-1 text-sm text-neutral-500 dark:text-neutral-400">
|
<div className="flex items-center gap-1 text-sm text-neutral-400">
|
||||||
{site.domain}
|
{site.domain}
|
||||||
<a
|
<a
|
||||||
href={`https://${site.domain}`}
|
href={`https://${site.domain}`}
|
||||||
@@ -84,13 +84,13 @@ function SiteCard({ site, stats, statsLoading, onDelete, canDelete }: SiteCardPr
|
|||||||
<div className="mb-6 grid grid-cols-2 gap-4 rounded-lg bg-neutral-50 p-3 dark:bg-neutral-800/50">
|
<div className="mb-6 grid grid-cols-2 gap-4 rounded-lg bg-neutral-50 p-3 dark:bg-neutral-800/50">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-neutral-500">Visitors (24h)</p>
|
<p className="text-xs text-neutral-500">Visitors (24h)</p>
|
||||||
<p className="font-mono text-lg font-medium text-neutral-900 dark:text-white">
|
<p className="font-mono text-lg font-medium text-white">
|
||||||
{statsLoading ? '--' : formatNumber(visitors24h)}
|
{statsLoading ? '--' : formatNumber(visitors24h)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-neutral-500">Pageviews</p>
|
<p className="text-xs text-neutral-500">Pageviews</p>
|
||||||
<p className="font-mono text-lg font-medium text-neutral-900 dark:text-white">
|
<p className="font-mono text-lg font-medium text-white">
|
||||||
{statsLoading ? '--' : formatNumber(pageviews)}
|
{statsLoading ? '--' : formatNumber(pageviews)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,8 +144,8 @@ export default function SiteList({ sites, siteStats, loading, onDelete }: SiteLi
|
|||||||
className="mb-6"
|
className="mb-6"
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">No sites yet</h3>
|
<h3 className="text-lg font-semibold text-white">No sites yet</h3>
|
||||||
<p className="mt-2 text-sm text-neutral-500 dark:text-neutral-400 mb-4">Create your first site to get started.</p>
|
<p className="mt-2 text-sm text-neutral-400 mb-4">Create your first site to get started.</p>
|
||||||
<Link href="/sites/new">
|
<Link href="/sites/new">
|
||||||
<Button variant="primary" className="text-sm">
|
<Button variant="primary" className="text-sm">
|
||||||
Add your first site
|
Add your first site
|
||||||
@@ -176,8 +176,8 @@ export default function SiteList({ sites, siteStats, loading, onDelete }: SiteLi
|
|||||||
<div className="mb-3 rounded-full bg-neutral-200 p-3 dark:bg-neutral-800">
|
<div className="mb-3 rounded-full bg-neutral-200 p-3 dark:bg-neutral-800">
|
||||||
<BookOpenIcon className="h-6 w-6 text-neutral-500" />
|
<BookOpenIcon className="h-6 w-6 text-neutral-500" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-semibold text-neutral-900 dark:text-white">Need help setup?</h3>
|
<h3 className="font-semibold text-white">Need help setup?</h3>
|
||||||
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">Check our documentation for installation guides.</p>
|
<p className="mb-4 text-sm text-neutral-400">Check our documentation for installation guides.</p>
|
||||||
<Link href="https://docs.ciphera.net" target="_blank" className="text-sm font-medium text-brand-orange hover:underline">
|
<Link href="https://docs.ciphera.net" target="_blank" className="text-sm font-medium text-brand-orange hover:underline">
|
||||||
Read Documentation →
|
Read Documentation →
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function VerificationModal({ isOpen, onClose, site, onVerified }:
|
|||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-neutral-100 dark:border-neutral-800">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-neutral-100 dark:border-neutral-800">
|
||||||
<h3 className="font-semibold text-neutral-900 dark:text-white">
|
<h3 className="font-semibold text-white">
|
||||||
Verify Installation
|
Verify Installation
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
@@ -148,10 +148,10 @@ export default function VerificationModal({ isOpen, onClose, site, onVerified }:
|
|||||||
<div className="absolute inset-0 w-16 h-16 border-4 border-brand-orange border-t-transparent rounded-full animate-spin" />
|
<div className="absolute inset-0 w-16 h-16 border-4 border-brand-orange border-t-transparent rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center space-y-1">
|
<div className="text-center space-y-1">
|
||||||
<h4 className="font-medium text-neutral-900 dark:text-white">
|
<h4 className="font-medium text-white">
|
||||||
Checking connection...
|
Checking connection...
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
<p className="text-sm text-neutral-400">
|
||||||
Waiting for signal from {site.domain}
|
Waiting for signal from {site.domain}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,10 +164,10 @@ export default function VerificationModal({ isOpen, onClose, site, onVerified }:
|
|||||||
<CheckCircleIcon className="w-8 h-8" />
|
<CheckCircleIcon className="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center space-y-1">
|
<div className="text-center space-y-1">
|
||||||
<h4 className="text-xl font-bold text-neutral-900 dark:text-white">
|
<h4 className="text-xl font-bold text-white">
|
||||||
You're all set!
|
You're all set!
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-neutral-500 dark:text-neutral-400">
|
<p className="text-neutral-400">
|
||||||
We are successfully receiving data from your website.
|
We are successfully receiving data from your website.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,7 +189,7 @@ export default function VerificationModal({ isOpen, onClose, site, onVerified }:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 bg-neutral-50 dark:bg-neutral-800/50 rounded-xl border border-neutral-100 dark:border-neutral-800">
|
<div className="p-4 bg-neutral-50 dark:bg-neutral-800/50 rounded-xl border border-neutral-100 dark:border-neutral-800">
|
||||||
<p className="text-sm font-medium text-neutral-900 dark:text-white mb-2">
|
<p className="text-sm font-medium text-white mb-2">
|
||||||
Troubleshooting Checklist:
|
Troubleshooting Checklist:
|
||||||
</p>
|
</p>
|
||||||
<ul className="text-sm text-neutral-600 dark:text-neutral-400 space-y-1 list-disc list-inside">
|
<ul className="text-sm text-neutral-600 dark:text-neutral-400 space-y-1 list-disc list-inside">
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
|
|||||||
{/* Site Selector */}
|
{/* Site Selector */}
|
||||||
{sites.length > 0 && (
|
{sites.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1 text-neutral-900 dark:text-white">Select Site</label>
|
<label className="block text-sm font-medium mb-1 text-white">Select Site</label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedSiteId}
|
value={selectedSiteId}
|
||||||
onChange={handleSiteChange}
|
onChange={handleSiteChange}
|
||||||
@@ -138,7 +138,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1.5 text-neutral-900 dark:text-white">Website URL *</label>
|
<label className="block text-sm font-medium mb-1.5 text-white">Website URL *</label>
|
||||||
{selectedSite ? (
|
{selectedSite ? (
|
||||||
<div className="flex rounded-xl shadow-sm transition-all duration-200 focus-within:ring-4 focus-within:ring-brand-orange/10 focus-within:border-brand-orange hover:border-brand-orange/50 border border-neutral-200 dark:border-neutral-800">
|
<div className="flex rounded-xl shadow-sm transition-all duration-200 focus-within:ring-4 focus-within:ring-brand-orange/10 focus-within:border-brand-orange hover:border-brand-orange/50 border border-neutral-200 dark:border-neutral-800">
|
||||||
<span className="inline-flex items-center px-4 rounded-l-xl border-r border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-900 text-neutral-500 text-sm select-none">
|
<span className="inline-flex items-center px-4 rounded-l-xl border-r border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-900 text-neutral-500 text-sm select-none">
|
||||||
@@ -146,7 +146,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
|
|||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="flex-1 min-w-0 block w-full px-4 py-3 rounded-none rounded-r-xl bg-neutral-50/50 dark:bg-neutral-900/50 outline-none transition-all text-neutral-900 dark:text-white text-sm placeholder:text-neutral-400"
|
className="flex-1 min-w-0 block w-full px-4 py-3 rounded-none rounded-r-xl bg-neutral-50/50 dark:bg-neutral-900/50 outline-none transition-all text-white text-sm placeholder:text-neutral-400"
|
||||||
placeholder="/blog/post-1"
|
placeholder="/blog/post-1"
|
||||||
value={getCurrentPath()}
|
value={getCurrentPath()}
|
||||||
onChange={handlePathChange}
|
onChange={handlePathChange}
|
||||||
@@ -167,7 +167,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1.5 text-neutral-900 dark:text-white">Source *</label>
|
<label className="block text-sm font-medium mb-1.5 text-white">Source *</label>
|
||||||
<Input
|
<Input
|
||||||
name="source"
|
name="source"
|
||||||
placeholder="google, newsletter"
|
placeholder="google, newsletter"
|
||||||
@@ -176,7 +176,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1.5 text-neutral-900 dark:text-white">Medium *</label>
|
<label className="block text-sm font-medium mb-1.5 text-white">Medium *</label>
|
||||||
<Input
|
<Input
|
||||||
name="medium"
|
name="medium"
|
||||||
placeholder="cpc, email"
|
placeholder="cpc, email"
|
||||||
@@ -186,7 +186,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1.5 text-neutral-900 dark:text-white">Campaign Name *</label>
|
<label className="block text-sm font-medium mb-1.5 text-white">Campaign Name *</label>
|
||||||
<Input
|
<Input
|
||||||
name="campaign"
|
name="campaign"
|
||||||
placeholder="spring_sale"
|
placeholder="spring_sale"
|
||||||
|
|||||||
@@ -2310,7 +2310,7 @@ export default defineConfig({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary className="cursor-pointer text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300">
|
<summary className="cursor-pointer text-sm text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300">
|
||||||
Advanced: override domain or configure options
|
Advanced: override domain or configure options
|
||||||
</summary>
|
</summary>
|
||||||
<p className="mt-3 text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="mt-3 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
|||||||
20
lib/utils/dateRanges.ts
Normal file
20
lib/utils/dateRanges.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { getDateRange, formatDate } from '@ciphera-net/ui'
|
||||||
|
|
||||||
|
/** Monday–today range for "This week" option */
|
||||||
|
export function getThisWeekRange(): { start: string; end: string } {
|
||||||
|
const today = new Date()
|
||||||
|
const dayOfWeek = today.getDay()
|
||||||
|
const monday = new Date(today)
|
||||||
|
monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1))
|
||||||
|
return { start: formatDate(monday), end: formatDate(today) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 1st of month–today range for "This month" option */
|
||||||
|
export function getThisMonthRange(): { start: string; end: string } {
|
||||||
|
const today = new Date()
|
||||||
|
const firstOfMonth = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||||
|
return { start: formatDate(firstOfMonth), end: formatDate(today) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export for convenience
|
||||||
|
export { getDateRange, formatDate }
|
||||||
@@ -20,8 +20,6 @@
|
|||||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@simplewebauthn/browser": "^13.2.2",
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
"@stripe/react-stripe-js": "^5.6.0",
|
|
||||||
"@stripe/stripe-js": "^8.7.0",
|
|
||||||
"@tanstack/react-virtual": "^3.13.21",
|
"@tanstack/react-virtual": "^3.13.21",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@visx/curve": "^3.12.0",
|
"@visx/curve": "^3.12.0",
|
||||||
@@ -44,7 +42,6 @@
|
|||||||
"jspdf": "^4.0.0",
|
"jspdf": "^4.0.0",
|
||||||
"jspdf-autotable": "^5.0.7",
|
"jspdf-autotable": "^5.0.7",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"motion": "^12.35.2",
|
|
||||||
"next": "^16.1.1",
|
"next": "^16.1.1",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user