diff --git a/components/dashboard/ContentStats.tsx b/components/dashboard/ContentStats.tsx index 952b2f6..f1119f7 100644 --- a/components/dashboard/ContentStats.tsx +++ b/components/dashboard/ContentStats.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { formatNumber } from '@/lib/utils/format' import { TopPage } from '@/lib/api/stats' +import { Modal } from '@ciphera-net/ui' interface ContentStatsProps { topPages: TopPage[] @@ -12,80 +13,116 @@ interface ContentStatsProps { type Tab = 'top_pages' | 'entry_pages' | 'exit_pages' +const LIMIT = 7 + export default function ContentStats({ topPages, entryPages, exitPages }: ContentStatsProps) { const [activeTab, setActiveTab] = useState('top_pages') + const [isModalOpen, setIsModalOpen] = useState(false) - 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 + const getData = () => { + switch (activeTab) { + case 'top_pages': + return topPages + case 'entry_pages': + return entryPages + case 'exit_pages': + return exitPages + default: + return [] } + } - if (!data || data.length === 0) { - return

No data available

+ const data = getData() + const hasData = data && data.length > 0 + const displayedData = hasData ? data.slice(0, LIMIT) : [] + const emptySlots = Math.max(0, LIMIT - displayedData.length) + const showViewAll = hasData && data.length > LIMIT + + const getTabLabel = (tab: Tab) => { + switch (tab) { + case 'top_pages': return 'Top Pages' + case 'entry_pages': return 'Entry' + case 'exit_pages': return 'Exit' } - - return ( -
- {data.map((page, index) => ( -
-
- {page.path} -
-
- {formatNumber(page.pageviews)} -
-
- ))} -
- ) } return ( -
-
-

- Content -

-
- - - + <> +
+
+
+

+ Content +

+ {showViewAll && ( + + )} +
+
+ {(['top_pages', 'entry_pages', 'exit_pages'] as Tab[]).map((tab) => ( + + ))} +
+
+ +
+ {hasData ? ( + <> + {displayedData.map((page, index) => ( +
+
+ {page.path} +
+
+ {formatNumber(page.pageviews)} +
+
+ ))} + {Array.from({ length: emptySlots }).map((_, i) => ( +
- {renderContent()} -
+ + setIsModalOpen(false)} + title={`Content - ${getTabLabel(activeTab)}`} + > +
+ {data.map((page, index) => ( +
+
+ {page.path} +
+
+ {formatNumber(page.pageviews)} +
+
+ ))} +
+
+ ) } diff --git a/components/dashboard/Locations.tsx b/components/dashboard/Locations.tsx index 6f57197..342ea14 100644 --- a/components/dashboard/Locations.tsx +++ b/components/dashboard/Locations.tsx @@ -4,6 +4,7 @@ import { useState } from 'react' import { formatNumber } from '@/lib/utils/format' import * as Flags from 'country-flag-icons/react/3x2' import WorldMap from './WorldMap' +import { Modal } from '@ciphera-net/ui' interface LocationProps { countries: Array<{ country: string; pageviews: number }> @@ -13,13 +14,14 @@ interface LocationProps { type Tab = 'map' | 'countries' | 'regions' | 'cities' +const LIMIT = 7 + export default function Locations({ countries, cities, regions }: LocationProps) { const [activeTab, setActiveTab] = useState('map') + const [isModalOpen, setIsModalOpen] = useState(false) const getFlagComponent = (countryCode: string) => { if (!countryCode || countryCode === 'Unknown') return null - // * The API returns 2-letter country codes (e.g. US, DE) - // * We cast it to the flag component name const FlagComponent = (Flags as any)[countryCode] return FlagComponent ? : null } @@ -34,128 +36,118 @@ export default function Locations({ countries, cities, regions }: LocationProps) } } - const renderContent = () => { - if (activeTab === 'map') { - if (!countries || countries.length === 0) { - return

No data available

- } - return - } - - if (activeTab === 'countries') { - if (!countries || countries.length === 0) { - return

No data available

- } - return ( -
- {countries.map((country, index) => ( -
-
- {getFlagComponent(country.country)} - {getCountryName(country.country)} -
-
- {formatNumber(country.pageviews)} -
-
- ))} -
- ) - } - - 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

- } - return ( -
- {cities.map((city, index) => ( -
-
- {getFlagComponent(city.country)} - {city.city === 'Unknown' ? 'Unknown' : city.city} -
-
- {formatNumber(city.pageviews)} -
-
- ))} -
- ) + const getData = () => { + switch (activeTab) { + case 'countries': return countries + case 'regions': return regions + case 'cities': return cities + default: return [] } } + const data = activeTab === 'map' ? [] : getData() + const hasData = activeTab === 'map' ? (countries && countries.length > 0) : (data && data.length > 0) + const displayedData = (activeTab !== 'map' && hasData) ? (data as any[]).slice(0, LIMIT) : [] + const emptySlots = Math.max(0, LIMIT - displayedData.length) + const showViewAll = activeTab !== 'map' && hasData && data.length > LIMIT + return ( -
-
-

- Locations -

-
- - - - + <> +
+
+
+

+ Locations +

+ {showViewAll && ( + + )} +
+
+ {(['map', 'countries', 'regions', 'cities'] as Tab[]).map((tab) => ( + + ))} +
+
+ +
+ {activeTab === 'map' ? ( + hasData ? : ( +
+

No data available

+
+ ) + ) : ( + hasData ? ( + <> + {displayedData.map((item, index) => ( +
+
+ {activeTab === 'countries' && {getFlagComponent(item.country)}} + {activeTab !== 'countries' && {getFlagComponent(item.country)}} + + + {activeTab === 'countries' ? getCountryName(item.country) : + activeTab === 'regions' ? (item.region === 'Unknown' ? 'Unknown' : item.region) : + (item.city === 'Unknown' ? 'Unknown' : item.city)} + +
+
+ {formatNumber(item.pageviews)} +
+
+ ))} + {Array.from({ length: emptySlots }).map((_, i) => ( +
- {renderContent()} -
+ + setIsModalOpen(false)} + 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' ? (item.region === 'Unknown' ? 'Unknown' : item.region) : + (item.city === 'Unknown' ? 'Unknown' : item.city)} + +
+
+ {formatNumber(item.pageviews)} +
+
+ ))} +
+
+ ) } diff --git a/components/dashboard/TechSpecs.tsx b/components/dashboard/TechSpecs.tsx index d33f1f8..14fa5ef 100644 --- a/components/dashboard/TechSpecs.tsx +++ b/components/dashboard/TechSpecs.tsx @@ -4,6 +4,7 @@ import { useState } 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' interface TechSpecsProps { browsers: Array<{ browser: string; pageviews: number }> @@ -14,93 +15,112 @@ interface TechSpecsProps { type Tab = 'browsers' | 'os' | 'devices' | 'screens' +const LIMIT = 7 + export default function TechSpecs({ browsers, os, devices, screenResolutions }: TechSpecsProps) { const [activeTab, setActiveTab] = useState('browsers') + const [isModalOpen, setIsModalOpen] = useState(false) - const renderContent = () => { - let data: Array<{ name: string; pageviews: number; icon?: React.ReactNode }> = [] - - if (activeTab === 'browsers') { - data = browsers.map(b => ({ name: b.browser, pageviews: b.pageviews, icon: getBrowserIcon(b.browser) })) - } else if (activeTab === 'os') { - data = os.map(o => ({ name: o.os, pageviews: o.pageviews, icon: getOSIcon(o.os) })) - } else if (activeTab === 'devices') { - data = devices.map(d => ({ name: d.device, pageviews: d.pageviews, icon: getDeviceIcon(d.device) })) - } else if (activeTab === 'screens') { - data = screenResolutions.map(s => ({ name: s.screen_resolution, pageviews: s.pageviews, icon: })) + const getData = () => { + switch (activeTab) { + case 'browsers': + return browsers.map(b => ({ name: b.browser, pageviews: b.pageviews, icon: getBrowserIcon(b.browser) })) + case 'os': + return os.map(o => ({ name: o.os, pageviews: o.pageviews, icon: getOSIcon(o.os) })) + case 'devices': + return devices.map(d => ({ name: d.device, pageviews: d.pageviews, icon: getDeviceIcon(d.device) })) + case 'screens': + return screenResolutions.map(s => ({ name: s.screen_resolution, pageviews: s.pageviews, icon: })) + default: + return [] } - - if (!data || data.length === 0) { - return

No data available

- } - - return ( -
- {data.map((item, index) => ( -
-
- {item.icon && {item.icon}} - {item.name === 'Unknown' ? 'Unknown' : item.name} -
-
- {formatNumber(item.pageviews)} -
-
- ))} -
- ) } + const data = getData() + const hasData = data && data.length > 0 + const displayedData = hasData ? data.slice(0, LIMIT) : [] + const emptySlots = Math.max(0, LIMIT - displayedData.length) + const showViewAll = hasData && data.length > LIMIT + return ( -
-
-

- Technology -

-
- - - - + <> +
+
+
+

+ Technology +

+ {showViewAll && ( + + )} +
+
+ {(['browsers', 'os', 'devices', 'screens'] as Tab[]).map((tab) => ( + + ))} +
+
+ +
+ {hasData ? ( + <> + {displayedData.map((item, index) => ( +
+
+ {item.icon && {item.icon}} + {item.name === 'Unknown' ? 'Unknown' : item.name} +
+
+ {formatNumber(item.pageviews)} +
+
+ ))} + {Array.from({ length: emptySlots }).map((_, i) => ( +
- {renderContent()} -
+ + setIsModalOpen(false)} + title={`Technology - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`} + > +
+ {data.map((item, index) => ( +
+
+ {item.icon && {item.icon}} + {item.name === 'Unknown' ? 'Unknown' : item.name} +
+
+ {formatNumber(item.pageviews)} +
+
+ ))} +
+
+ ) } diff --git a/components/dashboard/TopReferrers.tsx b/components/dashboard/TopReferrers.tsx index e533c08..753f7b5 100644 --- a/components/dashboard/TopReferrers.tsx +++ b/components/dashboard/TopReferrers.tsx @@ -1,42 +1,86 @@ 'use client' +import { useState } from 'react' import { formatNumber } from '@/lib/utils/format' import { getReferrerIcon } from '@/lib/utils/icons' +import { Modal } from '@ciphera-net/ui' interface TopReferrersProps { referrers: Array<{ referrer: string; pageviews: number }> } +const LIMIT = 7 + export default function TopReferrers({ referrers }: TopReferrersProps) { - if (!referrers || referrers.length === 0) { - return ( -
-

- Top Referrers -

-

No data available

-
- ) - } + const [isModalOpen, setIsModalOpen] = useState(false) + + const hasData = referrers && referrers.length > 0 + const displayedReferrers = hasData ? referrers.slice(0, LIMIT) : [] + const emptySlots = Math.max(0, LIMIT - displayedReferrers.length) + const showViewAll = hasData && referrers.length > LIMIT return ( -
-

- Top Referrers -

-
- {referrers.map((ref, index) => ( -
-
- {getReferrerIcon(ref.referrer)} - {ref.referrer} + <> +
+
+

+ Top Referrers +

+ {showViewAll && ( + + )} +
+ +
+ {hasData ? ( + <> + {displayedReferrers.map((ref, index) => ( +
+
+ {getReferrerIcon(ref.referrer)} + {ref.referrer} +
+
+ {formatNumber(ref.pageviews)} +
+
+ ))} + {Array.from({ length: emptySlots }).map((_, i) => ( + - ))} + )} +
-
+ + setIsModalOpen(false)} + title="Top Referrers" + > +
+ {referrers?.map((ref, index) => ( +
+
+ {getReferrerIcon(ref.referrer)} + {ref.referrer} +
+
+ {formatNumber(ref.pageviews)} +
+
+ ))} +
+
+ ) }