"use client"; import { useState, useEffect } from 'react'; import { GeoData } from '@/app/api/types'; import { getLocationsFromIPs } from '@/app/utils/ipLocation'; interface GeoAnalyticsProps { data: GeoData[]; } // Interface for IP location data in our cache interface IpLocationDetail { country: string; city: string; region: string; continent: string; } // Cache for IP location data interface LocationCache { [key: string]: IpLocationDetail; } export default function GeoAnalytics({ data }: GeoAnalyticsProps) { const [viewMode, setViewMode] = useState<'country' | 'city' | 'region' | 'continent'>('country'); const [locationCache, setLocationCache] = useState({}); const [isLoading, setIsLoading] = useState(false); // Track IPs that failed to resolve const [failedIPs, setFailedIPs] = useState>(new Set()); // 安全地格式化数字 const formatNumber = (value: number | undefined | null): string => { if (value === undefined || value === null) return '0'; return value.toLocaleString(); }; // 安全地格式化百分比 const formatPercent = (value: number | undefined | null): string => { if (value === undefined || value === null) return '0'; return value.toFixed(1); }; const sortedData = [...data].sort((a, b) => (b.visits || 0) - (a.visits || 0)); // Handle tab selection - only change local view mode const handleViewModeChange = (mode: 'country' | 'city' | 'region' | 'continent') => { setViewMode(mode); }; // Load location data for all IPs when the data changes useEffect(() => { const fetchLocations = async () => { if (sortedData.length === 0) return; setIsLoading(true); const tempCache: LocationCache = {...locationCache}; const tempFailedIPs = new Set(failedIPs); // Get all unique IPs that aren't already in the cache and haven't failed const uniqueIPs = [...new Set(sortedData.map(item => item.location))].filter(ip => ip && ip !== 'Unknown' && !tempCache[ip] && !tempFailedIPs.has(ip) ); if (uniqueIPs.length === 0) { setIsLoading(false); return; } try { // Use batch lookup for better performance const batchResults = await getLocationsFromIPs(uniqueIPs); // Convert results to our cache format for (const [ip, data] of Object.entries(batchResults)) { if (data) { tempCache[ip] = { country: data.country_name, city: data.city, region: data.region, continent: data.continent_name }; } else { // Mark as failed tempFailedIPs.add(ip); } } setLocationCache(tempCache); setFailedIPs(tempFailedIPs); } catch (error) { console.error('Error fetching location data:', error); } finally { setIsLoading(false); } }; fetchLocations(); }, [data]); // Get the appropriate location value based on the current view mode const getLocationValue = (item: GeoData): string => { const ip = item.location || ''; // If there's no IP or it's "Unknown", return that value if (!ip || ip === 'Unknown') return 'Unknown'; // If IP failed to resolve, return Unknown if (failedIPs.has(ip)) { return 'Unknown'; } // Return from cache if available if (locationCache[ip]) { switch (viewMode) { case 'country': return locationCache[ip].country || 'Unknown'; case 'city': return locationCache[ip].city || 'Unknown'; case 'region': return locationCache[ip].region || 'Unknown'; case 'continent': return locationCache[ip].continent || 'Unknown'; default: return ip; } } // Return placeholder if not in cache yet return `Loading...`; }; // Get the appropriate area value based on the current view mode const getAreaValue = (item: GeoData): string => { const ip = item.location || ''; // If there's no IP or it's "Unknown", return empty string if (!ip || ip === 'Unknown' || failedIPs.has(ip)) return ''; // Return from cache if available if (locationCache[ip]) { switch (viewMode) { case 'country': // For country view, show the continent as area return locationCache[ip].continent || ''; case 'city': // For city view, show the country and region return `${locationCache[ip].country}, ${locationCache[ip].region}`; case 'region': // For region view, show the country return locationCache[ip].country || ''; case 'continent': // For continent view, no additional area needed return ''; default: return ''; } } // Return empty if not in cache yet return ''; }; return (
{/* Tabs for geographic levels */}
{/* Loading indicator */} {isLoading && (
Loading location data...
)} {/* Table with added area column */}
{sortedData.length > 0 ? ( sortedData.map((item, index) => ( )) ) : ( )}
{viewMode === 'country' ? 'Country' : viewMode === 'city' ? 'City' : viewMode === 'region' ? 'Region' : 'Continent'} {viewMode === 'country' ? 'Continent' : viewMode === 'city' ? 'Location' : viewMode === 'region' ? 'Country' : 'Area'} Visits Unique Visitors % of Total
{getLocationValue(item)} {item.location && (
{item.location}
)}
{getAreaValue(item)} {formatNumber(item.visits)} {formatNumber(item.visitors)}
{formatPercent(item.percentage)}%
No location data available
); }