From fe5eaac724a96b346c1e17a0040268ad8245c7cf Mon Sep 17 00:00:00 2001 From: Chneemann Date: Mon, 27 Jul 2026 21:48:45 +0200 Subject: [PATCH] feat: implement terminal-themed modal for detailed metrics inspection --- src/components/AnalyticsModal.tsx | 203 +++++++++++++++++++++++++++++ src/components/AnalyticsWidget.tsx | 72 ++++++---- 2 files changed, 248 insertions(+), 27 deletions(-) create mode 100644 src/components/AnalyticsModal.tsx 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( +
+ {/* Terminal Card Container */} +
e.stopPropagation()} + > + {/* Glow Effects */} +
+
+ + {/* Top Window Bar */} +
+
+ {/* Window Controls */} +
+
+ + {/* Simulated process header */} + + ~/ + analytics.sh + + PID: {Math.floor(Math.random() * 8000 + 1000)} + + +
+ + +
+ + {/* System Command Header */} +
+
+ $ + sys.analytics --inspect +
+
+ + STATUS: ONLINE + + + LATENCY:{" "} + + {stats.latencyMs ?? "<1"}ms + + +
+
+ + {/* Primary Traffic Metrics Grid */} +
+
+
+ views_today + #1 +
+
+ {stats.todayViews} +
+
+ +
+
+ unique_visitors + #2 +
+
+ {stats.uniqueVisitors} +
+
+
+ + {/* Total Hits & Device Breakdown */} +
+
+ total_hits: + + {stats.totalViews} + +
+ + {/* Device ratio progress bar */} +
+
+ device_ratio: + + 💻 {desktopCount} / 📱{" "} + {mobileCount} + +
+ +
+
+
+
+
+
+ + {/* GDPR & Activity Footer */} +
+ GDPR_ANONYMIZED: TRUE + + LAST_PING:{" "} + {getTimeAgo(stats.lastPing)} + +
+
+
, + document.body, + ); +} diff --git a/src/components/AnalyticsWidget.tsx b/src/components/AnalyticsWidget.tsx index 47fb763..d44f3fb 100644 --- a/src/components/AnalyticsWidget.tsx +++ b/src/components/AnalyticsWidget.tsx @@ -1,21 +1,18 @@ "use client"; import { useEffect, useState } from "react"; +import AnalyticsModal, { AnalyticsData } from "./AnalyticsModal"; -export interface AnalyticsData { - totalViews: number; - todayViews: number; - uniqueVisitors: number; - devices: { - Desktop: number; - Mobile: number; - }; -} - +/** Footer badge widget that fetches live metrics and triggers the analytics modal */ export default function AnalyticsWidget() { const [stats, setStats] = useState(null); + const [isOpen, setIsOpen] = useState(false); + const [mounted, setMounted] = useState(false); useEffect(() => { + setMounted(true); + + // Track current page view and fetch live analytics data fetch("https://andre-kempf.com/backend/analytics.php?track=true") .then((res) => res.json()) .then((data) => { @@ -24,9 +21,10 @@ export default function AnalyticsWidget() { .catch(() => null); }, []); + // Skeleton badge while metrics are loading if (!stats) { return ( -
+
$ loading_stats... @@ -35,21 +33,41 @@ export default function AnalyticsWidget() { } return ( -
- - $ - sys.metrics - [ - - today: - {stats.todayViews} - - | - - total: - {stats.totalViews} - - ] -
+ <> + {/* Interactive terminal badge in footer */} + + + {/* Render modal only on client after hydration */} + {mounted && ( + setIsOpen(false)} + stats={stats} + /> + )} + ); }