'use client' import { useEffect, useState } from 'react' import dynamic from 'next/dynamic' import { useParams } from 'next/navigation' import Link from 'next/link' import * as Flags from 'country-flag-icons/react/3x2' const DottedMap = dynamic(() => import('@/components/dashboard/DottedMap'), { ssr: false }) import { getDateRange, formatDate, Select } from '@ciphera-net/ui' import { ArrowSquareOut, CloudArrowUp } from '@phosphor-icons/react' import { ResponsiveContainer, AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, } from 'recharts' import { useDashboard, useBunnyStatus, useBunnyOverview, useBunnyDailyStats, useBunnyTopCountries } from '@/lib/swr/dashboard' import { SkeletonLine, StatCardSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons' // ─── Helpers ──────────────────────────────────────────────────── // US state codes → map to "US" for the dotted map const US_STATES = new Set([ 'AL','AK','AZ','AR','CO','CT','DC','DE','FL','GA','HI','ID','IL','IN','IA', 'KS','KY','LA','ME','MD','MA','MI','MN','MS','MO','MT','NE','NV','NH','NJ', 'NM','NY','NC','ND','OH','OK','OR','PA','RI','SC','SD','TN','TX','UT','VT', 'VA','WA','WV','WI','WY', ]) // Canadian province codes → map to "CA" const CA_PROVINCES = new Set(['AB','BC','MB','NB','NL','NS','NT','NU','ON','PE','QC','SK','YT']) /** * Extract ISO country code from BunnyCDN datacenter string. * e.g. "EU: Zurich, CH" → "CH", "NA: Chicago, IL" → "US", "NA: Toronto, CA" → "CA" */ function extractCountryCode(datacenter: string): string { const parts = datacenter.split(', ') const code = parts[parts.length - 1]?.trim().toUpperCase() if (!code || code.length !== 2) return '' if (US_STATES.has(code)) return 'US' if (CA_PROVINCES.has(code)) return 'CA' return code } /** * Extract the city name from a BunnyCDN datacenter string. * e.g. "EU: Zurich, CH" → "Zurich" */ function extractCity(datacenter: string): string { const afterColon = datacenter.split(': ')[1] || datacenter return afterColon.split(',')[0]?.trim() || datacenter } /** Get flag icon component for a country code */ function getFlagIcon(code: string) { if (!code) return null const FlagComponent = (Flags as Record>)[code] return FlagComponent ? : null } /** * Map each datacenter entry to its country's centroid for the dotted map. * Each datacenter gets its own dot (sized by bandwidth) at the country's position. */ function mapToCountryCentroids(data: Array<{ country_code: string; bandwidth: number }>): Array<{ country: string; pageviews: number }> { return data .map((row) => ({ country: extractCountryCode(row.country_code), pageviews: row.bandwidth, })) .filter((d) => d.country !== '') } function formatBytes(bytes: number): string { if (bytes === 0) return '0 B' const units = ['B', 'KB', 'MB', 'GB', 'TB'] const i = Math.floor(Math.log(bytes) / Math.log(1024)) const value = bytes / Math.pow(1024, i) return value.toFixed(i === 0 ? 0 : 1) + ' ' + units[i] } function formatNumber(n: number): string { if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M' if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K' return n.toLocaleString() } function formatDateShort(date: string): string { const d = new Date(date + 'T00:00:00') return d.getDate() + ' ' + d.toLocaleString('en-US', { month: 'short' }) } function changePercent( current: number, prev: number ): { value: number; positive: boolean } | null { if (prev === 0) return null const pct = ((current - prev) / prev) * 100 return { value: pct, positive: pct >= 0 } } // ─── Page ─────────────────────────────────────────────────────── export default function CDNPage() { const params = useParams() const siteId = params.id as string // Date range const [period, setPeriod] = useState('7') const [dateRange, setDateRange] = useState(() => getDateRange(7)) // Data fetching const { data: bunnyStatus } = useBunnyStatus(siteId) const { data: dashboard } = useDashboard(siteId, dateRange.start, dateRange.end) const { data: overview } = useBunnyOverview(siteId, dateRange.start, dateRange.end) const { data: dailyStats } = useBunnyDailyStats(siteId, dateRange.start, dateRange.end) const { data: topCountries } = useBunnyTopCountries(siteId, dateRange.start, dateRange.end) const showSkeleton = useMinimumLoading(!bunnyStatus) const fadeClass = useSkeletonFade(showSkeleton) // Document title useEffect(() => { const domain = dashboard?.site?.domain document.title = domain ? `CDN \u00b7 ${domain} | Pulse` : 'CDN | Pulse' }, [dashboard?.site?.domain]) // ─── Loading skeleton ───────────────────────────────────── if (showSkeleton) { return (
) } // ─── Not connected state ────────────────────────────────── if (bunnyStatus && !bunnyStatus.connected) { return (

Connect BunnyCDN

Monitor your CDN performance including bandwidth usage, cache hit rates, request volumes, and geographic distribution.

Connect in Settings
) } // ─── Connected — main view ──────────────────────────────── const bandwidthChange = overview ? changePercent(overview.total_bandwidth, overview.prev_total_bandwidth) : null const requestsChange = overview ? changePercent(overview.total_requests, overview.prev_total_requests) : null const cacheHitChange = overview ? changePercent(overview.cache_hit_rate, overview.prev_cache_hit_rate) : null const originChange = overview ? changePercent(overview.avg_origin_response, overview.prev_avg_origin_response) : null const errorsChange = overview ? changePercent(overview.total_errors, overview.prev_total_errors) : null const daily = dailyStats?.daily_stats ?? [] const countries = topCountries?.countries ?? [] const totalBandwidth = countries.reduce((sum, row) => sum + row.bandwidth, 0) return (
{/* Header */}

CDN Analytics

BunnyCDN performance, bandwidth, and cache metrics