diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 0d254cf..0bf773d 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -3,15 +3,15 @@ import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { getSite, type Site } from '@/lib/api/sites' -import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, getCountries, getCities, getBrowsers, getOS, getDevices } from '@/lib/api/stats' -import { formatNumber, getDateRange } from '@/lib/utils/format' +import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, getCountries, getCities, getRegions, getBrowsers, getOS, getDevices, getScreenResolutions, getEntryPages, getExitPages } from '@/lib/api/stats' +import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format' import { toast } from 'sonner' import LoadingOverlay from '@/components/LoadingOverlay' import StatsCard from '@/components/dashboard/StatsCard' import RealtimeVisitors from '@/components/dashboard/RealtimeVisitors' -import TopPages from '@/components/dashboard/TopPages' +import ContentStats from '@/components/dashboard/ContentStats' import TopReferrers from '@/components/dashboard/TopReferrers' -import Countries from '@/components/dashboard/Countries' +import Locations from '@/components/dashboard/Locations' import TechSpecs from '@/components/dashboard/TechSpecs' import Chart from '@/components/dashboard/Chart' @@ -22,16 +22,20 @@ export default function SiteDashboardPage() { const [site, setSite] = useState(null) const [loading, setLoading] = useState(true) - const [stats, setStats] = useState({ pageviews: 0, visitors: 0 }) + const [stats, setStats] = useState({ pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 }) const [realtime, setRealtime] = useState(0) const [dailyStats, setDailyStats] = useState([]) const [topPages, setTopPages] = useState([]) + const [entryPages, setEntryPages] = useState([]) + const [exitPages, setExitPages] = useState([]) const [topReferrers, setTopReferrers] = useState([]) const [countries, setCountries] = useState([]) const [cities, setCities] = useState([]) + const [regions, setRegions] = useState([]) const [browsers, setBrowsers] = useState([]) const [os, setOS] = useState([]) const [devices, setDevices] = useState([]) + const [screenResolutions, setScreenResolutions] = useState([]) const [dateRange, setDateRange] = useState(getDateRange(30)) useEffect(() => { @@ -45,30 +49,54 @@ export default function SiteDashboardPage() { const loadData = async () => { try { setLoading(true) - const [siteData, statsData, realtimeData, dailyData, pagesData, referrersData, countriesData, citiesData, browsersData, osData, devicesData] = await Promise.all([ + const [ + siteData, + statsData, + realtimeData, + dailyData, + pagesData, + entryPagesData, + exitPagesData, + referrersData, + countriesData, + citiesData, + regionsData, + browsersData, + osData, + devicesData, + screensData + ] = await Promise.all([ getSite(siteId), getStats(siteId, dateRange.start, dateRange.end), getRealtime(siteId), getDailyStats(siteId, dateRange.start, dateRange.end), getTopPages(siteId, dateRange.start, dateRange.end, 10), + getEntryPages(siteId, dateRange.start, dateRange.end, 10), + getExitPages(siteId, dateRange.start, dateRange.end, 10), getTopReferrers(siteId, dateRange.start, dateRange.end, 10), getCountries(siteId, dateRange.start, dateRange.end, 10), getCities(siteId, dateRange.start, dateRange.end, 10), + getRegions(siteId, dateRange.start, dateRange.end, 10), getBrowsers(siteId, dateRange.start, dateRange.end, 10), getOS(siteId, dateRange.start, dateRange.end, 10), getDevices(siteId, dateRange.start, dateRange.end, 10), + getScreenResolutions(siteId, dateRange.start, dateRange.end, 10), ]) setSite(siteData) - setStats(statsData || { pageviews: 0, visitors: 0 }) + setStats(statsData || { pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 }) setRealtime(realtimeData?.visitors || 0) setDailyStats(Array.isArray(dailyData) ? dailyData : []) setTopPages(Array.isArray(pagesData) ? pagesData : []) + setEntryPages(Array.isArray(entryPagesData) ? entryPagesData : []) + setExitPages(Array.isArray(exitPagesData) ? exitPagesData : []) setTopReferrers(Array.isArray(referrersData) ? referrersData : []) setCountries(Array.isArray(countriesData) ? countriesData : []) setCities(Array.isArray(citiesData) ? citiesData : []) + setRegions(Array.isArray(regionsData) ? regionsData : []) setBrowsers(Array.isArray(browsersData) ? browsersData : []) setOS(Array.isArray(osData) ? osData : []) setDevices(Array.isArray(devicesData) ? devicesData : []) + setScreenResolutions(Array.isArray(screensData) ? screensData : []) } catch (error: any) { toast.error('Failed to load data: ' + (error.message || 'Unknown error')) } finally { @@ -132,25 +160,26 @@ export default function SiteDashboardPage() { -
+
- + +
- +
- +
- +
) diff --git a/components/dashboard/ContentStats.tsx b/components/dashboard/ContentStats.tsx new file mode 100644 index 0000000..952b2f6 --- /dev/null +++ b/components/dashboard/ContentStats.tsx @@ -0,0 +1,91 @@ +'use client' + +import { useState } from 'react' +import { formatNumber } from '@/lib/utils/format' +import { TopPage } from '@/lib/api/stats' + +interface ContentStatsProps { + topPages: TopPage[] + entryPages: TopPage[] + exitPages: TopPage[] +} + +type Tab = 'top_pages' | 'entry_pages' | 'exit_pages' + +export default function ContentStats({ topPages, entryPages, exitPages }: ContentStatsProps) { + const [activeTab, setActiveTab] = useState('top_pages') + + const renderContent = () => { + let data: TopPage[] = [] + + if (activeTab === 'top_pages') { + data = topPages + } else if (activeTab === 'entry_pages') { + data = entryPages + } else if (activeTab === 'exit_pages') { + data = exitPages + } + + if (!data || data.length === 0) { + return

No data available

+ } + + return ( +
+ {data.map((page, index) => ( +
+
+ {page.path} +
+
+ {formatNumber(page.pageviews)} +
+
+ ))} +
+ ) + } + + return ( +
+
+

+ Content +

+
+ + + +
+
+ {renderContent()} +
+ ) +} diff --git a/components/dashboard/Countries.tsx b/components/dashboard/Locations.tsx similarity index 75% rename from components/dashboard/Countries.tsx rename to components/dashboard/Locations.tsx index d416820..6f57197 100644 --- a/components/dashboard/Countries.tsx +++ b/components/dashboard/Locations.tsx @@ -8,11 +8,12 @@ import WorldMap from './WorldMap' interface LocationProps { countries: Array<{ country: string; pageviews: number }> cities: Array<{ city: string; country: string; pageviews: number }> + regions: Array<{ region: string; country: string; pageviews: number }> } -type Tab = 'map' | 'countries' | 'cities' +type Tab = 'map' | 'countries' | 'regions' | 'cities' -export default function Locations({ countries, cities }: LocationProps) { +export default function Locations({ countries, cities, regions }: LocationProps) { const [activeTab, setActiveTab] = useState('map') const getFlagComponent = (countryCode: string) => { @@ -62,6 +63,27 @@ export default function Locations({ countries, cities }: LocationProps) { ) } + if (activeTab === 'regions') { + if (!regions || regions.length === 0) { + return

No data available

+ } + return ( +
+ {regions.map((region, index) => ( +
+
+ {getFlagComponent(region.country)} + {region.region === 'Unknown' ? 'Unknown' : region.region} +
+
+ {formatNumber(region.pageviews)} +
+
+ ))} +
+ ) + } + if (activeTab === 'cities') { if (!cities || cities.length === 0) { return

No data available

@@ -111,6 +133,16 @@ export default function Locations({ countries, cities }: LocationProps) { > Countries + + {renderContent()} diff --git a/components/dashboard/TopPages.tsx b/components/dashboard/TopPages.tsx deleted file mode 100644 index 84c3e92..0000000 --- a/components/dashboard/TopPages.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client' - -import { formatNumber } from '@/lib/utils/format' - -interface TopPagesProps { - pages: Array<{ path: string; pageviews: number }> -} - -export default function TopPages({ pages }: TopPagesProps) { - if (!pages || pages.length === 0) { - return ( -
-

- Top Pages -

-

No data available

-
- ) - } - - return ( -
-

- Top Pages -

-
- {pages.map((page, index) => ( -
-
- {page.path} -
-
- {formatNumber(page.pageviews)} -
-
- ))} -
-
- ) -} diff --git a/lib/api/stats.ts b/lib/api/stats.ts index fe232cb..475abeb 100644 --- a/lib/api/stats.ts +++ b/lib/api/stats.ts @@ -3,11 +3,19 @@ import apiRequest from './client' export interface Stats { pageviews: number visitors: number + bounce_rate: number + avg_duration: number } export interface TopPage { path: string pageviews: number + visits?: number // For entry/exit pages +} + +export interface ScreenResolutionStat { + screen_resolution: string + pageviews: number } export interface TopReferrer { @@ -139,3 +147,26 @@ export async function getDailyStats(siteId: string, startDate?: string, endDate? if (endDate) params.append('end_date', endDate) 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 { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest<{ pages: TopPage[] }>(`/sites/${siteId}/entry-pages?${params.toString()}`).then(r => r?.pages || []) +} + +export async function getExitPages(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest<{ pages: TopPage[] }>(`/sites/${siteId}/exit-pages?${params.toString()}`).then(r => r?.pages || []) +} + +export async function getScreenResolutions(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest<{ screen_resolutions: ScreenResolutionStat[] }>(`/sites/${siteId}/screen-resolutions?${params.toString()}`).then(r => r?.screen_resolutions || []) +} diff --git a/lib/api/stats_extra.ts b/lib/api/stats_extra.ts new file mode 100644 index 0000000..025f9cf --- /dev/null +++ b/lib/api/stats_extra.ts @@ -0,0 +1,23 @@ +export async function getEntryPages(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest<{ pages: TopPage[] }>(`/sites/${siteId}/entry-pages?${params.toString()}`).then(r => r?.pages || []) +} + +export async function getExitPages(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest<{ pages: TopPage[] }>(`/sites/${siteId}/exit-pages?${params.toString()}`).then(r => r?.pages || []) +} + +export async function getScreenResolutions(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest<{ screen_resolutions: ScreenResolutionStat[] }>(`/sites/${siteId}/screen-resolutions?${params.toString()}`).then(r => r?.screen_resolutions || []) +} diff --git a/lib/utils/format.ts b/lib/utils/format.ts index 339aa36..fb0a200 100644 --- a/lib/utils/format.ts +++ b/lib/utils/format.ts @@ -42,3 +42,17 @@ export function formatRelativeTime(date: string | Date): string { if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago` return 'Just now' } +/** + * Format duration in seconds to "1m 30s" or "30s" + */ +export function formatDuration(seconds: number): string { + if (!seconds) return '0s' + + const m = Math.floor(seconds / 60) + const s = Math.floor(seconds % 60) + + if (m > 0) { + return `${m}m ${s}s` + } + return `${s}s` +} diff --git a/lib/utils/format_extra.ts b/lib/utils/format_extra.ts new file mode 100644 index 0000000..47810f0 --- /dev/null +++ b/lib/utils/format_extra.ts @@ -0,0 +1,14 @@ +/** + * Format duration in seconds to "1m 30s" or "30s" + */ +export function formatDuration(seconds: number): string { + if (!seconds) return '0s' + + const m = Math.floor(seconds / 60) + const s = Math.floor(seconds % 60) + + if (m > 0) { + return `${m}m ${s}s` + } + return `${s}s` +}