diff --git a/src/app/analytics/page.tsx b/src/app/analytics/page.tsx
new file mode 100644
index 0000000..11f5d0a
--- /dev/null
+++ b/src/app/analytics/page.tsx
@@ -0,0 +1,46 @@
+import AnalyticsChart from "@/components/AnalyticsChart";
+import Card from "@/components/Card";
+import Container from "@/components/Container";
+
+import { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "Search",
+};
+
+export async function getData() {
+ const response = await fetch(
+ "https://bs-tracker.fascinated.cc/analytics?time=30d",
+ {
+ next: {
+ revalidate: 600, // 10 minutes
+ },
+ },
+ );
+
+ const json = await response.json();
+ return json;
+}
+
+export default async function Home() {
+ const playerCountHistory = await getData();
+
+ return (
+
+
+
+ Analytics
+
+ Scoresaber metrics and statistics over time.
+
+
+
+
+
+ );
+}
diff --git a/src/components/AnalyticsChart.tsx b/src/components/AnalyticsChart.tsx
new file mode 100644
index 0000000..660a1fb
--- /dev/null
+++ b/src/components/AnalyticsChart.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import { ScoresaberPlayerCountHistory } from "@/schemas/fascinated/scoresaberPlayerCountHistory";
+import { formatTimeAgo } from "@/utils/timeUtils";
+import {
+ CategoryScale,
+ Chart as ChartJS,
+ Legend,
+ LineElement,
+ LinearScale,
+ PointElement,
+ Title,
+ Tooltip,
+} from "chart.js";
+import { Line } from "react-chartjs-2";
+
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend,
+);
+
+type PlayerChartProps = {
+ className?: string;
+ playerCountHistoryData: ScoresaberPlayerCountHistory;
+};
+
+export const options: any = {
+ maintainAspectRatio: false,
+ aspectRatio: 1,
+ interaction: {
+ mode: "index",
+ intersect: false,
+ },
+ scales: {
+ y: {
+ ticks: {
+ autoSkip: true,
+ maxTicksLimit: 8,
+ stepSize: 1,
+ },
+ },
+ x: {
+ ticks: {
+ autoSkip: true,
+ },
+ },
+ },
+ elements: {
+ point: {
+ radius: 0,
+ },
+ },
+ plugins: {
+ legend: {
+ position: "top" as const,
+ labels: {
+ color: "white",
+ },
+ },
+ title: {
+ display: false,
+ },
+ tooltip: {
+ callbacks: {
+ label(context: any) {
+ switch (
+ context.dataset.label
+ // case "Rank": {
+ // return `Rank #${formatNumber(context.parsed.y.toFixed(0))}`;
+ // }
+ ) {
+ }
+ },
+ },
+ },
+ },
+};
+
+export default function AnalyticsChart({
+ className,
+ playerCountHistoryData,
+}: PlayerChartProps) {
+ const playerCountHistory = playerCountHistoryData.history;
+
+ let labels = [];
+ for (let i = 0; i < playerCountHistory.length; i++) {
+ if (i == playerCountHistory.length - 1) {
+ labels.push("now");
+ continue;
+ }
+ labels.push(formatTimeAgo(playerCountHistory[i].time));
+ }
+
+ const data = {
+ labels,
+ datasets: [
+ {
+ lineTension: 0.5,
+ data: playerCountHistory.map((count) => count.value),
+ label: "Active Players",
+ borderColor: "#3e95cd",
+ fill: false,
+ color: "#fff",
+ },
+ ],
+ };
+
+ return ;
+}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index e91deab..3e8649b 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -5,6 +5,7 @@ import useStore from "@/utils/useStore";
import {
CogIcon,
MagnifyingGlassIcon,
+ ServerIcon,
UserIcon,
} from "@heroicons/react/20/solid";
import { GlobeAltIcon } from "@heroicons/react/24/outline";
@@ -95,6 +96,11 @@ export default function Navbar() {
icon={}
href="/ranking/global/1"
/>
+ }
+ href="/analytics"
+ />
diff --git a/src/components/player/Score.tsx b/src/components/player/Score.tsx
index b10774a..8aa95de 100644
--- a/src/components/player/Score.tsx
+++ b/src/components/player/Score.tsx
@@ -29,6 +29,7 @@ export default function Score({ score, player, leaderboard }: ScoreProps) {
leaderboard.difficulty.difficulty,
);
const diffColor = songDifficultyToColor(diffName);
+ const accuracy = ((score.baseScore / leaderboard.maxScore) * 100).toFixed(2);
return (
//
@@ -125,9 +126,7 @@ export default function Score({ score, player, leaderboard }: ScoreProps) {
value={
!leaderboard.maxScore
? formatNumber(score.baseScore)
- : ((score.baseScore / leaderboard.maxScore) * 100).toFixed(
- 2,
- ) + "%"
+ : accuracy + "%"
}
/>
diff --git a/src/schemas/fascinated/scoresaberPlayerCountHistory.ts b/src/schemas/fascinated/scoresaberPlayerCountHistory.ts
new file mode 100644
index 0000000..0cded69
--- /dev/null
+++ b/src/schemas/fascinated/scoresaberPlayerCountHistory.ts
@@ -0,0 +1,7 @@
+export type ScoresaberPlayerCountHistory = {
+ serverTimeTaken: number;
+ history: {
+ time: string;
+ value: number | null;
+ }[];
+};