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:
Shivam Patel
2026-02-09 02:39:30 -05:00
parent 8e18bf64b2
commit 9d8d34abb3
2 changed files with 150 additions and 8 deletions

View File

@@ -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,