feat: add HUD with timer, score, hint, shuffle, pause, and toast

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 21:15:44 +07:00
co-authored by Claude Sonnet 4.6
parent 49fd12373c
commit 6b5c5efb93
3 changed files with 152 additions and 6 deletions
+32 -6
View File
@@ -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<Screen>("menu");
const [difficulty, setDifficulty] = useState<Difficulty>("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 (
<div style={{ textAlign: "center" }}>
{screen === "menu" && (
@@ -34,15 +52,23 @@ function App() {
/>
)}
{screen === "game" && (
<GameContainer
difficulty={difficulty}
stateManager={stateManager}
onGameOver={handleGameOver}
/>
<div>
<HUD stateManager={stateManager} onPause={handlePause} />
<GameContainer
difficulty={difficulty}
stateManager={stateManager}
onGameOver={handleGameOver}
/>
</div>
)}
{screen === "gameover" && (
<div>Game Over placeholder</div>
)}
<Toast
message="No moves available — board shuffled!"
visible={toastVisible}
onHide={() => setToastVisible(false)}
/>
</div>
);
}
+79
View File
@@ -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 (
<div style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
gap: "24px",
padding: "12px 24px",
background: "#16213e",
borderRadius: "8px",
marginBottom: "8px",
fontSize: "16px",
}}>
<div> {timeStr}</div>
<div> {state.score}</div>
<button
onClick={() => stateManager.emit("hint")}
disabled={state.hintsRemaining <= 0}
style={{
padding: "6px 16px",
background: state.hintsRemaining > 0 ? "#533483" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: state.hintsRemaining > 0 ? "pointer" : "not-allowed",
}}
>
💡 Hint ({state.hintsRemaining})
</button>
<button
onClick={() => stateManager.emit("shuffle")}
disabled={state.shufflesRemaining <= 0}
style={{
padding: "6px 16px",
background: state.shufflesRemaining > 0 ? "#533483" : "#333",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: state.shufflesRemaining > 0 ? "pointer" : "not-allowed",
}}
>
🔀 Shuffle ({state.shufflesRemaining})
</button>
<button
onClick={onPause}
style={{
padding: "6px 16px",
background: "#0f3460",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
>
Pause
</button>
</div>
);
}
+41
View File
@@ -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 (
<div style={{
position: "fixed",
top: "20px",
left: "50%",
transform: "translateX(-50%)",
background: "#e94560",
color: "#fff",
padding: "12px 24px",
borderRadius: "8px",
fontSize: "16px",
zIndex: 1000,
}}>
{message}
</div>
);
}