From 6b5c5efb930ec55991183b6e76894bc047afb80a Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Tue, 7 Apr 2026 21:15:44 +0700 Subject: [PATCH] feat: add HUD with timer, score, hint, shuffle, pause, and toast Co-Authored-By: Claude Sonnet 4.6 --- superpowers/src/components/App.tsx | 38 ++++++++++--- superpowers/src/components/HUD.tsx | 79 ++++++++++++++++++++++++++++ superpowers/src/components/Toast.tsx | 41 +++++++++++++++ 3 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 superpowers/src/components/HUD.tsx create mode 100644 superpowers/src/components/Toast.tsx diff --git a/superpowers/src/components/App.tsx b/superpowers/src/components/App.tsx index 8ce3a9d..712908e 100644 --- a/superpowers/src/components/App.tsx +++ b/superpowers/src/components/App.tsx @@ -1,15 +1,18 @@ -import { useState, useRef, useCallback } from "react"; +import { useState, useRef, useCallback, useEffect } from "react"; import { Difficulty } from "../types"; import { GameStateManager } from "../game/state"; import { Menu } from "./Menu"; import { DifficultySelect } from "./DifficultySelect"; import { GameContainer } from "./GameContainer"; +import { HUD } from "./HUD"; +import { Toast } from "./Toast"; type Screen = "menu" | "difficulty" | "game" | "gameover"; function App() { const [screen, setScreen] = useState("menu"); const [difficulty, setDifficulty] = useState("easy"); + const [toastVisible, setToastVisible] = useState(false); const stateManager = useRef(new GameStateManager()).current; const handleSelectDifficulty = (d: Difficulty) => { @@ -22,6 +25,21 @@ function App() { setScreen("gameover"); }, []); + const handlePause = () => { + const state = stateManager.getState(); + if (state.status === "playing") { + stateManager.setStatus("paused"); + } else if (state.status === "paused") { + stateManager.setStatus("playing"); + } + }; + + useEffect(() => { + const handleAutoShuffle = () => setToastVisible(true); + stateManager.on("autoShuffle", handleAutoShuffle); + return () => stateManager.off("autoShuffle", handleAutoShuffle); + }, [stateManager]); + return (
{screen === "menu" && ( @@ -34,15 +52,23 @@ function App() { /> )} {screen === "game" && ( - +
+ + +
)} {screen === "gameover" && (
Game Over placeholder
)} + setToastVisible(false)} + />
); } diff --git a/superpowers/src/components/HUD.tsx b/superpowers/src/components/HUD.tsx new file mode 100644 index 0000000..f174dbf --- /dev/null +++ b/superpowers/src/components/HUD.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react"; +import { GameStateManager } from "../game/state"; + +interface HUDProps { + stateManager: GameStateManager; + onPause: () => void; +} + +export function HUD({ stateManager, onPause }: HUDProps) { + const [state, setState] = useState(stateManager.getState()); + + useEffect(() => { + const update = () => setState(stateManager.getState()); + stateManager.on("stateChange", update); + return () => stateManager.off("stateChange", update); + }, [stateManager]); + + const minutes = Math.floor(state.timerSeconds / 60); + const seconds = state.timerSeconds % 60; + const timeStr = `${minutes}:${seconds.toString().padStart(2, "0")}`; + + return ( +
+
⏱ {timeStr}
+
⭐ {state.score}
+ + + +
+ ); +} diff --git a/superpowers/src/components/Toast.tsx b/superpowers/src/components/Toast.tsx new file mode 100644 index 0000000..f2e4fa5 --- /dev/null +++ b/superpowers/src/components/Toast.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from "react"; + +interface ToastProps { + message: string; + visible: boolean; + onHide: () => void; +} + +export function Toast({ message, visible, onHide }: ToastProps) { + const [show, setShow] = useState(false); + + useEffect(() => { + if (visible) { + setShow(true); + const timer = setTimeout(() => { + setShow(false); + onHide(); + }, 2000); + return () => clearTimeout(timer); + } + }, [visible, onHide]); + + if (!show) return null; + + return ( +
+ {message} +
+ ); +}