feat(web): add game settings screen and submit confirmation

- Add GameSetup component with Duolingo-style button selectors for
  language pair, POS, difficulty, and rounds
- Language swap: selecting the same language for source and target
  automatically swaps them instead of allowing duplicates
- Add selection-before-submission flow: user clicks an option to
  highlight it, then confirms with a Submit button to prevent misclicks
- Add selected state to OptionButton (purple ring highlight)
- Play Again on score screen returns to settings instead of
  auto-restarting with the same configuration
- Remove hardcoded GAME_SETTINGS, game configuration is now user-driven
This commit is contained in:
lila 2026-04-11 21:18:35 +02:00
parent b7b1cd383f
commit bc7977463e
4 changed files with 209 additions and 26 deletions

View file

@ -1,38 +1,38 @@
import { createFileRoute } from "@tanstack/react-router";
import { useState, useEffect } from "react";
import type { GameSession, AnswerResult } from "@glossa/shared";
import { useState, useCallback } from "react";
import type { GameSession, GameRequest, AnswerResult } from "@glossa/shared";
import { QuestionCard } from "../components/game/QuestionCard";
import { ScoreScreen } from "../components/game/ScoreScreen";
const GAME_SETTINGS = {
source_language: "en",
target_language: "it",
pos: "noun",
difficulty: "easy",
rounds: "3",
} as const;
import { GameSetup } from "../components/game/GameSetup";
function Play() {
const [gameSession, setGameSession] = useState<GameSession | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [results, setResults] = useState<AnswerResult[]>([]);
const [currentResult, setCurrentResult] = useState<AnswerResult | null>(null);
const startGame = async () => {
const startGame = useCallback(async (settings: GameRequest) => {
setIsLoading(true);
const response = await fetch("/api/v1/game/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(GAME_SETTINGS),
body: JSON.stringify(settings),
});
const data = await response.json();
setGameSession(data.data);
setCurrentQuestionIndex(0);
setResults([]);
setCurrentResult(null);
};
setIsLoading(false);
}, []);
useEffect(() => {
startGame();
const resetToSetup = useCallback(() => {
setGameSession(null);
setIsLoading(false);
setCurrentQuestionIndex(0);
setResults([]);
setCurrentResult(null);
}, []);
const handleAnswer = async (optionId: number) => {
@ -61,8 +61,17 @@ function Play() {
setCurrentResult(null);
};
// Phase: setup
if (!gameSession && !isLoading) {
return (
<div className="min-h-screen bg-linear-to-b from-purple-100 to-pink-50 flex items-center justify-center p-6">
<GameSetup onStart={startGame} />
</div>
);
}
// Phase: loading
if (!gameSession) {
if (isLoading || !gameSession) {
return (
<div className="min-h-screen bg-linear-to-b from-purple-100 to-pink-50 flex items-center justify-center">
<p className="text-purple-400 text-lg font-medium">Loading...</p>
@ -74,7 +83,7 @@ function Play() {
if (currentQuestionIndex >= gameSession.questions.length) {
return (
<div className="min-h-screen bg-linear-to-b from-purple-100 to-pink-50 flex items-center justify-center p-6">
<ScoreScreen results={results} onPlayAgain={startGame} />
<ScoreScreen results={results} onPlayAgain={resetToSetup} />
</div>
);
}