diff --git a/apps/web/src/components/game/OptionButton.tsx b/apps/web/src/components/game/OptionButton.tsx
new file mode 100644
index 0000000..9b4a8b3
--- /dev/null
+++ b/apps/web/src/components/game/OptionButton.tsx
@@ -0,0 +1,27 @@
+type OptionButtonProps = {
+ text: string;
+ state: "idle" | "disabled" | "correct" | "wrong";
+ onSelect: () => void;
+};
+
+export const OptionButton = ({ text, state, onSelect }: OptionButtonProps) => {
+ const base =
+ "w-full py-3 px-6 rounded-2xl text-lg font-semibold transition-all duration-200 border-b-4 cursor-pointer";
+
+ const styles = {
+ idle: "bg-white text-purple-900 border-purple-200 hover:bg-purple-50 hover:border-purple-300 hover:-translate-y-0.5 active:translate-y-0 active:border-b-2",
+ disabled: "bg-gray-100 text-gray-400 border-gray-200 cursor-default",
+ correct: "bg-emerald-400 text-white border-emerald-600 scale-[1.02]",
+ wrong: "bg-pink-400 text-white border-pink-600",
+ };
+
+ return (
+
+ );
+};
diff --git a/apps/web/src/components/game/QuestionCard.tsx b/apps/web/src/components/game/QuestionCard.tsx
new file mode 100644
index 0000000..38334c0
--- /dev/null
+++ b/apps/web/src/components/game/QuestionCard.tsx
@@ -0,0 +1,66 @@
+import type { GameQuestion, AnswerResult } from "@glossa/shared";
+import { OptionButton } from "./OptionButton";
+
+type QuestionCardProps = {
+ question: GameQuestion;
+ questionNumber: number;
+ totalQuestions: number;
+ currentResult: AnswerResult | null;
+ onAnswer: (optionId: number) => void;
+ onNext: () => void;
+};
+
+export const QuestionCard = ({
+ question,
+ questionNumber,
+ totalQuestions,
+ currentResult,
+ onAnswer,
+ onNext,
+}: QuestionCardProps) => {
+ const getOptionState = (optionId: number) => {
+ if (!currentResult) return "idle" as const;
+ if (optionId === currentResult.correctOptionId) return "correct" as const;
+ if (optionId === currentResult.selectedOptionId) return "wrong" as const;
+ return "disabled" as const;
+ };
+
+ return (
+
+
+
+ {questionNumber} / {totalQuestions}
+
+
+
+
+
+ {question.prompt}
+
+ {question.gloss && (
+
{question.gloss}
+ )}
+
+
+
+ {question.options.map((option) => (
+ onAnswer(option.optionId)}
+ />
+ ))}
+
+
+ {currentResult && (
+
+ )}
+
+ );
+};
diff --git a/apps/web/src/components/game/ScoreScreen.tsx b/apps/web/src/components/game/ScoreScreen.tsx
new file mode 100644
index 0000000..44b3250
--- /dev/null
+++ b/apps/web/src/components/game/ScoreScreen.tsx
@@ -0,0 +1,60 @@
+import type { AnswerResult } from "@glossa/shared";
+
+type ScoreScreenProps = { results: AnswerResult[]; onPlayAgain: () => void };
+
+export const ScoreScreen = ({ results, onPlayAgain }: ScoreScreenProps) => {
+ const score = results.filter((r) => r.isCorrect).length;
+ const total = results.length;
+ const percentage = Math.round((score / total) * 100);
+
+ const getMessage = () => {
+ if (percentage === 100) return "Perfect! 🎉";
+ if (percentage >= 80) return "Great job! 🌟";
+ if (percentage >= 60) return "Not bad! 💪";
+ if (percentage >= 40) return "Keep practicing! 📚";
+ return "Don't give up! 🔄";
+ };
+
+ return (
+
+
+
Your Score
+
+ {score}/{total}
+
+
{getMessage()}
+
+
+
{percentage}% correct
+
+
+
+ {results.map((result, index) => (
+
+ {index + 1}.
+ {result.isCorrect ? "✓ Correct" : "✗ Wrong"}
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/apps/web/src/routes/play.tsx b/apps/web/src/routes/play.tsx
index e4100be..905f92e 100644
--- a/apps/web/src/routes/play.tsx
+++ b/apps/web/src/routes/play.tsx
@@ -1,6 +1,8 @@
import { createFileRoute } from "@tanstack/react-router";
import { useState, useEffect } from "react";
import type { GameSession, AnswerResult } from "@glossa/shared";
+import { QuestionCard } from "../components/game/QuestionCard";
+import { ScoreScreen } from "../components/game/ScoreScreen";
const GAME_SETTINGS = {
source_language: "en",
@@ -16,16 +18,20 @@ function Play() {
const [results, setResults] = useState([]);
const [currentResult, setCurrentResult] = useState(null);
+ const startGame = async () => {
+ const response = await fetch("/api/v1/game/start", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(GAME_SETTINGS),
+ });
+ const data = await response.json();
+ setGameSession(data.data);
+ setCurrentQuestionIndex(0);
+ setResults([]);
+ setCurrentResult(null);
+ };
+
useEffect(() => {
- const startGame = async () => {
- const response = await fetch("/api/v1/game/start", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(GAME_SETTINGS),
- });
- const data = await response.json();
- setGameSession(data.data);
- };
startGame();
}, []);
@@ -57,18 +63,18 @@ function Play() {
// Phase: loading
if (!gameSession) {
- return Loading...
;
+ return (
+
+ );
}
// Phase: finished
if (currentQuestionIndex >= gameSession.questions.length) {
- const score = results.filter((r) => r.isCorrect).length;
return (
-
-
- {score} / {results.length}
-
-
Game over!
+
+
);
}
@@ -77,45 +83,15 @@ function Play() {
const question = gameSession.questions[currentQuestionIndex]!;
return (
-
-
- Question {currentQuestionIndex + 1} / {gameSession.questions.length}
-
-
{question.prompt}
- {question.gloss &&
{question.gloss}
}
-
- {question.options.map((option) => {
- let style = {};
- if (currentResult) {
- if (option.optionId === currentResult.correctOptionId) {
- style = { backgroundColor: "green", color: "white" };
- } else if (option.optionId === currentResult.selectedOptionId) {
- style = { backgroundColor: "red", color: "white" };
- }
- }
-
- return (
-
- );
- })}
-
- {currentResult && (
-
- )}
+
+
);
}