feat: implement advanced chart dashboard with trends and comparisons
This commit is contained in:
@@ -3,12 +3,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
import { getSite, type Site } from '@/lib/api/sites'
|
import { getSite, type Site } from '@/lib/api/sites'
|
||||||
import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, getCountries, getCities, getRegions, getBrowsers, getOS, getDevices, getScreenResolutions, getEntryPages, getExitPages, getDashboard } from '@/lib/api/stats'
|
import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, getCountries, getCities, getRegions, getBrowsers, getOS, getDevices, getScreenResolutions, getEntryPages, getExitPages, getDashboard, type Stats, type DailyStat } from '@/lib/api/stats'
|
||||||
import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format'
|
import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import LoadingOverlay from '@/components/LoadingOverlay'
|
import LoadingOverlay from '@/components/LoadingOverlay'
|
||||||
import StatsCard from '@/components/dashboard/StatsCard'
|
|
||||||
import RealtimeVisitors from '@/components/dashboard/RealtimeVisitors'
|
|
||||||
import ContentStats from '@/components/dashboard/ContentStats'
|
import ContentStats from '@/components/dashboard/ContentStats'
|
||||||
import TopReferrers from '@/components/dashboard/TopReferrers'
|
import TopReferrers from '@/components/dashboard/TopReferrers'
|
||||||
import Locations from '@/components/dashboard/Locations'
|
import Locations from '@/components/dashboard/Locations'
|
||||||
@@ -22,9 +20,11 @@ export default function SiteDashboardPage() {
|
|||||||
|
|
||||||
const [site, setSite] = useState<Site | null>(null)
|
const [site, setSite] = useState<Site | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [stats, setStats] = useState({ pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 })
|
const [stats, setStats] = useState<Stats>({ pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 })
|
||||||
|
const [prevStats, setPrevStats] = useState<Stats | undefined>(undefined)
|
||||||
const [realtime, setRealtime] = useState(0)
|
const [realtime, setRealtime] = useState(0)
|
||||||
const [dailyStats, setDailyStats] = useState<any[]>([])
|
const [dailyStats, setDailyStats] = useState<DailyStat[]>([])
|
||||||
|
const [prevDailyStats, setPrevDailyStats] = useState<DailyStat[] | undefined>(undefined)
|
||||||
const [topPages, setTopPages] = useState<any[]>([])
|
const [topPages, setTopPages] = useState<any[]>([])
|
||||||
const [entryPages, setEntryPages] = useState<any[]>([])
|
const [entryPages, setEntryPages] = useState<any[]>([])
|
||||||
const [exitPages, setExitPages] = useState<any[]>([])
|
const [exitPages, setExitPages] = useState<any[]>([])
|
||||||
@@ -46,15 +46,55 @@ export default function SiteDashboardPage() {
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [siteId, dateRange])
|
}, [siteId, dateRange])
|
||||||
|
|
||||||
|
const getPreviousDateRange = (start: string, end: string) => {
|
||||||
|
const startDate = new Date(start)
|
||||||
|
const endDate = new Date(end)
|
||||||
|
const duration = endDate.getTime() - startDate.getTime()
|
||||||
|
|
||||||
|
// * If duration is 0 (Today), set previous range to yesterday
|
||||||
|
if (duration === 0) {
|
||||||
|
const prevEnd = new Date(startDate.getTime() - 24 * 60 * 60 * 1000)
|
||||||
|
const prevStart = prevEnd
|
||||||
|
return {
|
||||||
|
start: prevStart.toISOString().split('T')[0],
|
||||||
|
end: prevEnd.toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevEnd = new Date(startDate.getTime() - 24 * 60 * 60 * 1000)
|
||||||
|
const prevStart = new Date(prevEnd.getTime() - duration)
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: prevStart.toISOString().split('T')[0],
|
||||||
|
end: prevEnd.toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const data = await getDashboard(siteId, dateRange.start, dateRange.end, 10)
|
const interval = dateRange.start === dateRange.end ? 'hour' : 'day'
|
||||||
|
|
||||||
|
const [data, prevStatsData, prevDailyStatsData] = await Promise.all([
|
||||||
|
getDashboard(siteId, dateRange.start, dateRange.end, 10, interval),
|
||||||
|
(async () => {
|
||||||
|
const prevRange = getPreviousDateRange(dateRange.start, dateRange.end)
|
||||||
|
return getStats(siteId, prevRange.start, prevRange.end)
|
||||||
|
})(),
|
||||||
|
(async () => {
|
||||||
|
const prevRange = getPreviousDateRange(dateRange.start, dateRange.end)
|
||||||
|
return getDailyStats(siteId, prevRange.start, prevRange.end, interval)
|
||||||
|
})()
|
||||||
|
])
|
||||||
|
|
||||||
setSite(data.site)
|
setSite(data.site)
|
||||||
setStats(data.stats || { pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 })
|
setStats(data.stats || { pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 })
|
||||||
setRealtime(data.realtime_visitors || 0)
|
setRealtime(data.realtime_visitors || 0)
|
||||||
setDailyStats(Array.isArray(data.daily_stats) ? data.daily_stats : [])
|
setDailyStats(Array.isArray(data.daily_stats) ? data.daily_stats : [])
|
||||||
|
|
||||||
|
setPrevStats(prevStatsData)
|
||||||
|
setPrevDailyStats(prevDailyStatsData)
|
||||||
|
|
||||||
setTopPages(Array.isArray(data.top_pages) ? data.top_pages : [])
|
setTopPages(Array.isArray(data.top_pages) ? data.top_pages : [])
|
||||||
setEntryPages(Array.isArray(data.entry_pages) ? data.entry_pages : [])
|
setEntryPages(Array.isArray(data.entry_pages) ? data.entry_pages : [])
|
||||||
setExitPages(Array.isArray(data.exit_pages) ? data.exit_pages : [])
|
setExitPages(Array.isArray(data.exit_pages) ? data.exit_pages : [])
|
||||||
@@ -98,6 +138,7 @@ export default function SiteDashboardPage() {
|
|||||||
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
<div className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-neutral-900 dark:text-white mb-2">
|
<h1 className="text-3xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||||
{site.name}
|
{site.name}
|
||||||
@@ -106,15 +147,41 @@ export default function SiteDashboardPage() {
|
|||||||
{site.domain}
|
{site.domain}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Realtime Indicator */}
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1 bg-green-500/10 rounded-full border border-green-500/20">
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium text-green-700 dark:text-green-400">
|
||||||
|
{realtime} current visitors
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<select
|
<select
|
||||||
value={dateRange.start === getDateRange(7).start ? '7' : dateRange.start === getDateRange(30).start ? '30' : 'custom'}
|
value={
|
||||||
|
dateRange.start === new Date().toISOString().split('T')[0] && dateRange.end === new Date().toISOString().split('T')[0]
|
||||||
|
? 'today'
|
||||||
|
: dateRange.start === getDateRange(7).start
|
||||||
|
? '7'
|
||||||
|
: dateRange.start === getDateRange(30).start
|
||||||
|
? '30'
|
||||||
|
: 'custom'
|
||||||
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.value === '7') setDateRange(getDateRange(7))
|
if (e.target.value === '7') setDateRange(getDateRange(7))
|
||||||
else if (e.target.value === '30') setDateRange(getDateRange(30))
|
else if (e.target.value === '30') setDateRange(getDateRange(30))
|
||||||
|
else if (e.target.value === 'today') {
|
||||||
|
const today = new Date().toISOString().split('T')[0]
|
||||||
|
setDateRange({ start: today, end: today })
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="btn-secondary text-sm"
|
className="btn-secondary text-sm"
|
||||||
>
|
>
|
||||||
|
<option value="today">Today</option>
|
||||||
<option value="7">Last 7 days</option>
|
<option value="7">Last 7 days</option>
|
||||||
<option value="30">Last 30 days</option>
|
<option value="30">Last 30 days</option>
|
||||||
<option value="custom">Custom</option>
|
<option value="custom">Custom</option>
|
||||||
@@ -129,16 +196,15 @@ export default function SiteDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-5 mb-8">
|
{/* Advanced Chart with Integrated Stats */}
|
||||||
<StatsCard title="Pageviews" value={formatNumber(stats.pageviews)} />
|
|
||||||
<StatsCard title="Visitors" value={formatNumber(stats.visitors)} />
|
|
||||||
<RealtimeVisitors count={realtime} siteId={siteId} />
|
|
||||||
<StatsCard title="Bounce Rate" value={`${Math.round(stats.bounce_rate)}%`} />
|
|
||||||
<StatsCard title="Avg Visit Duration" value={formatDuration(stats.avg_duration)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<Chart data={dailyStats} />
|
<Chart
|
||||||
|
data={dailyStats}
|
||||||
|
prevData={prevDailyStats}
|
||||||
|
stats={stats}
|
||||||
|
prevStats={prevStats}
|
||||||
|
interval={dateRange.start === dateRange.end ? 'hour' : 'day'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 lg:grid-cols-2 mb-8">
|
<div className="grid gap-6 lg:grid-cols-2 mb-8">
|
||||||
|
|||||||
@@ -1,44 +1,233 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
import { useState } from 'react'
|
||||||
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||||
|
import { formatNumber, formatDuration } from '@/lib/utils/format'
|
||||||
|
import { ArrowTopRightIcon, ArrowBottomRightIcon, DownloadIcon } from '@radix-ui/react-icons'
|
||||||
|
|
||||||
interface ChartProps {
|
interface DailyStat {
|
||||||
data: Array<{ date: string; pageviews: number; visitors: number }>
|
date: string
|
||||||
|
pageviews: number
|
||||||
|
visitors: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Chart({ data }: ChartProps) {
|
interface Stats {
|
||||||
const chartData = data.map(item => ({
|
pageviews: number
|
||||||
date: new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
visitors: number
|
||||||
|
bounce_rate: number
|
||||||
|
avg_duration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartProps {
|
||||||
|
data: DailyStat[]
|
||||||
|
prevData?: DailyStat[]
|
||||||
|
stats: Stats
|
||||||
|
prevStats?: Stats
|
||||||
|
interval: 'hour' | 'day' | 'month'
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricType = 'pageviews' | 'visitors' | 'bounce_rate' | 'avg_duration'
|
||||||
|
|
||||||
|
export default function Chart({ data, prevData, stats, prevStats, interval }: ChartProps) {
|
||||||
|
const [metric, setMetric] = useState<MetricType>('visitors')
|
||||||
|
|
||||||
|
// * Align current and previous data
|
||||||
|
const chartData = data.map((item, i) => {
|
||||||
|
// * Try to find matching previous item (assuming same length/order)
|
||||||
|
// * For more robustness, we could match by relative index
|
||||||
|
const prevItem = prevData && prevData[i]
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: new Date(item.date).toLocaleDateString('en-US', interval === 'hour' ? { hour: 'numeric', minute: 'numeric' } : { month: 'short', day: 'numeric' }),
|
||||||
|
originalDate: item.date,
|
||||||
pageviews: item.pageviews,
|
pageviews: item.pageviews,
|
||||||
visitors: item.visitors,
|
visitors: item.visitors,
|
||||||
}))
|
prevPageviews: prevItem?.pageviews,
|
||||||
|
prevVisitors: prevItem?.visitors,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// * Calculate trends
|
||||||
|
const calculateTrend = (current: number, previous?: number) => {
|
||||||
|
if (!previous) return null
|
||||||
|
if (previous === 0) return current > 0 ? 100 : 0
|
||||||
|
return Math.round(((current - previous) / previous) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
const csvContent = "data:text/csv;charset=utf-8,"
|
||||||
|
+ "Date,Pageviews,Visitors\n"
|
||||||
|
+ data.map(row => `${new Date(row.date).toISOString()},${row.pageviews},${row.visitors}`).join("\n")
|
||||||
|
|
||||||
|
const encodedUri = encodeURI(csvContent)
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.setAttribute("href", encodedUri)
|
||||||
|
link.setAttribute("download", `analytics_export_${new Date().toISOString().split('T')[0]}.csv`)
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = [
|
||||||
|
{
|
||||||
|
id: 'visitors',
|
||||||
|
label: 'Unique Visitors',
|
||||||
|
value: formatNumber(stats.visitors),
|
||||||
|
trend: calculateTrend(stats.visitors, prevStats?.visitors),
|
||||||
|
color: '#4F46E5', // Indigo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pageviews',
|
||||||
|
label: 'Total Pageviews',
|
||||||
|
value: formatNumber(stats.pageviews),
|
||||||
|
trend: calculateTrend(stats.pageviews, prevStats?.pageviews),
|
||||||
|
color: '#FD5E0F', // Orange
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bounce_rate',
|
||||||
|
label: 'Bounce Rate',
|
||||||
|
value: `${Math.round(stats.bounce_rate)}%`,
|
||||||
|
trend: calculateTrend(stats.bounce_rate, prevStats?.bounce_rate),
|
||||||
|
color: '#EF4444', // Red
|
||||||
|
invertTrend: true, // Lower bounce rate is better
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'avg_duration',
|
||||||
|
label: 'Visit Duration',
|
||||||
|
value: formatDuration(stats.avg_duration),
|
||||||
|
trend: calculateTrend(stats.avg_duration, prevStats?.avg_duration),
|
||||||
|
color: '#10B981', // Emerald
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const activeMetric = metrics.find(m => m.id === metric) || metrics[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden shadow-sm">
|
||||||
<h3 className="text-lg font-semibold mb-4 text-neutral-900 dark:text-white">
|
{/* Stats Header (Interactive Tabs) */}
|
||||||
Pageviews Over Time
|
<div className="grid grid-cols-2 md:grid-cols-4 divide-x divide-neutral-200 dark:divide-neutral-800 border-b border-neutral-200 dark:border-neutral-800">
|
||||||
</h3>
|
{metrics.map((item) => (
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
<button
|
||||||
<LineChart data={chartData}>
|
key={item.id}
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e5e5" />
|
onClick={() => {
|
||||||
<XAxis dataKey="date" stroke="#737373" />
|
if (item.id === 'visitors' || item.id === 'pageviews') {
|
||||||
<YAxis stroke="#737373" />
|
setMetric(item.id as MetricType)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`
|
||||||
|
p-6 text-left transition-colors relative group
|
||||||
|
hover:bg-neutral-50 dark:hover:bg-neutral-800/50
|
||||||
|
${metric === item.id ? 'bg-neutral-50 dark:bg-neutral-800/50' : ''}
|
||||||
|
${(item.id !== 'visitors' && item.id !== 'pageviews') ? 'cursor-default' : 'cursor-pointer'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className={`text-xs font-semibold uppercase tracking-wider mb-1 flex items-center gap-2 ${metric === item.id ? 'text-neutral-900 dark:text-white' : 'text-neutral-500'}`}>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<span className="text-2xl font-bold text-neutral-900 dark:text-white">
|
||||||
|
{item.value}
|
||||||
|
</span>
|
||||||
|
{item.trend !== null && (
|
||||||
|
<span className={`flex items-center text-sm font-medium ${
|
||||||
|
(item.invertTrend ? -item.trend : item.trend) > 0
|
||||||
|
? 'text-emerald-600 dark:text-emerald-500'
|
||||||
|
: (item.invertTrend ? -item.trend : item.trend) < 0
|
||||||
|
? 'text-red-600 dark:text-red-500'
|
||||||
|
: 'text-neutral-500'
|
||||||
|
}`}>
|
||||||
|
{(item.invertTrend ? -item.trend : item.trend) > 0 ? (
|
||||||
|
<ArrowTopRightIcon className="w-3 h-3 mr-0.5" />
|
||||||
|
) : (item.invertTrend ? -item.trend : item.trend) < 0 ? (
|
||||||
|
<ArrowBottomRightIcon className="w-3 h-3 mr-0.5" />
|
||||||
|
) : null}
|
||||||
|
{Math.abs(item.trend)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{metric === item.id && (
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-1" style={{ backgroundColor: item.color }} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart Area */}
|
||||||
|
<div className="p-6 relative">
|
||||||
|
<div className="absolute top-6 right-6 z-10">
|
||||||
|
<button
|
||||||
|
onClick={handleExport}
|
||||||
|
className="p-2 rounded-lg text-neutral-500 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
|
||||||
|
title="Export to CSV"
|
||||||
|
>
|
||||||
|
<DownloadIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="h-[400px] w-full">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<AreaChart data={chartData} margin={{ top: 10, right: 0, left: -20, bottom: 0 }}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id={`gradient-${metric}`} x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor={activeMetric.color} stopOpacity={0.2}/>
|
||||||
|
<stop offset="95%" stopColor={activeMetric.color} stopOpacity={0}/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E5E5E5" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="date"
|
||||||
|
stroke="#A3A3A3"
|
||||||
|
fontSize={12}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
minTickGap={30}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
stroke="#A3A3A3"
|
||||||
|
fontSize={12}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => value >= 1000 ? `${value/1000}k` : value}
|
||||||
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||||
border: '1px solid #e5e5e5',
|
border: '1px solid #E5E5E5',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||||
|
padding: '12px'
|
||||||
}}
|
}}
|
||||||
|
itemStyle={{ color: '#171717', fontWeight: 600, fontSize: '14px' }}
|
||||||
|
labelStyle={{ color: '#737373', marginBottom: '8px', fontSize: '12px' }}
|
||||||
|
cursor={{ stroke: activeMetric.color, strokeDasharray: '4 4' }}
|
||||||
/>
|
/>
|
||||||
<Line
|
|
||||||
|
{/* Previous Period Line (Dashed) */}
|
||||||
|
{prevData && (
|
||||||
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="pageviews"
|
dataKey={metric === 'visitors' ? 'prevVisitors' : 'prevPageviews'}
|
||||||
stroke="#FD5E0F"
|
stroke="#A3A3A3"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dot={{ fill: '#FD5E0F' }}
|
strokeDasharray="4 4"
|
||||||
|
fill="none"
|
||||||
|
animationDuration={1000}
|
||||||
/>
|
/>
|
||||||
</LineChart>
|
)}
|
||||||
|
|
||||||
|
{/* Current Period Area */}
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey={metric}
|
||||||
|
stroke={activeMetric.color}
|
||||||
|
strokeWidth={2}
|
||||||
|
fillOpacity={1}
|
||||||
|
fill={`url(#gradient-${metric})`}
|
||||||
|
animationDuration={1000}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,12 +142,14 @@ export async function getDevices(siteId: string, startDate?: string, endDate?: s
|
|||||||
return apiRequest<{ devices: DeviceStat[] }>(`/sites/${siteId}/devices?${params.toString()}`).then(r => r?.devices || [])
|
return apiRequest<{ devices: DeviceStat[] }>(`/sites/${siteId}/devices?${params.toString()}`).then(r => r?.devices || [])
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDailyStats(siteId: string, startDate?: string, endDate?: string): Promise<DailyStat[]> {
|
export async function getDailyStats(siteId: string, startDate?: string, endDate?: string, interval?: string): Promise<DailyStat[]> {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (startDate) params.append('start_date', startDate)
|
if (startDate) params.append('start_date', startDate)
|
||||||
if (endDate) params.append('end_date', endDate)
|
if (endDate) params.append('end_date', endDate)
|
||||||
|
if (interval) params.append('interval', interval)
|
||||||
return apiRequest<{ stats: DailyStat[] }>(`/sites/${siteId}/daily?${params.toString()}`).then(r => r?.stats || [])
|
return apiRequest<{ stats: DailyStat[] }>(`/sites/${siteId}/daily?${params.toString()}`).then(r => r?.stats || [])
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntryPages(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise<TopPage[]> {
|
export async function getEntryPages(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise<TopPage[]> {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (startDate) params.append('start_date', startDate)
|
if (startDate) params.append('start_date', startDate)
|
||||||
@@ -190,10 +192,11 @@ export interface DashboardData {
|
|||||||
screen_resolutions: ScreenResolutionStat[]
|
screen_resolutions: ScreenResolutionStat[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDashboard(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise<DashboardData> {
|
export async function getDashboard(siteId: string, startDate?: string, endDate?: string, limit = 10, interval?: string): Promise<DashboardData> {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (startDate) params.append('start_date', startDate)
|
if (startDate) params.append('start_date', startDate)
|
||||||
if (endDate) params.append('end_date', endDate)
|
if (endDate) params.append('end_date', endDate)
|
||||||
|
if (interval) params.append('interval', interval)
|
||||||
params.append('limit', limit.toString())
|
params.append('limit', limit.toString())
|
||||||
return apiRequest<DashboardData>(`/sites/${siteId}/dashboard?${params.toString()}`)
|
return apiRequest<DashboardData>(`/sites/${siteId}/dashboard?${params.toString()}`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user