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:
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 { 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 dynamic from 'next/dynamic'
|
||||
import { getRageClicks, getDeadClicks } from '@/lib/api/stats'
|
||||
@@ -15,20 +15,6 @@ import { BehaviorSkeleton, useMinimumLoading, useSkeletonFade } from '@/componen
|
||||
|
||||
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() {
|
||||
const params = useParams()
|
||||
const siteId = params.id as string
|
||||
@@ -74,10 +60,10 @@ export default function BehaviorPage() {
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<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
|
||||
</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
|
||||
</p>
|
||||
</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">
|
||||
<CloudArrowUp size={40} className="text-neutral-400 dark:text-neutral-500" />
|
||||
</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
|
||||
</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.
|
||||
</p>
|
||||
<Link
|
||||
@@ -212,10 +212,10 @@ export default function CDNPage() {
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<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
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -281,7 +281,7 @@ export default function CDNPage() {
|
||||
|
||||
{/* Bandwidth chart */}
|
||||
<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 ? (
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<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
|
||||
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">
|
||||
<p className="text-neutral-500 dark:text-neutral-400 mb-1">{formatDateShort(label)}</p>
|
||||
<p className="text-neutral-900 dark:text-white font-medium">
|
||||
<p className="text-neutral-400 mb-1">{formatDateShort(label)}</p>
|
||||
<p className="text-white font-medium">
|
||||
Total: {formatBytes(payload[0]?.value as number)}
|
||||
</p>
|
||||
{payload[1] && (
|
||||
@@ -359,7 +359,7 @@ export default function CDNPage() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{/* Requests chart */}
|
||||
<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 ? (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<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
|
||||
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">
|
||||
<p className="text-neutral-500 dark:text-neutral-400 mb-1">{formatDateShort(label)}</p>
|
||||
<p className="text-neutral-900 dark:text-white font-medium">
|
||||
<p className="text-neutral-400 mb-1">{formatDateShort(label)}</p>
|
||||
<p className="text-white font-medium">
|
||||
{formatNumber(payload[0]?.value as number)} requests
|
||||
</p>
|
||||
</div>
|
||||
@@ -405,7 +405,7 @@ export default function CDNPage() {
|
||||
|
||||
{/* Errors chart */}
|
||||
<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 ? (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<BarChart
|
||||
@@ -439,7 +439,7 @@ export default function CDNPage() {
|
||||
if (!active || !payload?.length) return null
|
||||
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">
|
||||
<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) => (
|
||||
<p key={entry.name} style={{ color: entry.color }} className="font-medium">
|
||||
{entry.name}: {formatNumber(entry.value as number)}
|
||||
@@ -464,7 +464,7 @@ export default function CDNPage() {
|
||||
|
||||
{/* Traffic Distribution */}
|
||||
<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 ? (
|
||||
<>
|
||||
<div className="h-[360px] mb-8">
|
||||
@@ -480,9 +480,9 @@ export default function CDNPage() {
|
||||
<div className="flex items-center gap-2.5 mb-2">
|
||||
{cc && getFlagIcon(cc)}
|
||||
<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>
|
||||
<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)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -530,13 +530,13 @@ function OverviewCard({
|
||||
|
||||
return (
|
||||
<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-2xl font-bold text-neutral-900 dark:text-white">{value}</p>
|
||||
<p className="text-xs font-medium text-neutral-400 mb-1">{label}</p>
|
||||
<p className="text-2xl font-bold text-white">{value}</p>
|
||||
{changeLabel && (
|
||||
<p className={`text-xs mt-1 font-medium ${
|
||||
isGood ? 'text-green-600 dark:text-green-400' :
|
||||
isBad ? 'text-red-600 dark:text-red-400' :
|
||||
'text-neutral-500 dark:text-neutral-400'
|
||||
'text-neutral-400'
|
||||
}`}>
|
||||
{changeLabel} vs previous period
|
||||
</p>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function FunnelReportPage() {
|
||||
const [funnel, setFunnel] = useState<Funnel | null>(null)
|
||||
const [stats, setStats] = useState<FunnelStats | null>(null)
|
||||
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 [isDatePickerOpen, setIsDatePickerOpen] = useState(false)
|
||||
const [loadError, setLoadError] = useState<'not_found' | 'forbidden' | 'error' | null>(null)
|
||||
@@ -154,7 +154,7 @@ export default function FunnelReportPage() {
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
||||
<h1 className="text-2xl font-bold text-white">
|
||||
{funnel.name}
|
||||
</h1>
|
||||
{funnel.description && (
|
||||
@@ -236,7 +236,7 @@ export default function FunnelReportPage() {
|
||||
{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="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
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -322,10 +322,10 @@ export default function FunnelReportPage() {
|
||||
<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">
|
||||
<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-500 dark: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-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">Step</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-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">Conversion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-800">
|
||||
@@ -338,13 +338,13 @@ export default function FunnelReportPage() {
|
||||
{i + 1}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium text-neutral-900 dark: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="font-medium text-white">{step.step.name}</p>
|
||||
<p className="text-neutral-400 text-xs font-mono mt-0.5">{step.step.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<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()}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function FunnelsPage() {
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
||||
<h1 className="text-2xl font-bold text-white">
|
||||
Funnels
|
||||
</h1>
|
||||
<p className="text-neutral-600 dark:text-neutral-400">
|
||||
@@ -65,7 +65,7 @@ export default function FunnelsPage() {
|
||||
className="mb-6"
|
||||
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
|
||||
</h3>
|
||||
<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="flex items-center justify-between">
|
||||
<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}
|
||||
</h3>
|
||||
{funnel.description && (
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
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 ColumnJourney from '@/components/journeys/ColumnJourney'
|
||||
import SankeyJourney from '@/components/journeys/SankeyJourney'
|
||||
@@ -18,20 +18,6 @@ import {
|
||||
|
||||
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() {
|
||||
const params = useParams()
|
||||
const siteId = params.id as string
|
||||
@@ -91,10 +77,10 @@ export default function JourneysPage() {
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<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
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -143,7 +129,7 @@ export default function JourneysPage() {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-6">
|
||||
{/* Depth slider */}
|
||||
<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 className="text-brand-orange font-bold">
|
||||
{depth} steps deep
|
||||
@@ -196,7 +182,7 @@ export default function JourneysPage() {
|
||||
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 ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
@@ -232,7 +218,7 @@ export default function JourneysPage() {
|
||||
|
||||
{/* Footer */}
|
||||
{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
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
type Stats,
|
||||
type DailyStat,
|
||||
} 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 { Button } 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 } {
|
||||
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 gap-4">
|
||||
<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}
|
||||
</h1>
|
||||
<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 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 { useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
|
||||
|
||||
// * Metric status thresholds (Google's Core Web Vitals thresholds)
|
||||
function getMetricStatus(metric: string, value: number | null): { label: string; color: string } {
|
||||
@@ -223,8 +224,10 @@ export default function PageSpeedPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// * Loading state
|
||||
if (isLoading && !latestChecks) return <PageSpeedSkeleton />
|
||||
// * Loading state with minimum display time (consistent with other pages)
|
||||
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>
|
||||
|
||||
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">
|
||||
{/* Header */}
|
||||
<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
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -246,14 +249,14 @@ export default function PageSpeedPage() {
|
||||
{/* 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="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" />
|
||||
</svg>
|
||||
</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
|
||||
</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.
|
||||
</p>
|
||||
|
||||
@@ -263,7 +266,7 @@ export default function PageSpeedPage() {
|
||||
<select
|
||||
value={frequency}
|
||||
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="weekly">Weekly</option>
|
||||
@@ -358,10 +361,10 @@ export default function PageSpeedPage() {
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<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
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -443,7 +446,7 @@ export default function PageSpeedPage() {
|
||||
|
||||
{/* 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 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 */}
|
||||
{checkTimestamps.length > 1 && (
|
||||
<button
|
||||
@@ -497,7 +500,7 @@ export default function PageSpeedPage() {
|
||||
{/* Filmstrip — page load progression */}
|
||||
{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">
|
||||
<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
|
||||
</h3>
|
||||
<div className="flex items-center overflow-x-auto gap-1 scrollbar-none">
|
||||
@@ -521,7 +524,7 @@ export default function PageSpeedPage() {
|
||||
|
||||
{/* 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">
|
||||
<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
|
||||
</h3>
|
||||
<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">
|
||||
<span className={`mt-1.5 inline-block w-2.5 h-2.5 rounded-full flex-shrink-0 ${getMetricDotColor(key, value)}`} />
|
||||
<div>
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<div className="text-sm text-neutral-400">
|
||||
{label}
|
||||
</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)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -544,7 +547,7 @@ export default function PageSpeedPage() {
|
||||
{/* Section 3 — Score Trend Chart (visx) */}
|
||||
{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">
|
||||
<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
|
||||
</h3>
|
||||
<div>
|
||||
@@ -597,10 +600,10 @@ export default function PageSpeedPage() {
|
||||
<div className="flex items-center gap-5 mb-6">
|
||||
<ScoreGauge score={scoreByGroup[group.key]} label="" size={56} />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
{group.label}
|
||||
</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
|
||||
return realIssues === 0 ? 'No issues found' : `${realIssues} issue${realIssues !== 1 ? 's' : ''} found`
|
||||
@@ -615,7 +618,7 @@ export default function PageSpeedPage() {
|
||||
|
||||
{groupManual.length > 0 && (
|
||||
<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>
|
||||
</summary>
|
||||
<div className="mt-2 divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||
@@ -626,7 +629,7 @@ export default function PageSpeedPage() {
|
||||
|
||||
{groupPassed.length > 0 && (
|
||||
<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>
|
||||
</summary>
|
||||
<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">
|
||||
<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} />
|
||||
<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 && (
|
||||
<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">
|
||||
{/* Description with parsed markdown links */}
|
||||
{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} />
|
||||
</p>
|
||||
)}
|
||||
@@ -822,15 +825,15 @@ function AuditItem({ item }: { item: Record<string, any> }) {
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
{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 && (
|
||||
<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 && (
|
||||
<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 { useParams } from 'next/navigation'
|
||||
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 { useDashboard, useGSCStatus, useGSCOverview, useGSCTopQueries, useGSCTopPages, useGSCNewQueries } from '@/lib/swr/dashboard'
|
||||
import { getGSCQueryPages, getGSCPageQueries } from '@/lib/api/gsc'
|
||||
@@ -13,20 +14,6 @@ import ClicksImpressionsChart from '@/components/search/ClicksImpressionsChart'
|
||||
|
||||
// ─── 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 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">
|
||||
<MagnifyingGlass size={40} className="text-neutral-400 dark:text-neutral-500" />
|
||||
</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
|
||||
</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.
|
||||
</p>
|
||||
<Link
|
||||
@@ -215,10 +202,10 @@ export default function SearchConsolePage() {
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<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
|
||||
</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
|
||||
</p>
|
||||
</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">
|
||||
{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">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<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([]) }}
|
||||
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-all cursor-pointer ${
|
||||
activeView === 'queries'
|
||||
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
|
||||
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||
? 'bg-white dark:bg-neutral-700 text-white shadow-sm'
|
||||
: 'text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
Top Queries
|
||||
@@ -332,8 +319,8 @@ export default function SearchConsolePage() {
|
||||
onClick={() => { setActiveView('pages'); setExpandedPage(null); setExpandedData([]) }}
|
||||
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-all cursor-pointer ${
|
||||
activeView === 'pages'
|
||||
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
|
||||
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||
? 'bg-white dark:bg-neutral-700 text-white shadow-sm'
|
||||
: 'text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
|
||||
}`}
|
||||
>
|
||||
Top Pages
|
||||
@@ -347,12 +334,12 @@ export default function SearchConsolePage() {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<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-500 dark: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-500 dark: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-500 dark:text-neutral-400">Position</th>
|
||||
<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-400">Query</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-400">Impressions</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-400">Position</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -369,7 +356,7 @@ export default function SearchConsolePage() {
|
||||
))
|
||||
) : queries.length === 0 ? (
|
||||
<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.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -391,7 +378,7 @@ export default function SearchConsolePage() {
|
||||
{/* Pagination */}
|
||||
{queriesTotal > PAGE_SIZE && (
|
||||
<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()}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
@@ -421,12 +408,12 @@ export default function SearchConsolePage() {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<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-500 dark: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-500 dark: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-500 dark:text-neutral-400">Position</th>
|
||||
<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-400">Page</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-400">Impressions</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-400">Position</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -443,7 +430,7 @@ export default function SearchConsolePage() {
|
||||
))
|
||||
) : pages.length === 0 ? (
|
||||
<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.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -465,7 +452,7 @@ export default function SearchConsolePage() {
|
||||
{/* Pagination */}
|
||||
{pagesTotal > PAGE_SIZE && (
|
||||
<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()}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
@@ -522,13 +509,13 @@ function OverviewCard({
|
||||
|
||||
return (
|
||||
<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-2xl font-bold text-neutral-900 dark:text-white">{value}</p>
|
||||
<p className="text-xs font-medium text-neutral-400 mb-1">{label}</p>
|
||||
<p className="text-2xl font-bold text-white">{value}</p>
|
||||
{change && (
|
||||
<p className={`text-xs mt-1 font-medium ${
|
||||
isPositive ? 'text-green-600 dark:text-green-400' :
|
||||
isNegative ? 'text-red-600 dark:text-red-400' :
|
||||
'text-neutral-500 dark:text-neutral-400'
|
||||
'text-neutral-400'
|
||||
}`}>
|
||||
{change.label} vs previous period
|
||||
</p>
|
||||
@@ -560,7 +547,7 @@ function QueryRow({
|
||||
<td className="px-4 py-3 text-neutral-400 dark:text-neutral-500">
|
||||
<Caret size={14} />
|
||||
</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.impressions.toLocaleString()}</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>
|
||||
) : 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">
|
||||
<thead>
|
||||
@@ -631,7 +618,7 @@ function PageRow({
|
||||
<td className="px-4 py-3 text-neutral-400 dark:text-neutral-500">
|
||||
<Caret size={14} />
|
||||
</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.impressions.toLocaleString()}</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>
|
||||
) : 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">
|
||||
<thead>
|
||||
|
||||
@@ -738,9 +738,9 @@ export default function SiteSettingsPage() {
|
||||
|
||||
<div className="space-y-8">
|
||||
<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">
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -860,8 +860,8 @@ export default function SiteSettingsPage() {
|
||||
<div className="space-y-12">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark: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>
|
||||
<h2 className="text-2xl font-bold text-white mb-1">General Configuration</h2>
|
||||
<p className="text-sm text-neutral-400">Update your site details and tracking script.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
@@ -907,17 +907,17 @@ export default function SiteSettingsPage() {
|
||||
type="text"
|
||||
value={site.domain}
|
||||
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
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-4">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Tracking Script</h3>
|
||||
<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.
|
||||
</p>
|
||||
<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 ? 'Verified' : 'Verify Installation'}
|
||||
</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.'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -964,7 +964,7 @@ export default function SiteSettingsPage() {
|
||||
<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 your site.</p>
|
||||
<p className="text-sm text-neutral-400">Irreversible actions for your site.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
@@ -1003,8 +1003,8 @@ export default function SiteSettingsPage() {
|
||||
<div className="space-y-12">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark: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>
|
||||
<h2 className="text-2xl font-bold text-white mb-1">Visibility Settings</h2>
|
||||
<p className="text-sm text-neutral-400">Manage who can view your dashboard.</p>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-neutral-900 dark:text-white">Public Dashboard</h3>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<h3 className="font-medium text-white">Public Dashboard</h3>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Allow anyone with the link to view this dashboard
|
||||
</p>
|
||||
</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"
|
||||
>
|
||||
<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
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
@@ -1054,12 +1054,12 @@ export default function SiteSettingsPage() {
|
||||
<button
|
||||
type="button"
|
||||
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'}
|
||||
</button>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1154,8 +1154,8 @@ export default function SiteSettingsPage() {
|
||||
<div className="space-y-12">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark: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>
|
||||
<h2 className="text-2xl font-bold text-white mb-1">Data & Privacy</h2>
|
||||
<p className="text-sm text-neutral-400">Control what visitor data is collected. Less data = more privacy.</p>
|
||||
</div>
|
||||
|
||||
{/* 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="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-neutral-900 dark:text-white">Page Paths</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||
<h4 className="font-medium text-white">Page Paths</h4>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">
|
||||
Track which pages visitors view
|
||||
</p>
|
||||
</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="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-neutral-900 dark:text-white">Referrers</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||
<h4 className="font-medium text-white">Referrers</h4>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">
|
||||
Track where visitors come from
|
||||
</p>
|
||||
</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="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-neutral-900 dark:text-white">Device Info</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||
<h4 className="font-medium text-white">Device Info</h4>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">
|
||||
Track browser, OS, and device type
|
||||
</p>
|
||||
</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="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-neutral-900 dark:text-white">Geographic Data</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||
<h4 className="font-medium text-white">Geographic Data</h4>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">
|
||||
Control location tracking granularity
|
||||
</p>
|
||||
</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="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-neutral-900 dark:text-white">Screen Resolution</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||
<h4 className="font-medium text-white">Screen Resolution</h4>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">
|
||||
Track visitor screen sizes
|
||||
</p>
|
||||
</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="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-neutral-900 dark:text-white">Hide unknown locations</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||
<h4 className="font-medium text-white">Hide unknown locations</h4>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">
|
||||
Exclude entries where geographic data could not be resolved from location stats
|
||||
</p>
|
||||
</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="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-neutral-900 dark:text-white">Keep raw event data for</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||
<h4 className="font-medium text-white">Keep raw event data for</h4>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">
|
||||
Events older than this are automatically deleted. Aggregated daily stats are kept permanently.
|
||||
</p>
|
||||
</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="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium text-neutral-900 dark:text-white">Check frequency</h4>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
|
||||
<h4 className="font-medium text-white">Check frequency</h4>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">
|
||||
How often PageSpeed Insights runs automated checks on your site.
|
||||
</p>
|
||||
</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"
|
||||
/>
|
||||
</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/*).
|
||||
</p>
|
||||
</div>
|
||||
@@ -1417,7 +1417,7 @@ export default function SiteSettingsPage() {
|
||||
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
For your privacy policy
|
||||
</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.
|
||||
It updates automatically based on your saved settings above.
|
||||
</p>
|
||||
@@ -1445,7 +1445,7 @@ export default function SiteSettingsPage() {
|
||||
{snippetCopied ? (
|
||||
<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" />
|
||||
<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>
|
||||
@@ -1468,7 +1468,7 @@ export default function SiteSettingsPage() {
|
||||
{activeTab === 'bot' && (
|
||||
<div className="flex-1 space-y-6">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -1627,8 +1627,8 @@ export default function SiteSettingsPage() {
|
||||
{activeTab === 'goals' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white mb-1">Goals & Events</h2>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<h2 className="text-2xl font-bold text-white mb-1">Goals & Events</h2>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1643,7 +1643,7 @@ export default function SiteSettingsPage() {
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{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.
|
||||
</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"
|
||||
>
|
||||
<div>
|
||||
<span className="font-medium text-neutral-900 dark:text-white">{goal.name}</span>
|
||||
<span className="text-neutral-500 dark:text-neutral-400 text-sm ml-2">({goal.event_name})</span>
|
||||
<span className="font-medium text-white">{goal.name}</span>
|
||||
<span className="text-neutral-400 text-sm ml-2">({goal.event_name})</span>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -1686,16 +1686,16 @@ export default function SiteSettingsPage() {
|
||||
{activeTab === 'notifications' && (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark: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>
|
||||
<h2 className="text-2xl font-bold text-white mb-1">Notifications</h2>
|
||||
<p className="text-sm text-neutral-400">Configure how you receive reports and alerts.</p>
|
||||
</div>
|
||||
|
||||
{/* Reports subsection */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-base font-medium text-neutral-900 dark: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>
|
||||
<h3 className="text-base font-medium text-white">Reports</h3>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">Automatically deliver analytics reports via email or webhooks.</p>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<Button onClick={() => { setEditingSchedule(null); resetReportForm(); setReportModalOpen(true) }}>
|
||||
@@ -1711,7 +1711,7 @@ export default function SiteSettingsPage() {
|
||||
))}
|
||||
</div>
|
||||
) : 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.
|
||||
</div>
|
||||
) : (
|
||||
@@ -1732,7 +1732,7 @@ export default function SiteSettingsPage() {
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<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)}
|
||||
</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">
|
||||
@@ -1742,7 +1742,7 @@ export default function SiteSettingsPage() {
|
||||
{getReportTypeLabel(schedule.report_type)}
|
||||
</span>
|
||||
</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_config as EmailConfig).recipients.join(', ')
|
||||
: (schedule.channel_config as WebhookConfig).url}
|
||||
@@ -1821,8 +1821,8 @@ export default function SiteSettingsPage() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-base font-medium text-neutral-900 dark: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>
|
||||
<h3 className="text-base font-medium text-white">Alerts</h3>
|
||||
<p className="text-sm text-neutral-400 mt-0.5">Get notified when your site goes down or recovers.</p>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<Button onClick={() => { setEditingAlert(null); resetAlertForm(); setAlertModalOpen(true) }}>
|
||||
@@ -1838,7 +1838,7 @@ export default function SiteSettingsPage() {
|
||||
))}
|
||||
</div>
|
||||
) : 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.
|
||||
</div>
|
||||
) : (
|
||||
@@ -1859,14 +1859,14 @@ export default function SiteSettingsPage() {
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<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)}
|
||||
</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">
|
||||
Uptime Alert
|
||||
</span>
|
||||
</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_config as EmailConfig).recipients.join(', ')
|
||||
: (schedule.channel_config as WebhookConfig).url}
|
||||
@@ -1940,8 +1940,8 @@ export default function SiteSettingsPage() {
|
||||
{activeTab === 'integrations' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark: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>
|
||||
<h2 className="text-2xl font-bold text-white mb-1">Integrations</h2>
|
||||
<p className="text-sm text-neutral-400">Connect external services to enrich your analytics data.</p>
|
||||
</div>
|
||||
|
||||
{/* Google Search Console */}
|
||||
@@ -1958,7 +1958,7 @@ export default function SiteSettingsPage() {
|
||||
</svg>
|
||||
</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">
|
||||
See which search queries bring visitors to your site, with impressions, clicks, CTR, and ranking position.
|
||||
</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">
|
||||
<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>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -2005,7 +2005,7 @@ export default function SiteSettingsPage() {
|
||||
</svg>
|
||||
</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">
|
||||
<span className={`inline-flex items-center gap-1.5 text-xs font-medium ${
|
||||
gscStatus.status === 'active'
|
||||
@@ -2031,28 +2031,28 @@ export default function SiteSettingsPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{gscStatus.google_email && (
|
||||
<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-sm font-medium text-neutral-900 dark:text-white mt-0.5 truncate">{gscStatus.google_email}</p>
|
||||
<p className="text-xs text-neutral-400">Google Account</p>
|
||||
<p className="text-sm font-medium text-white mt-0.5 truncate">{gscStatus.google_email}</p>
|
||||
</div>
|
||||
)}
|
||||
{gscStatus.gsc_property && (
|
||||
<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-sm font-medium text-neutral-900 dark:text-white mt-0.5 truncate">{gscStatus.gsc_property}</p>
|
||||
<p className="text-xs text-neutral-400">Property</p>
|
||||
<p className="text-sm font-medium text-white mt-0.5 truncate">{gscStatus.gsc_property}</p>
|
||||
</div>
|
||||
)}
|
||||
{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">
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">Last Synced</p>
|
||||
<p className="text-sm font-medium text-neutral-900 dark:text-white mt-0.5">
|
||||
<p className="text-xs text-neutral-400">Last Synced</p>
|
||||
<p className="text-sm font-medium text-white mt-0.5">
|
||||
{new Date(gscStatus.last_synced_at).toLocaleString('en-GB')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{gscStatus.created_at && (
|
||||
<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-sm font-medium text-neutral-900 dark:text-white mt-0.5">
|
||||
<p className="text-xs text-neutral-400">Connected Since</p>
|
||||
<p className="text-sm font-medium text-white mt-0.5">
|
||||
{new Date(gscStatus.created_at).toLocaleString('en-GB')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -2121,7 +2121,7 @@ export default function SiteSettingsPage() {
|
||||
</svg>
|
||||
</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">
|
||||
Monitor CDN performance with bandwidth usage, cache hit rates, response times, and geographic distribution.
|
||||
</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">
|
||||
<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>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -2147,7 +2147,7 @@ export default function SiteSettingsPage() {
|
||||
setBunnySelectedZone(null)
|
||||
}}
|
||||
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
|
||||
onClick={async () => {
|
||||
@@ -2190,7 +2190,7 @@ export default function SiteSettingsPage() {
|
||||
const zone = bunnyPullZones.find(z => z.id === Number(e.target.value))
|
||||
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) => (
|
||||
<option key={zone.id} value={zone.id}>{zone.name}</option>
|
||||
@@ -2252,7 +2252,7 @@ export default function SiteSettingsPage() {
|
||||
</svg>
|
||||
</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">
|
||||
<span className={`inline-flex items-center gap-1.5 text-xs font-medium ${
|
||||
bunnyStatus.status === 'active'
|
||||
@@ -2278,22 +2278,22 @@ export default function SiteSettingsPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{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">
|
||||
<p className="text-xs text-neutral-500 dark: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-xs text-neutral-400">Pull Zone</p>
|
||||
<p className="text-sm font-medium text-white mt-0.5 truncate">{bunnyStatus.pull_zone_name}</p>
|
||||
</div>
|
||||
)}
|
||||
{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">
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">Last Synced</p>
|
||||
<p className="text-sm font-medium text-neutral-900 dark:text-white mt-0.5">
|
||||
<p className="text-xs text-neutral-400">Last Synced</p>
|
||||
<p className="text-sm font-medium text-white mt-0.5">
|
||||
{new Date(bunnyStatus.last_synced_at).toLocaleString('en-GB')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{bunnyStatus.created_at && (
|
||||
<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-sm font-medium text-neutral-900 dark:text-white mt-0.5">
|
||||
<p className="text-xs text-neutral-400">Connected Since</p>
|
||||
<p className="text-sm font-medium text-white mt-0.5">
|
||||
{new Date(bunnyStatus.created_at).toLocaleString('en-GB')}
|
||||
</p>
|
||||
</div>
|
||||
@@ -2354,7 +2354,7 @@ export default function SiteSettingsPage() {
|
||||
onChange={(e) => setGoalForm({ ...goalForm, name: e.target.value })}
|
||||
placeholder="e.g. Signups"
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
@@ -2366,11 +2366,11 @@ export default function SiteSettingsPage() {
|
||||
onChange={(e) => setGoalForm({ ...goalForm, event_name: e.target.value })}
|
||||
placeholder="e.g. signup_click (letters, numbers, underscores only)"
|
||||
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
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
{editingGoal && goalForm.event_name.trim().toLowerCase().replace(/\s+/g, '_') !== editingGoal.event_name && (
|
||||
@@ -2423,10 +2423,10 @@ export default function SiteSettingsPage() {
|
||||
value={reportForm.recipients}
|
||||
onChange={(e) => setReportForm({ ...reportForm, recipients: e.target.value })}
|
||||
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
|
||||
/>
|
||||
<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>
|
||||
@@ -2438,7 +2438,7 @@ export default function SiteSettingsPage() {
|
||||
value={reportForm.webhookUrl}
|
||||
onChange={(e) => setReportForm({ ...reportForm, webhookUrl: e.target.value })}
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
@@ -2582,10 +2582,10 @@ export default function SiteSettingsPage() {
|
||||
value={alertForm.recipients}
|
||||
onChange={(e) => setAlertForm({ ...alertForm, recipients: e.target.value })}
|
||||
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
|
||||
/>
|
||||
<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>
|
||||
@@ -2597,14 +2597,14 @@ export default function SiteSettingsPage() {
|
||||
value={alertForm.webhookUrl}
|
||||
onChange={(e) => setAlertForm({ ...alertForm, webhookUrl: e.target.value })}
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +100,7 @@ function getOverallStatusTextColor(status: string): string {
|
||||
case 'down':
|
||||
return 'text-red-600 dark:text-red-400'
|
||||
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%)' }}
|
||||
>
|
||||
<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 ? (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="text-neutral-500 dark:text-neutral-400">Uptime</span>
|
||||
<span className="font-medium text-neutral-900 dark:text-white">
|
||||
<span className="text-neutral-400">Uptime</span>
|
||||
<span className="font-medium text-white">
|
||||
{formatUptime(stat.uptime_percentage)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="text-neutral-500 dark:text-neutral-400">Checks</span>
|
||||
<span className="font-medium text-neutral-900 dark:text-white">{stat.total_checks}</span>
|
||||
<span className="text-neutral-400">Checks</span>
|
||||
<span className="font-medium text-white">{stat.total_checks}</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="text-neutral-500 dark:text-neutral-400">Avg Response</span>
|
||||
<span className="font-medium text-neutral-900 dark:text-white">
|
||||
<span className="text-neutral-400">Avg Response</span>
|
||||
<span className="font-medium text-white">
|
||||
{formatMs(Math.round(stat.avg_response_time_ms))}
|
||||
</span>
|
||||
</div>
|
||||
@@ -275,7 +275,7 @@ function ResponseTimeChart({ checks }: { checks: UptimeCheck[] }) {
|
||||
|
||||
return (
|
||||
<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
|
||||
</h4>
|
||||
<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}`}>
|
||||
{/* Header */}
|
||||
<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
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -417,14 +417,14 @@ export default function UptimePage() {
|
||||
{/* 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="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" />
|
||||
</svg>
|
||||
</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
|
||||
</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.
|
||||
</p>
|
||||
{canEdit && (
|
||||
@@ -446,10 +446,10 @@ export default function UptimePage() {
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<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
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -471,7 +471,7 @@ export default function UptimePage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-3.5 h-3.5 rounded-full ${getStatusDotColor(overallStatus)}`} />
|
||||
<div>
|
||||
<span className="font-semibold text-neutral-900 dark:text-white text-lg">
|
||||
<span className="font-semibold text-white text-lg">
|
||||
{site.name}
|
||||
</span>
|
||||
<span className={`text-sm font-medium ml-3 ${getOverallStatusTextColor(overallStatus)}`}>
|
||||
@@ -480,11 +480,11 @@ export default function UptimePage() {
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
</span>
|
||||
{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)}
|
||||
</div>
|
||||
)}
|
||||
@@ -495,7 +495,7 @@ export default function UptimePage() {
|
||||
{/* 90-day uptime bar */}
|
||||
{monitor && (
|
||||
<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
|
||||
</h3>
|
||||
<UptimeStatusBar dailyStats={monitor.daily_stats} />
|
||||
@@ -512,39 +512,39 @@ export default function UptimePage() {
|
||||
{/* Monitor details grid */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-5">
|
||||
<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
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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)}
|
||||
</span>
|
||||
</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
|
||||
</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)}
|
||||
</span>
|
||||
</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
|
||||
</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
|
||||
? `${Math.floor(monitor.monitor.check_interval_seconds / 60)}m`
|
||||
: `${monitor.monitor.check_interval_seconds}s`}
|
||||
</span>
|
||||
</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
|
||||
</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)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -559,7 +559,7 @@ export default function UptimePage() {
|
||||
|
||||
{/* Recent checks */}
|
||||
<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
|
||||
</h4>
|
||||
<div className="space-y-1.5 max-h-48 overflow-y-auto">
|
||||
@@ -576,7 +576,7 @@ export default function UptimePage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{check.status_code && (
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
<span className="text-xs text-neutral-400">
|
||||
{check.status_code}
|
||||
</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">
|
||||
<CheckCircleIcon className="h-7 w-7" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
Site created
|
||||
</h2>
|
||||
<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>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -146,7 +146,7 @@ export default function NewSitePage() {
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
@@ -174,7 +174,7 @@ export default function NewSitePage() {
|
||||
// * Step 1: Name & domain form
|
||||
return (
|
||||
<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
|
||||
</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">
|
||||
<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
|
||||
</label>
|
||||
<Input
|
||||
@@ -201,7 +201,7 @@ export default function NewSitePage() {
|
||||
</div>
|
||||
|
||||
<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
|
||||
</label>
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user