fix: use flag icons, show per-datacenter dots on map, format tooltip as bytes
This commit is contained in:
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import * as Flags from 'country-flag-icons/react/3x2'
|
||||||
|
|
||||||
const DottedMap = dynamic(() => import('@/components/dashboard/DottedMap'), { ssr: false })
|
const DottedMap = dynamic(() => import('@/components/dashboard/DottedMap'), { ssr: false })
|
||||||
import { getDateRange, formatDate, Select } from '@ciphera-net/ui'
|
import { getDateRange, formatDate, Select } from '@ciphera-net/ui'
|
||||||
@@ -56,29 +57,24 @@ function extractCity(datacenter: string): string {
|
|||||||
return afterColon.split(',')[0]?.trim() || datacenter
|
return afterColon.split(',')[0]?.trim() || datacenter
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert ISO country code to flag emoji */
|
/** Get flag icon component for a country code */
|
||||||
function countryFlag(code: string): string {
|
function getFlagIcon(code: string) {
|
||||||
try {
|
if (!code) return null
|
||||||
return code
|
const FlagComponent = (Flags as Record<string, React.ComponentType<{ className?: string }>>)[code]
|
||||||
.toUpperCase()
|
return FlagComponent ? <FlagComponent className="w-5 h-3.5 rounded-sm shadow-sm shrink-0" /> : null
|
||||||
.split('')
|
|
||||||
.map(c => String.fromCodePoint(0x1F1E6 + c.charCodeAt(0) - 65))
|
|
||||||
.join('')
|
|
||||||
} catch {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Aggregate bandwidth by ISO country code for the map */
|
/**
|
||||||
function aggregateByCountry(data: Array<{ country_code: string; bandwidth: number }>): Array<{ country: string; pageviews: number }> {
|
* Map each datacenter entry to its country's centroid for the dotted map.
|
||||||
const byCountry = new Map<string, number>()
|
* Each datacenter gets its own dot (sized by bandwidth) at the country's position.
|
||||||
for (const row of data) {
|
*/
|
||||||
const cc = extractCountryCode(row.country_code)
|
function mapToCountryCentroids(data: Array<{ country_code: string; bandwidth: number }>): Array<{ country: string; pageviews: number }> {
|
||||||
if (cc) {
|
return data
|
||||||
byCountry.set(cc, (byCountry.get(cc) || 0) + row.bandwidth)
|
.map((row) => ({
|
||||||
}
|
country: extractCountryCode(row.country_code),
|
||||||
}
|
pageviews: row.bandwidth,
|
||||||
return Array.from(byCountry, ([country, pageviews]) => ({ country, pageviews }))
|
}))
|
||||||
|
.filter((d) => d.country !== '')
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
@@ -472,7 +468,7 @@ export default function CDNPage() {
|
|||||||
{countries.length > 0 ? (
|
{countries.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="h-[360px] mb-8">
|
<div className="h-[360px] mb-8">
|
||||||
<DottedMap data={aggregateByCountry(countries)} />
|
<DottedMap data={mapToCountryCentroids(countries)} formatValue={formatBytes} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-x-6 gap-y-5">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-x-6 gap-y-5">
|
||||||
{countries.map((row) => {
|
{countries.map((row) => {
|
||||||
@@ -482,7 +478,7 @@ export default function CDNPage() {
|
|||||||
return (
|
return (
|
||||||
<div key={row.country_code} className="group relative">
|
<div key={row.country_code} className="group relative">
|
||||||
<div className="flex items-center gap-2.5 mb-2">
|
<div className="flex items-center gap-2.5 mb-2">
|
||||||
{cc && <span className="text-base leading-none">{countryFlag(cc)}</span>}
|
{cc && getFlagIcon(cc)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<span className="text-sm font-medium text-neutral-900 dark:text-white truncate block">{city}</span>
|
<span className="text-sm font-medium text-neutral-900 dark:text-white truncate block">{city}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ const BASE_DOTS_PATH = (() => {
|
|||||||
interface DottedMapProps {
|
interface DottedMapProps {
|
||||||
data: Array<{ country: string; pageviews: number }>
|
data: Array<{ country: string; pageviews: number }>
|
||||||
className?: string
|
className?: string
|
||||||
|
/** Custom formatter for tooltip values. Defaults to formatNumber. */
|
||||||
|
formatValue?: (value: number) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCountryName(code: string): string {
|
function getCountryName(code: string): string {
|
||||||
@@ -68,7 +70,7 @@ function getCountryName(code: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DottedMap({ data, className }: DottedMapProps) {
|
export default function DottedMap({ data, className, formatValue = formatNumber }: DottedMapProps) {
|
||||||
const [tooltip, setTooltip] = useState<{ x: number; y: number; country: string; pageviews: number } | null>(null)
|
const [tooltip, setTooltip] = useState<{ x: number; y: number; country: string; pageviews: number } | null>(null)
|
||||||
|
|
||||||
const markerData = useMemo(() => {
|
const markerData = useMemo(() => {
|
||||||
@@ -152,7 +154,7 @@ export default function DottedMap({ data, className }: DottedMapProps) {
|
|||||||
style={{ left: tooltip.x, top: tooltip.y }}
|
style={{ left: tooltip.x, top: tooltip.y }}
|
||||||
>
|
>
|
||||||
<span>{getCountryName(tooltip.country)}</span>
|
<span>{getCountryName(tooltip.country)}</span>
|
||||||
<span className="ml-1.5 text-brand-orange font-bold">{formatNumber(tooltip.pageviews)}</span>
|
<span className="ml-1.5 text-brand-orange font-bold">{formatValue(tooltip.pageviews)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user