mirror of
https://github.com/tiennm99/ai-coding-workflow-labs.git
synced 2026-08-13 11:22:51 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user