From 9e98af105320e24edf5e797a29ac7de3e21db7e2 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sat, 16 May 2026 17:16:51 +0800 Subject: [PATCH] feat: add analytics page with time-series charts to manage panel --- app/(manage)/manage/analytics/page.tsx | 10 +++ components/manage/analytics-client.tsx | 120 +++++++++++++++++++++++++ components/manage/manage-sidebar.tsx | 6 ++ 3 files changed, 136 insertions(+) create mode 100644 app/(manage)/manage/analytics/page.tsx create mode 100644 components/manage/analytics-client.tsx diff --git a/app/(manage)/manage/analytics/page.tsx b/app/(manage)/manage/analytics/page.tsx new file mode 100644 index 0000000..dcf4dbc --- /dev/null +++ b/app/(manage)/manage/analytics/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next" +import { AnalyticsClient } from "@/components/manage/analytics-client" + +export const metadata: Metadata = { + title: "Analytics", +} + +export default function AnalyticsPage() { + return +} diff --git a/components/manage/analytics-client.tsx b/components/manage/analytics-client.tsx new file mode 100644 index 0000000..299b0d5 --- /dev/null +++ b/components/manage/analytics-client.tsx @@ -0,0 +1,120 @@ +"use client" + +import { useState, useEffect } from "react" +import { TrendingUp, Users, Monitor, Tag } from "lucide-react" +import ReactEChartsCore from "echarts-for-react/lib/core" +import * as echarts from "echarts/core" +import { LineChart, BarChart } from "echarts/charts" +import { + GridComponent, + TooltipComponent, + LegendComponent, +} from "echarts/components" +import { CanvasRenderer } from "echarts/renderers" + +echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]) + +interface AnalyticsData { + benchmarkTimeline: Array<{ day: string; count: number }> + userTimeline: Array<{ day: string; count: number }> + deviceDistribution: Array<{ hardwareSlug: string; hardwareName: string; count: number }> + genrePopularity: Array<{ genre: string; count: number }> + commentTimeline: Array<{ day: string; count: number }> +} + +export function AnalyticsClient() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetch("/api/admin/analytics/overview") + .then((res) => res.json()) + .then((d) => { setData(d); setLoading(false) }) + .catch(() => setLoading(false)) + }, []) + + if (loading) { + return
+ } + + if (!data) { + return
Failed to load analytics
+ } + + const benchmarkOption = { + tooltip: { trigger: "axis" as const }, + xAxis: { type: "category" as const, data: data.benchmarkTimeline.map((d) => d.day), axisLabel: { fontSize: 10 } }, + yAxis: { type: "value" as const }, + series: [{ name: "Benchmarks", type: "line", data: data.benchmarkTimeline.map((d) => d.count), smooth: true, itemStyle: { color: "#eb3779" } }], + grid: { left: 40, right: 16, top: 16, bottom: 24 }, + } + + const userOption = { + tooltip: { trigger: "axis" as const }, + xAxis: { type: "category" as const, data: data.userTimeline.map((d) => d.day), axisLabel: { fontSize: 10 } }, + yAxis: { type: "value" as const }, + series: [{ name: "Registrations", type: "line", data: data.userTimeline.map((d) => d.count), smooth: true, itemStyle: { color: "#a78bfa" }, areaStyle: { color: "rgba(167,139,250,0.1)" } }], + grid: { left: 40, right: 16, top: 16, bottom: 24 }, + } + + const deviceOption = { + tooltip: { trigger: "axis" as const }, + xAxis: { type: "category" as const, data: data.deviceDistribution.map((d) => d.hardwareName), axisLabel: { fontSize: 10 } }, + yAxis: { type: "value" as const }, + series: [{ name: "Benchmarks", type: "bar", data: data.deviceDistribution.map((d) => d.count), itemStyle: { color: "#fb793c" } }], + grid: { left: 40, right: 16, top: 16, bottom: 24 }, + } + + const genreOption = { + tooltip: { trigger: "axis" as const }, + xAxis: { type: "category" as const, data: data.genrePopularity.map((d) => d.genre), axisLabel: { fontSize: 10, rotate: 30 } }, + yAxis: { type: "value" as const }, + series: [{ name: "Benchmarks", type: "bar", data: data.genrePopularity.map((d) => d.count), itemStyle: { color: "#22c55e" } }], + grid: { left: 40, right: 16, top: 16, bottom: 60 }, + } + + return ( +
+
+

Analytics

+

90-day trends and distributions

+
+ + {/* Benchmark Submissions Over Time */} +
+
+ +

Benchmark Submissions (90d)

+
+ +
+ + {/* User Registrations Over Time */} +
+
+ +

User Registrations (90d)

+
+ +
+ + {/* Device Distribution + Genre Popularity (side by side) */} +
+
+
+ +

Device Distribution

+
+ +
+
+
+ +

Genre Popularity

+
+ +
+
+
+ ) +} diff --git a/components/manage/manage-sidebar.tsx b/components/manage/manage-sidebar.tsx index d377440..23ac5f7 100644 --- a/components/manage/manage-sidebar.tsx +++ b/components/manage/manage-sidebar.tsx @@ -28,6 +28,12 @@ const navItems: NavItem[] = [ label: "Dashboard", icon: LayoutDashboardIcon, }, + { + type: "link", + href: "/manage/analytics", + label: "Analytics", + icon: BarChart3Icon, + }, { type: "divider" }, { type: "section", label: "Management" }, { type: "link", href: "/manage/users", label: "Users", icon: UsersIcon, adminOnly: true },