diff --git a/src/components/AnalyticsModal.tsx b/src/components/AnalyticsModal.tsx new file mode 100644 index 0000000..650f4aa --- /dev/null +++ b/src/components/AnalyticsModal.tsx @@ -0,0 +1,203 @@ +"use client"; + +import { useEffect } from "react"; +import { createPortal } from "react-dom"; + +/** Analytics metrics payload returned by the custom backend API */ +export interface AnalyticsData { + totalViews: number; + todayViews: number; + uniqueVisitors: number; + lastPing?: number; + latencyMs?: number; + devices: { + Desktop: number; + Mobile: number; + }; +} + +interface AnalyticsModalProps { + isOpen: boolean; + onClose: () => void; + stats: AnalyticsData; +} + +/** Formats unix timestamps into relative time strings (e.g., "5s ago", "2m ago") */ +function getTimeAgo(timestamp?: number): string { + if (!timestamp) return "just now"; + const seconds = Math.floor((Date.now() - timestamp * 1000) / 1000); + if (seconds < 10) return "just now"; + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + return `${hours}h ago`; +} + +/** Terminal-style modal inspector for viewing live traffic and system health metrics */ +export default function AnalyticsModal({ + isOpen, + onClose, + stats, +}: AnalyticsModalProps) { + // Global hotkeys to dismiss the modal (ESC) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + if (isOpen) window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + // Calculate device traffic ratio percentages + const desktopCount = stats.devices?.Desktop || 0; + const mobileCount = stats.devices?.Mobile || 0; + const totalDeviceCount = desktopCount + mobileCount || 1; + const desktopPercent = Math.round((desktopCount / totalDeviceCount) * 100); + + // Render modal into document.body via portal to escape parent z-index constraints + return createPortal( +