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 ( +
+

Loading...

+
+ ); } // 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 && ( - - )} +
+
); }