From cfc9cd8c7770ddc8f151610acd177e54169e28d7 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Fri, 5 Jun 2026 22:08:51 -0700 Subject: feat: historical stats tracking, chart --- src/components/Chart/index.tsx | 139 ++++++++++++++++++++++++++++++++++++++++ src/components/Game/index.tsx | 28 +++++++- src/components/Result/index.tsx | 17 +++++ src/index.tsx | 9 ++- 4 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 src/components/Chart/index.tsx (limited to 'src') diff --git a/src/components/Chart/index.tsx b/src/components/Chart/index.tsx new file mode 100644 index 0000000..6aac91e --- /dev/null +++ b/src/components/Chart/index.tsx @@ -0,0 +1,139 @@ +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Tooltip, + Legend, +} from "chart.js"; +import type { ChartOptions } from "chart.js"; +import { Bar } from "react-chartjs-2"; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Tooltip, + Legend +); + +interface Props { + currentTry: number; + didGuess: boolean; + sessionDate: string; +} + +interface HistoricalPlayData { + sessionDates?: string[]; + guesses?: number[]; + didGuess?: boolean[]; +} + +type GuessCount = 1 | 2 | 3 | 4 | 5 | 6; +type Distribution = Record & { NS: number }; + +function getHistoricalPlayData(): HistoricalPlayData { + try { + return JSON.parse(localStorage.getItem("historicalPlayData") || "{}"); + } catch { + return {}; + } +} + +function addResultToDistribution( + distribution: Distribution, + guessCount: number, + didGuess: boolean +) { + if (!didGuess) { + distribution.NS += 1; + return; + } + + if (guessCount >= 1 && guessCount <= 6) { + distribution[guessCount as GuessCount] += 1; + } +} + +export default function GuessDistributionChart({ + currentTry, + didGuess, + sessionDate, +}: Props) { + const historicalPlayData = getHistoricalPlayData(); + + const distribution: Distribution = { + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 6: 0, + NS: 0, + }; + + if (historicalPlayData.guesses && historicalPlayData.didGuess) { + historicalPlayData.guesses.forEach((guessCount, index) => { + addResultToDistribution( + distribution, + guessCount, + historicalPlayData.didGuess?.[index] ?? false + ); + }); + } + + if (!historicalPlayData.sessionDates?.includes(sessionDate)) { + addResultToDistribution(distribution, currentTry, didGuess); + } + + const data = { + labels: ["1", "2", "3", "4", "5", "6", "NS"], + datasets: [ + { + data: [ + distribution[1], + distribution[2], + distribution[3], + distribution[4], + distribution[5], + distribution[6], + distribution.NS, + ], + backgroundColor: "#6aaa64", + borderRadius: 4, + }, + ], + }; + + const options: ChartOptions<"bar"> = { + responsive: true, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: true, + }, + }, + scales: { + x: { + grid: { + display: false, + }, + }, + y: { + beginAtZero: true, + ticks: { + precision: 0, + }, + }, + }, + }; + + return ( +
+

Statistics

+ +
+ ); +} diff --git a/src/components/Game/index.tsx b/src/components/Game/index.tsx index 59f9289..657b2af 100644 --- a/src/components/Game/index.tsx +++ b/src/components/Game/index.tsx @@ -47,13 +47,36 @@ export function Game({ !!dailyDate && !hasFinishedResponseDaily && new Date(getUtcDate()) > new Date(dailyDate); + const sessionDate = dailyDate ?? getUtcDate(); React.useEffect(() => { if (mode !== "daily") return; if (!hasFinishedCurrentRound) return; + localStorage.setItem("recentFinishedPlay", sessionDate); - localStorage.setItem("recentFinishedPlay", dailyDate ?? getUtcDate()); - }, [mode, hasFinishedCurrentRound, dailyDate]); + const historicalPlayData = localStorage.getItem("historicalPlayData"); + if (historicalPlayData === null) { + localStorage.setItem( + "historicalPlayData", + JSON.stringify({ + sessionDates: [sessionDate], + guesses: [currentTry], + didGuess: [didGuess], + }) + ); + } else { + const parsedData = JSON.parse(historicalPlayData); + if (parsedData.sessionDates.includes(sessionDate)) return; + localStorage.setItem( + "historicalPlayData", + JSON.stringify({ + sessionDates: [...parsedData.sessionDates, sessionDate], + guesses: [...parsedData.guesses, currentTry], + didGuess: [...parsedData.didGuess, didGuess], + }) + ); + } + }, [mode, hasFinishedCurrentRound, sessionDate, currentTry, didGuess]); if (isBlocked) { return

Daily MIXX is not available yet. Check back soon!

; @@ -67,6 +90,7 @@ export function Game({ todaysSolution={todaysSolution} guesses={guesses} mode={mode} + sessionDate={sessionDate} onPlayAgain={onPlayAgain} /> ); diff --git a/src/components/Result/index.tsx b/src/components/Result/index.tsx index 4ce7f35..c74e4f8 100644 --- a/src/components/Result/index.tsx +++ b/src/components/Result/index.tsx @@ -9,6 +9,7 @@ import { MiniYouTubePlayer } from "../MiniYouTubePlayer"; import * as Styled from "./index.styled"; import { theme } from "../../constants"; +import GuessDistributionChart from '../Chart'; interface SolutionProps { didGuess: boolean; @@ -89,6 +90,7 @@ interface Props { todaysSolution: Song; guesses: GuessType[]; mode?: "daily" | "unlimited"; + sessionDate: string; onPlayAgain?: () => void; } @@ -98,6 +100,7 @@ export function Result({ guesses, currentTry, mode = "daily", + sessionDate, onPlayAgain, }: Props) { const now = new Date(); @@ -126,6 +129,13 @@ export function Result({ currentTry={currentTry} isUnlimited={isUnlimited} /> + {!isUnlimited && ( + + )} {!isUnlimited && } @@ -152,6 +162,13 @@ export function Result({ currentTry={currentTry} isUnlimited={isUnlimited} /> + {!isUnlimited && ( + + )} {!isUnlimited && } diff --git a/src/index.tsx b/src/index.tsx index e1b6a22..c4a2bfc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,15 +1,18 @@ import React from "react"; -import ReactDOM from "react-dom"; +import ReactDOM from "react-dom/client"; import { ThemeProvider } from "styled-components"; import { theme } from "./constants"; import "./index.css"; import App from "./app"; -ReactDOM.render( +const root = ReactDOM.createRoot( + document.getElementById("root") as HTMLElement +); + +root.render( , - document.getElementById("root") ); -- cgit v1.2.3