Add globe drag interaction and visitor country flag banner
Globe can now be dragged left/right to rotate (mouse + touch), with auto-rotation resuming from the new position on release. Visitor API aggregates hits by country/region/city, displayed as a scrolling flag emoji banner at the bottom of the globe card with expandable region breakdowns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,12 +29,14 @@ export async function GET() {
|
||||
`SELECT ip_address, COUNT(*) as hits FROM visits WHERE visited_at >= datetime('now', '-7 days') GROUP BY ip_address`
|
||||
);
|
||||
|
||||
// Geo-locate IPs and group by location
|
||||
// Geo-locate IPs and group by location + country
|
||||
const locationMap = new Map<string, { lat: number; lon: number; city: string; country: string; count: number }>();
|
||||
const countryMap = new Map<string, { count: number; regions: Map<string, { count: number; cities: Map<string, number> }> }>();
|
||||
|
||||
for (const row of recentIps) {
|
||||
const geo = geoip.lookup(row.ip_address);
|
||||
if (geo && geo.ll) {
|
||||
// Location grouping for globe markers
|
||||
const key = `${geo.ll[0]},${geo.ll[1]}`;
|
||||
const existing = locationMap.get(key);
|
||||
if (existing) {
|
||||
@@ -48,6 +50,27 @@ export async function GET() {
|
||||
count: row.hits,
|
||||
});
|
||||
}
|
||||
|
||||
// Country aggregation
|
||||
const countryCode = geo.country || 'XX';
|
||||
const regionCode = geo.region || 'Unknown';
|
||||
const cityName = geo.city || 'Unknown';
|
||||
|
||||
let country = countryMap.get(countryCode);
|
||||
if (!country) {
|
||||
country = { count: 0, regions: new Map() };
|
||||
countryMap.set(countryCode, country);
|
||||
}
|
||||
country.count += row.hits;
|
||||
|
||||
let region = country.regions.get(regionCode);
|
||||
if (!region) {
|
||||
region = { count: 0, cities: new Map() };
|
||||
country.regions.set(regionCode, region);
|
||||
}
|
||||
region.count += row.hits;
|
||||
|
||||
region.cities.set(cityName, (region.cities.get(cityName) || 0) + row.hits);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +81,25 @@ export async function GET() {
|
||||
country: loc.country,
|
||||
}));
|
||||
|
||||
const countries = Array.from(countryMap.entries())
|
||||
.map(([code, data]) => ({
|
||||
code,
|
||||
count: data.count,
|
||||
regions: Array.from(data.regions.entries())
|
||||
.map(([region, rData]) => ({
|
||||
region,
|
||||
count: rData.count,
|
||||
cities: Array.from(rData.cities.entries())
|
||||
.map(([city, count]) => ({ city, count }))
|
||||
.sort((a, b) => b.count - a.count),
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count),
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
return NextResponse.json({
|
||||
locations,
|
||||
countries,
|
||||
totalVisitors: total?.count || 0,
|
||||
active24h: last24h?.count || 0,
|
||||
active7d: last7d?.count || 0,
|
||||
|
||||
Reference in New Issue
Block a user