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:
@@ -36,7 +36,7 @@ export default function ErrorDisplay({
|
||||
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}
|
||||
</h2>
|
||||
<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">
|
||||
<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="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<div className="text-sm text-neutral-400">
|
||||
© 2024-{year} Ciphera. All rights reserved.
|
||||
</div>
|
||||
<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"
|
||||
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
|
||||
</span>
|
||||
</Link>
|
||||
@@ -125,7 +125,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
||||
|
||||
{/* * Products */}
|
||||
<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">
|
||||
{footerLinks.products.map((link) => (
|
||||
<li key={link.name}>
|
||||
@@ -153,7 +153,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
||||
|
||||
{/* * Company */}
|
||||
<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">
|
||||
{footerLinks.company.map((link) => (
|
||||
<li key={link.name}>
|
||||
@@ -181,7 +181,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
||||
|
||||
{/* * Resources */}
|
||||
<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">
|
||||
{footerLinks.resources.map((link) => (
|
||||
<li key={link.name}>
|
||||
@@ -209,7 +209,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
||||
|
||||
{/* * Legal */}
|
||||
<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">
|
||||
{footerLinks.legal.map((link) => (
|
||||
<li key={link.name}>
|
||||
@@ -232,10 +232,10 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate
|
||||
|
||||
{/* * Bottom bar */}
|
||||
<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.
|
||||
</p>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<p className="text-sm text-neutral-400">
|
||||
Where Privacy Still Exists
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -34,11 +34,11 @@ export default function FrustrationByPageTable({ pages, loading }: FrustrationBy
|
||||
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="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
|
||||
</h3>
|
||||
</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
|
||||
</p>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function FrustrationByPageTable({ pages, loading }: FrustrationBy
|
||||
style={{ width: `${barWidth}%` }}
|
||||
/>
|
||||
<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}
|
||||
>
|
||||
{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">
|
||||
{formatNumber(page.dead_clicks)}
|
||||
</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)}
|
||||
</span>
|
||||
<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="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>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
No frustration signals detected
|
||||
</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.
|
||||
</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>
|
||||
|
||||
@@ -31,7 +31,7 @@ function ChangeIndicator({ change }: { change: ReturnType<typeof pctChange> }) {
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: isDown
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-neutral-500 dark:text-neutral-400'
|
||||
: 'text-neutral-400'
|
||||
}`}
|
||||
>
|
||||
{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">
|
||||
{/* Rage Clicks */}
|
||||
<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
|
||||
</p>
|
||||
<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()}
|
||||
</span>
|
||||
<ChangeIndicator change={rageChange} />
|
||||
@@ -87,11 +87,11 @@ export default function FrustrationSummaryCards({ data, loading }: FrustrationSu
|
||||
|
||||
{/* Dead Clicks */}
|
||||
<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
|
||||
</p>
|
||||
<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()}
|
||||
</span>
|
||||
<ChangeIndicator change={deadChange} />
|
||||
@@ -103,10 +103,10 @@ export default function FrustrationSummaryCards({ data, loading }: FrustrationSu
|
||||
|
||||
{/* Total Frustration Signals */}
|
||||
<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
|
||||
</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()}
|
||||
</span>
|
||||
{topPage ? (
|
||||
|
||||
@@ -53,7 +53,7 @@ function SelectorCell({ selector }: { selector: string }) {
|
||||
className="flex items-center gap-1 min-w-0 group/copy cursor-pointer"
|
||||
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}
|
||||
</span>
|
||||
<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="flex items-center justify-between mb-1">
|
||||
<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}
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
@@ -159,7 +159,7 @@ export default function FrustrationTable({
|
||||
)}
|
||||
</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}
|
||||
</p>
|
||||
|
||||
@@ -182,15 +182,18 @@ export default function FrustrationTable({
|
||||
alt="No frustration signals"
|
||||
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
|
||||
</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:
|
||||
</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">
|
||||
{'<script defer src="https://pulse.ciphera.net/script.frustration.js"></script>'}
|
||||
</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>
|
||||
@@ -214,7 +217,7 @@ export default function FrustrationTable({
|
||||
))}
|
||||
</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
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -59,7 +59,7 @@ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<
|
||||
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: item.fill }}
|
||||
/>
|
||||
<span className="text-neutral-500 dark:text-neutral-400">
|
||||
<span className="text-neutral-400">
|
||||
{LABELS[item.type] ?? item.type}
|
||||
</span>
|
||||
<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 (
|
||||
<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">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
Frustration Trend
|
||||
</h3>
|
||||
</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
|
||||
</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="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>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
No trend data yet
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -118,11 +118,11 @@ export default function FrustrationTrend({ summary, loading }: FrustrationTrendP
|
||||
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="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
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
{hasPrevious
|
||||
? 'Rage and dead clicks split across current and previous period'
|
||||
: '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="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 && (
|
||||
<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" />}
|
||||
@@ -357,7 +357,7 @@ export default function Chart({
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between gap-3 mb-4 px-2">
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
@@ -526,7 +526,7 @@ export default function Chart({
|
||||
<span className="font-medium text-neutral-400 dark:text-neutral-500">
|
||||
{ANNOTATION_LABELS[a.category] || 'Note'} · {formatEU(a.date)}{a.time ? ` at ${a.time}` : ''}
|
||||
</span>
|
||||
<p className="text-neutral-900 dark:text-white">{a.text}</p>
|
||||
<p className="text-white">{a.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -593,16 +593,16 @@ export default function Chart({
|
||||
{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="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'}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<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
|
||||
type="button"
|
||||
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>
|
||||
<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>
|
||||
</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>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -619,7 +619,7 @@ export default function Chart({
|
||||
type="time"
|
||||
value={annotationForm.time}
|
||||
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 && (
|
||||
<button
|
||||
@@ -634,20 +634,20 @@ export default function Chart({
|
||||
</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
|
||||
type="text"
|
||||
value={annotationForm.text}
|
||||
onChange={(e) => setAnnotationForm((f) => ({ ...f, text: e.target.value.slice(0, 200) }))}
|
||||
placeholder="e.g. Launched new homepage"
|
||||
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
|
||||
/>
|
||||
<span className="text-[10px] text-neutral-400 mt-0.5 block text-right">{annotationForm.text.length}/200</span>
|
||||
</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
|
||||
value={annotationForm.category}
|
||||
onChange={(v) => setAnnotationForm((f) => ({ ...f, category: v }))}
|
||||
@@ -675,7 +675,7 @@ export default function Chart({
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -480,7 +480,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
||||
{/* Progress Bar */}
|
||||
{(isExporting || exportDone) && (
|
||||
<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 ? '100%' : `${Math.round((exportProgress.step / exportProgress.total) * 100)}%`}</span>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function FilterBar({ filters, onRemove, onClear }: FilterBarProps
|
||||
{filters.length > 1 && (
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function SiteNav({ siteId }: SiteNavProps) {
|
||||
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 ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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 gap-2">
|
||||
<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
|
||||
</h3>
|
||||
{showViewAll && (
|
||||
@@ -115,7 +115,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
||||
<div className="space-y-2 flex-1 min-h-[270px]">
|
||||
{!collectReferrers ? (
|
||||
<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>
|
||||
) : 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"
|
||||
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)}
|
||||
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>
|
||||
</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="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>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
No referrers yet
|
||||
</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.
|
||||
</p>
|
||||
<Link
|
||||
@@ -186,7 +186,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
||||
value={modalSearch}
|
||||
onChange={(e) => setModalSearch(e.target.value)}
|
||||
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 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) } }}
|
||||
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)}
|
||||
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function BreakdownDrawer({ siteId, funnelId, stepIndex, stepName,
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-800">
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
{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">
|
||||
<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)'}
|
||||
</span>
|
||||
<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
|
||||
</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'}
|
||||
</h1>
|
||||
<p className="text-neutral-600 dark:text-neutral-400">
|
||||
@@ -252,7 +252,7 @@ export default function FunnelForm({ siteId, initialData, onSubmit, submitLabel,
|
||||
{/* Steps */}
|
||||
<div className="space-y-4 mb-6">
|
||||
<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
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -166,7 +166,7 @@ function ColumnHeader({
|
||||
{column.index === 0 ? 'Entry' : `Step ${column.index}`}
|
||||
</span>
|
||||
<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
|
||||
</span>
|
||||
{column.dropOffPercent !== 0 && (
|
||||
@@ -235,10 +235,10 @@ function PageRow({
|
||||
<span
|
||||
className={`relative flex-1 truncate text-sm ${
|
||||
isSelected
|
||||
? 'text-neutral-900 dark:text-white font-medium'
|
||||
? 'text-white font-medium'
|
||||
: isOther
|
||||
? 'italic text-neutral-400 dark:text-neutral-500'
|
||||
: 'text-neutral-900 dark:text-white'
|
||||
: 'text-white'
|
||||
}`}
|
||||
>
|
||||
{isOther ? page.path : smartLabel(page.path)}
|
||||
@@ -561,12 +561,15 @@ export default function ColumnJourney({
|
||||
alt="No journey data"
|
||||
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
|
||||
</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.
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -510,14 +510,17 @@ export default function SankeyJourney({
|
||||
return (
|
||||
<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">
|
||||
<TreeStructure className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||
<TreeStructure className="w-8 h-8 text-neutral-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
No journey data yet
|
||||
</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.
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
<span className="text-neutral-700 dark:text-neutral-300">
|
||||
Showing flows through{' '}
|
||||
<span className="font-medium text-neutral-900 dark:text-white">
|
||||
<span className="font-medium text-white">
|
||||
{filterPath}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -38,11 +38,11 @@ export default function TopPathsTable({ paths, loading }: TopPathsTableProps) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6">
|
||||
<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
|
||||
</h3>
|
||||
</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
|
||||
</p>
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function TopPathsTable({ paths, loading }: TopPathsTableProps) {
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="text-sm text-neutral-900 dark:text-white truncate"
|
||||
className="text-sm text-white truncate"
|
||||
title={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="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>
|
||||
<h4 className="font-semibold text-neutral-900 dark:text-white">
|
||||
<h4 className="font-semibold text-white">
|
||||
No path data yet
|
||||
</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.
|
||||
</p>
|
||||
</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}
|
||||
>
|
||||
<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 && (
|
||||
<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>
|
||||
)}
|
||||
{!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
|
||||
</div>
|
||||
)}
|
||||
@@ -260,11 +260,11 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
|
||||
<div className="flex gap-3">
|
||||
{getTypeIcon(n.type)}
|
||||
<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}
|
||||
</p>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
@@ -283,11 +283,11 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
|
||||
<div className="flex gap-3">
|
||||
{getTypeIcon(n.type)}
|
||||
<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}
|
||||
</p>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
@@ -315,7 +315,7 @@ export default function NotificationCenter({ anchor = 'bottom', variant = 'defau
|
||||
<Link
|
||||
href="/org-settings?tab=notifications"
|
||||
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" />
|
||||
Manage settings
|
||||
|
||||
@@ -31,19 +31,19 @@ function CustomTooltip({ active, payload, label }: TooltipProps) {
|
||||
|
||||
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="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 && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="size-1.5 rounded-full" style={{ backgroundColor: '#FD5E0F' }} />
|
||||
<span className="text-neutral-500 dark:text-neutral-400">Clicks:</span>
|
||||
<span className="font-semibold text-neutral-900 dark:text-white">{clicks.value.toLocaleString()}</span>
|
||||
<span className="text-neutral-400">Clicks:</span>
|
||||
<span className="font-semibold text-white">{clicks.value.toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
{impressions && (
|
||||
<div className="flex items-center gap-2 text-sm mt-1">
|
||||
<div className="size-1.5 rounded-full" style={{ backgroundColor: '#9CA3AF' }} />
|
||||
<span className="text-neutral-500 dark:text-neutral-400">Impressions:</span>
|
||||
<span className="font-semibold text-neutral-900 dark:text-white">{impressions.value.toLocaleString()}</span>
|
||||
<span className="text-neutral-400">Impressions:</span>
|
||||
<span className="font-semibold text-white">{impressions.value.toLocaleString()}</span>
|
||||
</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 (!currentOrgId) {
|
||||
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>
|
||||
</div>
|
||||
)
|
||||
@@ -490,7 +490,7 @@ export default function OrganizationSettings() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<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">
|
||||
Manage your organization workspace and members.
|
||||
</p>
|
||||
@@ -580,8 +580,8 @@ export default function OrganizationSettings() {
|
||||
{activeTab === 'general' && (
|
||||
<div className="space-y-12">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">General Information</h2>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">Basic details about your organization.</p>
|
||||
<h2 className="text-2xl font-bold text-white mb-1">General Information</h2>
|
||||
<p className="text-sm text-neutral-400">Basic details about your organization.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleUpdateOrg} className="space-y-4">
|
||||
@@ -597,7 +597,7 @@ export default function OrganizationSettings() {
|
||||
minLength={2}
|
||||
maxLength={50}
|
||||
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>
|
||||
|
||||
@@ -606,7 +606,7 @@ export default function OrganizationSettings() {
|
||||
Organization Slug
|
||||
</label>
|
||||
<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/
|
||||
</span>
|
||||
<Input
|
||||
@@ -617,10 +617,10 @@ export default function OrganizationSettings() {
|
||||
minLength={3}
|
||||
maxLength={30}
|
||||
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>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -658,7 +658,7 @@ export default function OrganizationSettings() {
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<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 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">
|
||||
{/* Invite Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark: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>
|
||||
<h2 className="text-2xl font-bold text-white mb-1">Organization Members</h2>
|
||||
<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">
|
||||
<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">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
@@ -744,12 +744,12 @@ export default function OrganizationSettings() {
|
||||
|
||||
{/* Members List */}
|
||||
<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">
|
||||
{isLoadingMembers ? (
|
||||
<MembersListSkeleton />
|
||||
) : 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) => (
|
||||
<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() || '?'}
|
||||
</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'}
|
||||
</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))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -786,7 +786,7 @@ export default function OrganizationSettings() {
|
||||
{/* Pending Invitations */}
|
||||
{invitations.length > 0 && (
|
||||
<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">
|
||||
{invitations.map((invite) => (
|
||||
<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>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-neutral-900 dark:text-white">
|
||||
<div className="text-sm font-medium text-white">
|
||||
{invite.email}
|
||||
</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))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -821,8 +821,8 @@ export default function OrganizationSettings() {
|
||||
{activeTab === 'billing' && (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark: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>
|
||||
<h2 className="text-2xl font-bold text-white mb-1">Billing & Subscription</h2>
|
||||
<p className="text-sm text-neutral-400">Manage your plan, usage, and payment details.</p>
|
||||
</div>
|
||||
|
||||
{isLoadingSubscription ? (
|
||||
@@ -832,7 +832,7 @@ export default function OrganizationSettings() {
|
||||
</div>
|
||||
) : !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">
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
@@ -915,7 +915,7 @@ export default function OrganizationSettings() {
|
||||
{/* Plan header */}
|
||||
<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">
|
||||
<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
|
||||
</span>
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${
|
||||
@@ -940,7 +940,7 @@ export default function OrganizationSettings() {
|
||||
</Button>
|
||||
</div>
|
||||
{(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 && (
|
||||
<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>
|
||||
<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'
|
||||
? (() => {
|
||||
const limit = getSitesLimitForPlan(subscription.plan_id)
|
||||
@@ -967,7 +967,7 @@ export default function OrganizationSettings() {
|
||||
</div>
|
||||
<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_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">
|
||||
{subscription.subscription_status === 'trialing' ? 'Trial ends' : (subscription.cancel_at_period_end ? 'Access until' : 'Renews')}
|
||||
</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 d = ts ? new Date(ts) : null
|
||||
@@ -1005,7 +1005,7 @@ export default function OrganizationSettings() {
|
||||
</div>
|
||||
<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'}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1038,19 +1038,19 @@ export default function OrganizationSettings() {
|
||||
|
||||
{/* Order History */}
|
||||
<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">
|
||||
{isLoadingInvoices ? (
|
||||
<InvoicesListSkeleton />
|
||||
) : 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) => (
|
||||
<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>
|
||||
<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() })}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-500 ml-2">
|
||||
@@ -1084,8 +1084,8 @@ export default function OrganizationSettings() {
|
||||
{activeTab === 'notifications' && (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Notification Settings</h2>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-6">
|
||||
<h2 className="text-2xl font-bold text-white mb-1">Notification Settings</h2>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1094,7 +1094,7 @@ export default function OrganizationSettings() {
|
||||
<SettingsFormSkeleton />
|
||||
) : (
|
||||
<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">
|
||||
{notificationCategories.map((cat) => (
|
||||
<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"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-neutral-900 dark: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 font-medium text-white">{cat.label}</p>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">{cat.description}</p>
|
||||
</div>
|
||||
<div className="flex items-center shrink-0">
|
||||
<button
|
||||
@@ -1149,8 +1149,8 @@ export default function OrganizationSettings() {
|
||||
{activeTab === 'audit' && (
|
||||
<div className="space-y-12">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark: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>
|
||||
<h2 className="text-2xl font-bold text-white mb-1">Audit log</h2>
|
||||
<p className="text-sm text-neutral-400">Who did what and when for this organization.</p>
|
||||
</div>
|
||||
|
||||
{/* Advanced Filters */}
|
||||
@@ -1163,7 +1163,7 @@ export default function OrganizationSettings() {
|
||||
placeholder="e.g. 8a2b3c"
|
||||
value={auditLogIdFilter}
|
||||
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 className="space-y-1">
|
||||
@@ -1173,7 +1173,7 @@ export default function OrganizationSettings() {
|
||||
placeholder="e.g. site_created"
|
||||
value={auditActionFilter}
|
||||
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 className="space-y-1">
|
||||
@@ -1182,7 +1182,7 @@ export default function OrganizationSettings() {
|
||||
type="date"
|
||||
value={auditStartDate}
|
||||
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 className="space-y-1">
|
||||
@@ -1191,7 +1191,7 @@ export default function OrganizationSettings() {
|
||||
type="date"
|
||||
value={auditEndDate}
|
||||
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>
|
||||
@@ -1240,10 +1240,10 @@ export default function OrganizationSettings() {
|
||||
<td className="px-4 py-3 text-neutral-600 dark:text-neutral-400 whitespace-nowrap">
|
||||
{formatDateTime(new Date(entry.occurred_at))}
|
||||
</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'}
|
||||
</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>
|
||||
</tr>
|
||||
))}
|
||||
@@ -1255,7 +1255,7 @@ export default function OrganizationSettings() {
|
||||
{/* Pagination */}
|
||||
{auditTotal > auditPageSize && (
|
||||
<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}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
@@ -1361,7 +1361,7 @@ export default function OrganizationSettings() {
|
||||
value={deleteConfirm}
|
||||
onChange={(e) => setDeleteConfirm(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</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"
|
||||
>
|
||||
<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
|
||||
onClick={() => setShowCancelPrompt(false)}
|
||||
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"
|
||||
>
|
||||
<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
|
||||
type="button"
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<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}
|
||||
</span>
|
||||
<span className="block text-xs text-neutral-500 mt-0.5">{plan.sites}</span>
|
||||
@@ -1519,7 +1519,7 @@ export default function OrganizationSettings() {
|
||||
<select
|
||||
value={changePlanTierIndex}
|
||||
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) => (
|
||||
<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 === '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'
|
||||
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 {
|
||||
@@ -120,8 +120,8 @@ export default function SecurityActivityCard() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-1">Security Activity</h2>
|
||||
<p className="text-neutral-500 dark:text-neutral-400 text-sm mb-6">
|
||||
<h2 className="text-xl font-semibold text-white mb-1">Security Activity</h2>
|
||||
<p className="text-neutral-400 text-sm mb-6">
|
||||
Recent security events on your account{totalCount > 0 ? ` (${totalCount})` : ''}
|
||||
</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">
|
||||
<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>
|
||||
<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 className="space-y-2">
|
||||
@@ -165,11 +165,11 @@ export default function SecurityActivityCard() {
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<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}
|
||||
</span>
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -179,7 +179,7 @@ export default function SecurityActivityCard() {
|
||||
</span>
|
||||
)}
|
||||
</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 && (deviceStr || entry.ip_address) && <span>·</span>}
|
||||
{deviceStr && <span>{deviceStr}</span>}
|
||||
@@ -189,7 +189,7 @@ export default function SecurityActivityCard() {
|
||||
</div>
|
||||
|
||||
<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)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@ function NotificationCenterPlaceholder() {
|
||||
return (
|
||||
<div className="text-center max-w-md mx-auto py-8">
|
||||
<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>
|
||||
<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
|
||||
|
||||
@@ -56,8 +56,8 @@ export default function TrustedDevicesCard() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white mb-1">Trusted Devices</h2>
|
||||
<p className="text-neutral-500 dark:text-neutral-400 text-sm mb-6">
|
||||
<h2 className="text-xl font-semibold text-white mb-1">Trusted Devices</h2>
|
||||
<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.
|
||||
</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">
|
||||
<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>
|
||||
<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 className="space-y-2">
|
||||
@@ -83,7 +83,7 @@ export default function TrustedDevicesCard() {
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d={getDeviceIcon(device.display_hint)} />
|
||||
</svg>
|
||||
@@ -91,7 +91,7 @@ export default function TrustedDevicesCard() {
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<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'}
|
||||
</span>
|
||||
{device.is_current && (
|
||||
@@ -100,7 +100,7 @@ export default function TrustedDevicesCard() {
|
||||
</span>
|
||||
)}
|
||||
</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))}>
|
||||
First seen {formatRelativeTime(device.first_seen_at)}
|
||||
</span>
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function DeleteSiteModal({ open, onClose, onDeleted, siteName, si
|
||||
value={deleteConfirm}
|
||||
onChange={(e) => setDeleteConfirm(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -187,7 +187,7 @@ export default function DeleteSiteModal({ open, onClose, onDeleted, siteName, si
|
||||
value={permanentConfirm}
|
||||
onChange={(e) => setPermanentConfirm(e.target.value)}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +167,7 @@ export default function ScriptSetupBlock({
|
||||
|
||||
{/* ── Feature toggles ─────────────────────────────────────────────── */}
|
||||
<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
|
||||
</h4>
|
||||
<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"
|
||||
>
|
||||
<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}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<span className="text-xs text-neutral-400">
|
||||
{f.description}
|
||||
</span>
|
||||
</div>
|
||||
@@ -191,10 +191,10 @@ export default function ScriptSetupBlock({
|
||||
{/* * 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="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
|
||||
</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
|
||||
</span>
|
||||
</div>
|
||||
@@ -204,15 +204,15 @@ export default function ScriptSetupBlock({
|
||||
|
||||
{/* ── Storage + TTL ───────────────────────────────────────────────── */}
|
||||
<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
|
||||
</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.
|
||||
</p>
|
||||
<div className="flex items-end gap-3">
|
||||
<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
|
||||
</label>
|
||||
<Select
|
||||
@@ -224,7 +224,7 @@ export default function ScriptSetupBlock({
|
||||
</div>
|
||||
{storage === 'local' && (
|
||||
<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
|
||||
</label>
|
||||
<Select
|
||||
@@ -242,14 +242,14 @@ export default function ScriptSetupBlock({
|
||||
{showFrameworkPicker && (
|
||||
<div className="mt-6">
|
||||
<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
|
||||
</h4>
|
||||
<Link
|
||||
href="/integrations"
|
||||
target="_blank"
|
||||
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 →
|
||||
</Link>
|
||||
|
||||
@@ -46,8 +46,8 @@ function SiteCard({ site, stats, statsLoading, onDelete, canDelete }: SiteCardPr
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-neutral-900 dark:text-white">{site.name}</h3>
|
||||
<div className="flex items-center gap-1 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<h3 className="font-semibold text-white">{site.name}</h3>
|
||||
<div className="flex items-center gap-1 text-sm text-neutral-400">
|
||||
{site.domain}
|
||||
<a
|
||||
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>
|
||||
<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)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<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)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -144,8 +144,8 @@ export default function SiteList({ sites, siteStats, loading, onDelete }: SiteLi
|
||||
className="mb-6"
|
||||
unoptimized
|
||||
/>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark: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>
|
||||
<h3 className="text-lg font-semibold text-white">No sites yet</h3>
|
||||
<p className="mt-2 text-sm text-neutral-400 mb-4">Create your first site to get started.</p>
|
||||
<Link href="/sites/new">
|
||||
<Button variant="primary" className="text-sm">
|
||||
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">
|
||||
<BookOpenIcon className="h-6 w-6 text-neutral-500" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-neutral-900 dark: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>
|
||||
<h3 className="font-semibold text-white">Need help setup?</h3>
|
||||
<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">
|
||||
Read Documentation →
|
||||
</Link>
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function VerificationModal({ isOpen, onClose, site, onVerified }:
|
||||
>
|
||||
{/* Header */}
|
||||
<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
|
||||
</h3>
|
||||
<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>
|
||||
<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...
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -164,10 +164,10 @@ export default function VerificationModal({ isOpen, onClose, site, onVerified }:
|
||||
<CheckCircleIcon className="w-8 h-8" />
|
||||
</div>
|
||||
<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!
|
||||
</h4>
|
||||
<p className="text-neutral-500 dark:text-neutral-400">
|
||||
<p className="text-neutral-400">
|
||||
We are successfully receiving data from your website.
|
||||
</p>
|
||||
</div>
|
||||
@@ -189,7 +189,7 @@ export default function VerificationModal({ isOpen, onClose, site, onVerified }:
|
||||
</div>
|
||||
|
||||
<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:
|
||||
</p>
|
||||
<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 */}
|
||||
{sites.length > 0 && (
|
||||
<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
|
||||
value={selectedSiteId}
|
||||
onChange={handleSiteChange}
|
||||
@@ -138,7 +138,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
|
||||
)}
|
||||
|
||||
<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 ? (
|
||||
<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">
|
||||
@@ -146,7 +146,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
|
||||
</span>
|
||||
<input
|
||||
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"
|
||||
value={getCurrentPath()}
|
||||
onChange={handlePathChange}
|
||||
@@ -167,7 +167,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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
|
||||
name="source"
|
||||
placeholder="google, newsletter"
|
||||
@@ -176,7 +176,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">Medium *</label>
|
||||
<label className="block text-sm font-medium mb-1.5 text-white">Medium *</label>
|
||||
<Input
|
||||
name="medium"
|
||||
placeholder="cpc, email"
|
||||
@@ -186,7 +186,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
|
||||
</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
|
||||
name="campaign"
|
||||
placeholder="spring_sale"
|
||||
|
||||
Reference in New Issue
Block a user