'use client' import { useState, useEffect, useRef } from 'react' import dynamic from 'next/dynamic' import { motion } from 'framer-motion' import { logger } from '@/lib/utils/logger' import { formatNumber } from '@ciphera-net/ui' import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard' import * as Flags from 'country-flag-icons/react/3x2' import iso3166 from 'iso-3166-2' const DottedMap = dynamic(() => import('./DottedMap'), { ssr: false }) import Link from 'next/link' import { Modal, GlobeIcon, ArrowRightIcon } from '@ciphera-net/ui' import { ListSkeleton } from '@/components/skeletons' import VirtualList from './VirtualList' import { ShieldCheck, Detective, Broadcast, MapPin, FrameCornersIcon } from '@phosphor-icons/react' import { getCountries, getCities, getRegions } from '@/lib/api/stats' import { type DimensionFilter } from '@/lib/filters' 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 } onFilter?: (filter: DimensionFilter) => void } type Tab = 'map' | 'countries' | 'regions' | 'cities' const LIMIT = 7 const TAB_TO_DIMENSION: Record = { countries: 'country', regions: 'region', cities: 'city' } export default function Locations({ countries, cities, regions, geoDataLevel = 'full', siteId, dateRange, onFilter }: LocationProps) { const [activeTab, setActiveTab] = useState('countries') const handleTabKeyDown = useTabListKeyboard() const [isModalOpen, setIsModalOpen] = useState(false) const [modalSearch, setModalSearch] = useState('') type LocationItem = { country?: string; city?: string; region?: string; pageviews: number } const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) const containerRef = useRef(null) const [inView, setInView] = useState(false) useEffect(() => { const el = containerRef.current if (!el) return const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) setInView(true) }, { rootMargin: '200px' } ) observer.observe(el) return () => observer.disconnect() }, []) useEffect(() => { if (isModalOpen) { const fetchData = async () => { setIsLoadingFull(true) try { let data: LocationItem[] = [] 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) { logger.error(e) } finally { setIsLoadingFull(false) } } fetchData() } else { setFullData([]) } }, [isModalOpen, activeTab, siteId, dateRange]) const getFlagComponent = (countryCode: string) => { if (!countryCode || countryCode === 'Unknown') return null switch (countryCode) { case 'T1': return case 'A1': return case 'A2': return case 'O1': case 'EU': case 'AP': return } const FlagComponent = (Flags as Record>)[countryCode] return FlagComponent ? : null } const getCountryName = (code: string) => { if (!code || code === 'Unknown') return 'Unknown' switch (code) { case 'T1': return 'Tor Network' case 'A1': return 'Anonymous Proxy' case 'A2': return 'Satellite Provider' case 'O1': return 'Other' case 'EU': return 'Europe' case 'AP': return 'Asia/Pacific' } try { const regionNames = new Intl.DisplayNames(['en'], { type: 'region' }) return regionNames.of(code) || code } catch (e) { return code } } const getRegionName = (regionCode: string, countryCode: string) => { // Check for special country codes first switch (countryCode) { case 'T1': return 'Tor Network' case 'A1': return 'Anonymous Proxy' case 'A2': return 'Satellite Provider' case 'O1': return 'Other' case 'EU': return 'Europe' case 'AP': return 'Asia/Pacific' } if (!regionCode || regionCode === 'Unknown' || !countryCode || countryCode === 'Unknown') return 'Unknown' try { const countryData = iso3166.data[countryCode] if (!countryData || !countryData.sub) return regionCode // ISO 3166-2 structure keys are typically "US-OR" const fullCode = `${countryCode}-${regionCode}` const regionData = countryData.sub[fullCode] if (regionData && regionData.name) { return regionData.name } return regionCode } catch (e) { return regionCode } } const getCityName = (city: string) => { // Check for special codes that might appear in city field switch (city) { case 'T1': return 'Tor Network' case 'A1': return 'Anonymous Proxy' case 'A2': return 'Satellite Provider' case 'O1': return 'Other' } if (!city || city === 'Unknown') return 'Unknown' return city } const getData = () => { switch (activeTab) { case 'countries': return countries case 'regions': return regions case 'cities': return cities default: return [] } } // Check if the current tab's data is disabled by privacy settings const isTabDisabled = () => { if (geoDataLevel === 'none') return true if (geoDataLevel === 'country' && (activeTab === 'regions' || activeTab === 'cities')) return true return false } // Filter out "Unknown" entries that result from disabled collection const filterUnknown = (data: LocationItem[]) => { return data.filter(item => { if (activeTab === 'countries') return item.country && item.country !== 'Unknown' && item.country !== '' if (activeTab === 'regions') return item.region && item.region !== 'Unknown' && item.region !== '' if (activeTab === 'cities') return item.city && item.city !== 'Unknown' && item.city !== '' return true }) } const isVisualTab = activeTab === 'map' const rawData = isVisualTab ? [] : getData() const data = filterUnknown(rawData) const totalPageviews = data.reduce((sum, item) => sum + item.pageviews, 0) const hasData = isVisualTab ? (countries && filterUnknown(countries).length > 0) : (data && data.length > 0) const displayedData = (!isVisualTab && hasData) ? data.slice(0, LIMIT) : [] const emptySlots = Math.max(0, LIMIT - displayedData.length) const showViewAll = !isVisualTab && hasData && data.length > LIMIT const getDisabledMessage = () => { if (geoDataLevel === 'none') { return 'Geographic data collection is disabled in site settings' } if (geoDataLevel === 'country' && (activeTab === 'regions' || activeTab === 'cities')) { return `${activeTab === 'regions' ? 'Region' : 'City'} tracking is disabled. Only country-level data is collected.` } return 'No data available' } return ( <>

Locations

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

{getDisabledMessage()}

) : isVisualTab ? ( hasData ? ( inView ? : null ) : (

No location data yet

Visitor locations will appear here based on anonymous geographic data.

Install tracking script
) ) : ( hasData ? ( <> {displayedData.map((item) => { const dim = TAB_TO_DIMENSION[activeTab] const filterValue = activeTab === 'countries' ? item.country : activeTab === 'regions' ? item.region : item.city const canFilter = onFilter && dim && filterValue const maxPv = displayedData[0]?.pageviews ?? 0 const barWidth = maxPv > 0 ? (item.pageviews / maxPv) * 75 : 0 return (
canFilter && onFilter({ dimension: dim, operator: 'is', values: [filterValue!] })} className={`relative flex items-center justify-between h-9 group hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors${canFilter ? ' cursor-pointer' : ''}`} >
{getFlagComponent(item.country ?? '')} {activeTab === 'countries' ? getCountryName(item.country ?? '') : activeTab === 'regions' ? getRegionName(item.region ?? '', item.country ?? '') : getCityName(item.city ?? '')}
{totalPageviews > 0 ? `${Math.round((item.pageviews / totalPageviews) * 100)}%` : ''} {formatNumber(item.pageviews)}
) })} {Array.from({ length: emptySlots }).map((_, i) => (
{ setIsModalOpen(false); setModalSearch('') }} title={activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} className="max-w-2xl" >
setModalSearch(e.target.value)} placeholder="Search locations..." className="w-full px-3 py-2 mb-3 text-sm bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50" />
{isLoadingFull ? (
) : (() => { const rawModalData = fullData.length > 0 ? fullData : data const search = modalSearch.toLowerCase() const modalData = !modalSearch ? rawModalData : rawModalData.filter(item => { const label = activeTab === 'countries' ? getCountryName(item.country ?? '') : activeTab === 'regions' ? getRegionName(item.region ?? '', item.country ?? '') : getCityName(item.city ?? '') return label.toLowerCase().includes(search) }) const modalTotal = modalData.reduce((sum, item) => sum + item.pageviews, 0) return ( { const dim = TAB_TO_DIMENSION[activeTab] const filterValue = activeTab === 'countries' ? item.country : activeTab === 'regions' ? item.region : item.city const canFilter = onFilter && dim && filterValue return (
{ if (canFilter) { onFilter({ dimension: dim, operator: 'is', values: [filterValue!] }); setIsModalOpen(false) } }} className={`flex items-center justify-between h-9 group hover:bg-neutral-800 rounded-lg px-2 transition-colors${canFilter ? ' cursor-pointer' : ''}`} >
{getFlagComponent(item.country ?? '')} {activeTab === 'countries' ? getCountryName(item.country ?? '') : activeTab === 'regions' ? getRegionName(item.region ?? '', item.country ?? '') : getCityName(item.city ?? '')}
{modalTotal > 0 ? `${Math.round((item.pageviews / modalTotal) * 100)}%` : ''} {formatNumber(item.pageviews)}
) }} /> ) })()}
) }