From 415f1675a9180d9da460711da2571ff7dd42b83d Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 30 Jan 2026 13:33:04 +0100 Subject: [PATCH] feat: enhance dashboard components with siteId and dateRange props for improved data fetching and display --- app/sites/[id]/page.tsx | 8 +++ components/dashboard/ContentStats.tsx | 76 +++++++++++++++++++-------- components/dashboard/Locations.tsx | 69 ++++++++++++++++++------ components/dashboard/TechSpecs.tsx | 67 ++++++++++++++++++----- components/dashboard/TopReferrers.tsx | 58 +++++++++++++++----- 5 files changed, 214 insertions(+), 64 deletions(-) diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 8e51777..02bd2f7 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -268,10 +268,14 @@ export default function SiteDashboardPage() { exitPages={exitPages} domain={site.domain} collectPagePaths={site.collect_page_paths ?? true} + siteId={siteId} + dateRange={dateRange} /> @@ -281,6 +285,8 @@ export default function SiteDashboardPage() { cities={cities} regions={regions} geoDataLevel={site.collect_geo_data || 'full'} + siteId={siteId} + dateRange={dateRange} /> diff --git a/components/dashboard/ContentStats.tsx b/components/dashboard/ContentStats.tsx index bb16f05..ac81718 100644 --- a/components/dashboard/ContentStats.tsx +++ b/components/dashboard/ContentStats.tsx @@ -1,8 +1,8 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { formatNumber } from '@/lib/utils/format' -import { TopPage } from '@/lib/api/stats' +import { TopPage, getTopPages, getEntryPages, getExitPages } from '@/lib/api/stats' import { Modal, ArrowUpRightIcon } from '@ciphera-net/ui' interface ContentStatsProps { @@ -11,15 +11,19 @@ interface ContentStatsProps { exitPages: TopPage[] domain: string collectPagePaths?: boolean + siteId: string + dateRange: { start: string, end: string } } type Tab = 'top_pages' | 'entry_pages' | 'exit_pages' const LIMIT = 7 -export default function ContentStats({ topPages, entryPages, exitPages, domain, collectPagePaths = true }: ContentStatsProps) { +export default function ContentStats({ topPages, entryPages, exitPages, domain, collectPagePaths = true, siteId, dateRange }: ContentStatsProps) { const [activeTab, setActiveTab] = useState('top_pages') const [isModalOpen, setIsModalOpen] = useState(false) + const [fullData, setFullData] = useState([]) + const [isLoadingFull, setIsLoadingFull] = useState(false) // Filter out generic "/" entries when page paths are disabled (all traffic shows as "/") const filterGenericPaths = (pages: TopPage[]) => { @@ -28,6 +32,32 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain, return pages.filter(p => p.path && p.path !== '') } + useEffect(() => { + if (isModalOpen) { + const fetchData = async () => { + setIsLoadingFull(true) + try { + let data: TopPage[] = [] + if (activeTab === 'top_pages') { + data = await getTopPages(siteId, dateRange.start, dateRange.end, 100) + } else if (activeTab === 'entry_pages') { + data = await getEntryPages(siteId, dateRange.start, dateRange.end, 100) + } else if (activeTab === 'exit_pages') { + data = await getExitPages(siteId, dateRange.start, dateRange.end, 100) + } + setFullData(filterGenericPaths(data)) + } catch (e) { + console.error(e) + } finally { + setIsLoadingFull(false) + } + } + fetchData() + } else { + setFullData([]) + } + }, [isModalOpen, activeTab, siteId, dateRange, collectPagePaths]) + const getData = () => { switch (activeTab) { case 'top_pages': @@ -132,26 +162,30 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain, title={`Content - ${getTabLabel(activeTab)}`} >
- {data.map((page, index) => ( -
-
- - {page.path} - - + {isLoadingFull ? ( +
Loading...
+ ) : ( + (fullData.length > 0 ? fullData : data).map((page, index) => ( +
+ +
+ {formatNumber(page.pageviews)} +
-
- {formatNumber(page.pageviews)} -
-
- ))} + )) + )}
) -} +} \ No newline at end of file diff --git a/components/dashboard/Locations.tsx b/components/dashboard/Locations.tsx index 8ed96bd..2129e02 100644 --- a/components/dashboard/Locations.tsx +++ b/components/dashboard/Locations.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { formatNumber } from '@/lib/utils/format' import * as Flags from 'country-flag-icons/react/3x2' // @ts-ignore @@ -9,21 +9,52 @@ import WorldMap from './WorldMap' import { Modal, GlobeIcon } from '@ciphera-net/ui' import { SiTorproject } from 'react-icons/si' import { FaUserSecret, FaSatellite } from 'react-icons/fa' +import { getCountries, getCities, getRegions } from '@/lib/api/stats' interface LocationProps { countries: Array<{ country: string; pageviews: number }> cities: Array<{ city: string; country: string; pageviews: number }> regions: Array<{ region: string; country: string; pageviews: number }> geoDataLevel?: 'full' | 'country' | 'none' + siteId: string + dateRange: { start: string, end: string } } type Tab = 'map' | 'countries' | 'regions' | 'cities' const LIMIT = 7 -export default function Locations({ countries, cities, regions, geoDataLevel = 'full' }: LocationProps) { +export default function Locations({ countries, cities, regions, geoDataLevel = 'full', siteId, dateRange }: LocationProps) { const [activeTab, setActiveTab] = useState('map') const [isModalOpen, setIsModalOpen] = useState(false) + const [fullData, setFullData] = useState([]) + const [isLoadingFull, setIsLoadingFull] = useState(false) + + useEffect(() => { + if (isModalOpen) { + const fetchData = async () => { + setIsLoadingFull(true) + try { + let data: any[] = [] + if (activeTab === 'countries') { + data = await getCountries(siteId, dateRange.start, dateRange.end, 250) + } else if (activeTab === 'regions') { + data = await getRegions(siteId, dateRange.start, dateRange.end, 250) + } else if (activeTab === 'cities') { + data = await getCities(siteId, dateRange.start, dateRange.end, 250) + } + setFullData(data) + } catch (e) { + console.error(e) + } finally { + setIsLoadingFull(false) + } + } + fetchData() + } else { + setFullData([]) + } + }, [isModalOpen, activeTab, siteId, dateRange]) const getFlagComponent = (countryCode: string) => { if (!countryCode || countryCode === 'Unknown') return null @@ -238,23 +269,27 @@ export default function Locations({ countries, cities, regions, geoDataLevel = ' title={`Locations - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`} >
- {(data as any[]).map((item, index) => ( -
-
- {getFlagComponent(item.country)} - - {activeTab === 'countries' ? getCountryName(item.country) : - activeTab === 'regions' ? getRegionName(item.region, item.country) : - getCityName(item.city)} - + {isLoadingFull ? ( +
Loading...
+ ) : ( + (fullData.length > 0 ? fullData : data as any[]).map((item, index) => ( +
+
+ {getFlagComponent(item.country)} + + {activeTab === 'countries' ? getCountryName(item.country) : + activeTab === 'regions' ? getRegionName(item.region, item.country) : + getCityName(item.city)} + +
+
+ {formatNumber(item.pageviews)} +
-
- {formatNumber(item.pageviews)} -
-
- ))} + )) + )}
) -} +} \ No newline at end of file diff --git a/components/dashboard/TechSpecs.tsx b/components/dashboard/TechSpecs.tsx index dcef5a6..d3f025b 100644 --- a/components/dashboard/TechSpecs.tsx +++ b/components/dashboard/TechSpecs.tsx @@ -1,10 +1,11 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { formatNumber } from '@/lib/utils/format' import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons' import { MdMonitor } from 'react-icons/md' import { Modal } from '@ciphera-net/ui' +import { getBrowsers, getOS, getDevices, getScreenResolutions } from '@/lib/api/stats' interface TechSpecsProps { browsers: Array<{ browser: string; pageviews: number }> @@ -13,21 +14,57 @@ interface TechSpecsProps { screenResolutions: Array<{ screen_resolution: string; pageviews: number }> collectDeviceInfo?: boolean collectScreenResolution?: boolean + siteId: string + dateRange: { start: string, end: string } } type Tab = 'browsers' | 'os' | 'devices' | 'screens' const LIMIT = 7 -export default function TechSpecs({ browsers, os, devices, screenResolutions, collectDeviceInfo = true, collectScreenResolution = true }: TechSpecsProps) { +export default function TechSpecs({ browsers, os, devices, screenResolutions, collectDeviceInfo = true, collectScreenResolution = true, siteId, dateRange }: TechSpecsProps) { const [activeTab, setActiveTab] = useState('browsers') const [isModalOpen, setIsModalOpen] = useState(false) + const [fullData, setFullData] = useState([]) + const [isLoadingFull, setIsLoadingFull] = useState(false) // Filter out "Unknown" entries that result from disabled collection const filterUnknown = (items: Array<{ name: string; pageviews: number; icon: React.ReactNode }>) => { return items.filter(item => item.name && item.name !== 'Unknown' && item.name !== '') } + useEffect(() => { + if (isModalOpen) { + const fetchData = async () => { + setIsLoadingFull(true) + try { + let data: any[] = [] + if (activeTab === 'browsers') { + const res = await getBrowsers(siteId, dateRange.start, dateRange.end, 100) + data = res.map(b => ({ name: b.browser, pageviews: b.pageviews, icon: getBrowserIcon(b.browser) })) + } else if (activeTab === 'os') { + const res = await getOS(siteId, dateRange.start, dateRange.end, 100) + data = res.map(o => ({ name: o.os, pageviews: o.pageviews, icon: getOSIcon(o.os) })) + } else if (activeTab === 'devices') { + const res = await getDevices(siteId, dateRange.start, dateRange.end, 100) + data = res.map(d => ({ name: d.device, pageviews: d.pageviews, icon: getDeviceIcon(d.device) })) + } else if (activeTab === 'screens') { + const res = await getScreenResolutions(siteId, dateRange.start, dateRange.end, 100) + data = res.map(s => ({ name: s.screen_resolution, pageviews: s.pageviews, icon: })) + } + setFullData(filterUnknown(data)) + } catch (e) { + console.error(e) + } finally { + setIsLoadingFull(false) + } + } + fetchData() + } else { + setFullData([]) + } + }, [isModalOpen, activeTab, siteId, dateRange]) + const getRawData = () => { switch (activeTab) { case 'browsers': @@ -141,19 +178,23 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co title={`Technology - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`} >
- {data.map((item, index) => ( -
-
- {item.icon && {item.icon}} - {item.name === 'Unknown' ? 'Unknown' : item.name} + {isLoadingFull ? ( +
Loading...
+ ) : ( + (fullData.length > 0 ? fullData : data).map((item, index) => ( +
+
+ {item.icon && {item.icon}} + {item.name === 'Unknown' ? 'Unknown' : item.name} +
+
+ {formatNumber(item.pageviews)} +
-
- {formatNumber(item.pageviews)} -
-
- ))} + )) + )}
) -} +} \ No newline at end of file diff --git a/components/dashboard/TopReferrers.tsx b/components/dashboard/TopReferrers.tsx index 90c0e28..b5cabe5 100644 --- a/components/dashboard/TopReferrers.tsx +++ b/components/dashboard/TopReferrers.tsx @@ -1,19 +1,24 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { formatNumber } from '@/lib/utils/format' import { getReferrerIcon } from '@/lib/utils/icons' import { Modal } from '@ciphera-net/ui' +import { getTopReferrers, TopReferrer } from '@/lib/api/stats' interface TopReferrersProps { referrers: Array<{ referrer: string; pageviews: number }> collectReferrers?: boolean + siteId: string + dateRange: { start: string, end: string } } const LIMIT = 7 -export default function TopReferrers({ referrers, collectReferrers = true }: TopReferrersProps) { +export default function TopReferrers({ referrers, collectReferrers = true, siteId, dateRange }: TopReferrersProps) { const [isModalOpen, setIsModalOpen] = useState(false) + const [fullData, setFullData] = useState([]) + const [isLoadingFull, setIsLoadingFull] = useState(false) // Filter out empty/unknown referrers const filteredReferrers = (referrers || []).filter( @@ -25,6 +30,29 @@ export default function TopReferrers({ referrers, collectReferrers = true }: Top const emptySlots = Math.max(0, LIMIT - displayedReferrers.length) const showViewAll = hasData && filteredReferrers.length > LIMIT + useEffect(() => { + if (isModalOpen) { + const fetchData = async () => { + setIsLoadingFull(true) + try { + const data = await getTopReferrers(siteId, dateRange.start, dateRange.end, 100) + // Filter fetched data too + const filtered = (data || []).filter( + ref => ref.referrer && ref.referrer !== 'Unknown' && ref.referrer !== '' + ) + setFullData(filtered) + } catch (e) { + console.error(e) + } finally { + setIsLoadingFull(false) + } + } + fetchData() + } else { + setFullData([]) + } + }, [isModalOpen, siteId, dateRange]) + return ( <>
@@ -78,19 +106,23 @@ export default function TopReferrers({ referrers, collectReferrers = true }: Top title="Top Referrers" >
- {referrers?.map((ref, index) => ( -
-
- {getReferrerIcon(ref.referrer)} - {ref.referrer} + {isLoadingFull ? ( +
Loading...
+ ) : ( + (fullData.length > 0 ? fullData : filteredReferrers).map((ref, index) => ( +
+
+ {getReferrerIcon(ref.referrer)} + {ref.referrer} +
+
+ {formatNumber(ref.pageviews)} +
-
- {formatNumber(ref.pageviews)} -
-
- ))} + )) + )}
) -} +} \ No newline at end of file