From fa7e9fddb4dbe4e85094dc6ac056d7b1ba89ab05 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 16 Jan 2026 19:48:45 +0100 Subject: [PATCH] feat: add city and region tracking to analytics --- app/sites/[id]/page.tsx | 19 +++++++++++-- components/dashboard/Cities.tsx | 48 ++++++++++++++++++++++++++++++++ components/dashboard/Regions.tsx | 48 ++++++++++++++++++++++++++++++++ lib/api/stats.ts | 28 +++++++++++++++++++ 4 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 components/dashboard/Cities.tsx create mode 100644 components/dashboard/Regions.tsx diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 8d050c3..3b7a8c0 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -3,7 +3,7 @@ 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 } from '@/lib/api/stats' +import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, getCountries, getCities, getRegions } from '@/lib/api/stats' import { formatNumber, getDateRange } from '@/lib/utils/format' import { toast } from 'sonner' import LoadingOverlay from '@/components/LoadingOverlay' @@ -12,6 +12,8 @@ import RealtimeVisitors from '@/components/dashboard/RealtimeVisitors' import TopPages from '@/components/dashboard/TopPages' import TopReferrers from '@/components/dashboard/TopReferrers' import Countries from '@/components/dashboard/Countries' +import Cities from '@/components/dashboard/Cities' +import Regions from '@/components/dashboard/Regions' import Chart from '@/components/dashboard/Chart' export default function SiteDashboardPage() { @@ -27,6 +29,8 @@ export default function SiteDashboardPage() { const [topPages, setTopPages] = useState([]) const [topReferrers, setTopReferrers] = useState([]) const [countries, setCountries] = useState([]) + const [cities, setCities] = useState([]) + const [regions, setRegions] = useState([]) const [dateRange, setDateRange] = useState(getDateRange(30)) useEffect(() => { @@ -40,7 +44,7 @@ export default function SiteDashboardPage() { const loadData = async () => { try { setLoading(true) - const [siteData, statsData, realtimeData, dailyData, pagesData, referrersData, countriesData] = await Promise.all([ + const [siteData, statsData, realtimeData, dailyData, pagesData, referrersData, countriesData, citiesData, regionsData] = await Promise.all([ getSite(siteId), getStats(siteId, dateRange.start, dateRange.end), getRealtime(siteId), @@ -48,6 +52,8 @@ export default function SiteDashboardPage() { getTopPages(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), ]) setSite(siteData) setStats(statsData || { pageviews: 0, visitors: 0 }) @@ -56,6 +62,8 @@ export default function SiteDashboardPage() { setTopPages(Array.isArray(pagesData) ? pagesData : []) setTopReferrers(Array.isArray(referrersData) ? referrersData : []) setCountries(Array.isArray(countriesData) ? countriesData : []) + setCities(Array.isArray(citiesData) ? citiesData : []) + setRegions(Array.isArray(regionsData) ? regionsData : []) } catch (error: any) { toast.error('Failed to load data: ' + (error.message || 'Unknown error')) } finally { @@ -131,10 +139,15 @@ export default function SiteDashboardPage() { -
+
+ +
+ + +
) } diff --git a/components/dashboard/Cities.tsx b/components/dashboard/Cities.tsx new file mode 100644 index 0000000..de277ea --- /dev/null +++ b/components/dashboard/Cities.tsx @@ -0,0 +1,48 @@ +'use client' + +import { formatNumber } from '@/lib/utils/format' +import * as Flags from 'country-flag-icons/react/3x2' + +interface CitiesProps { + cities: Array<{ city: string; country: string; pageviews: number }> +} + +export default function Cities({ cities }: CitiesProps) { + if (!cities || cities.length === 0) { + return ( +
+

+ Cities +

+

No data available

+
+ ) + } + + const getFlagComponent = (countryCode: string) => { + if (!countryCode || countryCode === 'Unknown') return null + const FlagComponent = (Flags as any)[countryCode] + return FlagComponent ? : null + } + + return ( +
+

+ Cities +

+
+ {cities.map((city, index) => ( +
+
+ {getFlagComponent(city.country)} + {city.city === 'Unknown' ? 'Unknown' : city.city} +
+
+ {formatNumber(city.pageviews)} +
+
+ ))} +
+
+ ) +} diff --git a/components/dashboard/Regions.tsx b/components/dashboard/Regions.tsx new file mode 100644 index 0000000..3cf7dc4 --- /dev/null +++ b/components/dashboard/Regions.tsx @@ -0,0 +1,48 @@ +'use client' + +import { formatNumber } from '@/lib/utils/format' +import * as Flags from 'country-flag-icons/react/3x2' + +interface RegionsProps { + regions: Array<{ region: string; country: string; pageviews: number }> +} + +export default function Regions({ regions }: RegionsProps) { + if (!regions || regions.length === 0) { + return ( +
+

+ Regions +

+

No data available

+
+ ) + } + + const getFlagComponent = (countryCode: string) => { + if (!countryCode || countryCode === 'Unknown') return null + const FlagComponent = (Flags as any)[countryCode] + return FlagComponent ? : null + } + + return ( +
+

+ Regions +

+
+ {regions.map((region, index) => ( +
+
+ {getFlagComponent(region.country)} + {region.region === 'Unknown' ? 'Unknown' : region.region} +
+
+ {formatNumber(region.pageviews)} +
+
+ ))} +
+
+ ) +} diff --git a/lib/api/stats.ts b/lib/api/stats.ts index 6916e83..bb46322 100644 --- a/lib/api/stats.ts +++ b/lib/api/stats.ts @@ -20,6 +20,18 @@ export interface CountryStat { pageviews: number } +export interface CityStat { + city: string + country: string + pageviews: number +} + +export interface RegionStat { + region: string + country: string + pageviews: number +} + export interface DailyStat { date: string pageviews: number @@ -66,6 +78,22 @@ export async function getCountries(siteId: string, startDate?: string, endDate?: return apiRequest<{ countries: CountryStat[] }>(`/sites/${siteId}/countries?${params.toString()}`).then(r => r?.countries || []) } +export async function getCities(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<{ cities: CityStat[] }>(`/sites/${siteId}/cities?${params.toString()}`).then(r => r?.cities || []) +} + +export async function getRegions(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<{ regions: RegionStat[] }>(`/sites/${siteId}/regions?${params.toString()}`).then(r => r?.regions || []) +} + export async function getDailyStats(siteId: string, startDate?: string, endDate?: string): Promise { const params = new URLSearchParams() if (startDate) params.append('start_date', startDate)