+
{
);
diff --git a/apps/web/src/components/game/OptionButton.tsx b/apps/web/src/components/game/OptionButton.tsx
index e01e4ae..783d25d 100644
--- a/apps/web/src/components/game/OptionButton.tsx
+++ b/apps/web/src/components/game/OptionButton.tsx
@@ -6,26 +6,39 @@ type OptionButtonProps = {
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";
+ "group relative w-full overflow-hidden py-3 px-6 rounded-2xl text-lg font-semibold transition-all duration-200 border cursor-pointer text-left";
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",
+ idle: "bg-white text-(--color-primary-dark) border-(--color-primary-light) hover:bg-(--color-surface) hover:-translate-y-0.5 active:translate-y-0",
selected:
- "bg-purple-100 text-purple-900 border-purple-400 ring-2 ring-purple-400",
- 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",
+ "bg-(--color-surface) text-(--color-primary-dark) border-(--color-primary) ring-2 ring-(--color-primary)",
+ disabled:
+ "bg-(--color-surface) text-(--color-primary-light) border-(--color-primary-light) cursor-default",
+ correct:
+ "bg-emerald-400/90 text-white border-emerald-600 ring-2 ring-emerald-300 scale-[1.01]",
+ wrong: "bg-pink-500/90 text-white border-pink-700 ring-2 ring-pink-300",
};
+ const motion =
+ state === "correct" ? "lila-pop" : state === "wrong" ? "lila-shake" : "";
+
return (
);
};
diff --git a/apps/web/src/components/game/QuestionCard.tsx b/apps/web/src/components/game/QuestionCard.tsx
index f81ec0d..7878a5b 100644
--- a/apps/web/src/components/game/QuestionCard.tsx
+++ b/apps/web/src/components/game/QuestionCard.tsx
@@ -48,22 +48,31 @@ export const QuestionCard = ({
return (
-
-
- {questionNumber} / {totalQuestions}
-
+
+
+ Round {questionNumber}/{totalQuestions}
+
+
+ {currentResult ? "Checked" : selectedOptionId !== null ? "Ready" : "Pick one"}
+
-
-
+
+
+
+
+
{question.prompt}
{question.gloss && (
-
{question.gloss}
+
+ {question.gloss}
+
)}
-
+
+
{question.options.map((option) => (
handleSelect(option.optionId)}
/>
))}
+
{!currentResult && selectedOptionId !== null && (
)}
{currentResult && (
diff --git a/apps/web/src/components/game/ScoreScreen.tsx b/apps/web/src/components/game/ScoreScreen.tsx
index afd3295..ac64da2 100644
--- a/apps/web/src/components/game/ScoreScreen.tsx
+++ b/apps/web/src/components/game/ScoreScreen.tsx
@@ -1,4 +1,5 @@
import type { AnswerResult } from "@lila/shared";
+import { ConfettiBurst } from "../ui/ConfettiBurst";
type ScoreScreenProps = { results: AnswerResult[]; onPlayAgain: () => void };
@@ -17,30 +18,38 @@ export const ScoreScreen = ({ results, onPlayAgain }: ScoreScreenProps) => {
return (
-
-
Your Score
-
+
+ {percentage === 100 &&
}
+
+
+
+
+ Results
+
+
{score}/{total}
-
{getMessage()}
+
{getMessage()}
-
+
-
{percentage}% correct
+
+ {percentage}% correct
+
{results.map((result, index) => (
{index + 1}.
@@ -51,9 +60,9 @@ export const ScoreScreen = ({ results, onPlayAgain }: ScoreScreenProps) => {
);
diff --git a/apps/web/src/components/landing/FeatureCards.tsx b/apps/web/src/components/landing/FeatureCards.tsx
index 849afd0..e8085c2 100644
--- a/apps/web/src/components/landing/FeatureCards.tsx
+++ b/apps/web/src/components/landing/FeatureCards.tsx
@@ -12,29 +12,52 @@ const features = [
},
{
emoji: "⚔️",
- title: "Multiplayer coming",
- description: "Challenge friends and see who has the bigger vocabulary.",
+ title: "Real-time multiplayer",
+ description: "Create a room, share the code, and race to the best score.",
},
];
const FeatureCards = () => {
return (
-
-
- Why lila
-
+
+
+
+ Tiny rounds · big dopamine
+
+
+ Why lila
+
+
+ Built to be fast to start, satisfying to finish, and fun to repeat.
+
+
-
+
{features.map(({ emoji, title, description }) => (
-
{emoji}
-
{title}
-
+
+
+
+ {emoji}
+
+
{title}
+
+
{description}
+
+
+
+ Instant feedback
+
+
+
+ Type-safe API
+
+
))}
diff --git a/apps/web/src/components/landing/Hero.tsx b/apps/web/src/components/landing/Hero.tsx
index 0297e53..6a6de87 100644
--- a/apps/web/src/components/landing/Hero.tsx
+++ b/apps/web/src/components/landing/Hero.tsx
@@ -5,57 +5,132 @@ const Hero = () => {
const { data: session } = useSession();
return (
-
-
-
- Vocabulary trainer
-
+
+
-
- Meet{" "}
-
- lila
-
-
+
+
+
+
+ Duolingo-style drills · real-time multiplayer
+
+
-
- Learn words.{" "}
- Beat friends.
-
+
+ Learn vocabulary fast,{" "}
+
+ together
+
+ .
+
-
- {["🇬🇧", "🇮🇹", "🇩🇪", "🇫🇷", "🇪🇸"].map((flag) => (
-
- {flag}
-
- ))}
-
+
+ A word appears. You pick the translation. You score points.
+ Then you queue up a room and{" "}
+ beat friends{" "}
+ in real time.
+
-
- {session ? (
-
- Start playing →
-
- ) : (
- <>
-
- Get started →
-
-
- Login
-
- >
- )}
+
+ {["🇬🇧", "🇮🇹", "🇩🇪", "🇫🇷", "🇪🇸"].map((flag) => (
+
+ {flag}
+
+ ))}
+
+ Supported languages: English, Italian, German, French, Spanish
+
+
+
+
+ {session ? (
+ <>
+
+ Play solo
+
+
+ Play with friends
+
+ >
+ ) : (
+ <>
+
+ Get started
+
+
+ Log in
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+ Translate
+
+
+
+ finestra
+
+
+ (noun) · A2
+
+
+
+
+ {["window", "forest", "river", "kitchen"].map((opt) => (
+
+ {opt}
+
+ ))}
+
+
+
+
+ Round 2/10
+
+
+
+ Multiplayer room
+
+
+
+
+
+
);
diff --git a/apps/web/src/components/landing/HowItWorks.tsx b/apps/web/src/components/landing/HowItWorks.tsx
index b9791a8..8255493 100644
--- a/apps/web/src/components/landing/HowItWorks.tsx
+++ b/apps/web/src/components/landing/HowItWorks.tsx
@@ -20,26 +20,55 @@ const steps = [
const HowItWorks = () => {
return (
-
-
- How it works
-
-
-
- {steps.map(({ number, title, description }) => (
-
-
- {number}
-
-
{title}
-
- {description}
-
+
+
+
+
+
+
+ Quick · satisfying · replayable
- ))}
+
+ How it works
+
+
+ Short rounds, instant feedback, and just enough pressure to make the
+ words stick.
+
+
+
+
+ {steps.map(({ number, title, description }) => (
+ -
+
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+ Under 30 seconds
+
+
+
+
+ ))}
+
);
diff --git a/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx b/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx
index 5e95588..f530db8 100644
--- a/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx
+++ b/apps/web/src/components/multiplayer/MultiplayerScoreScreen.tsx
@@ -1,5 +1,6 @@
import { useNavigate } from "@tanstack/react-router";
import type { LobbyPlayer } from "@lila/shared";
+import { ConfettiBurst } from "../ui/ConfettiBurst";
type MultiplayerScoreScreenProps = {
players: LobbyPlayer[];
@@ -26,19 +27,27 @@ export const MultiplayerScoreScreen = ({
.join(" and ");
return (
-
-
+
+
+
+
+
+
+ {isWinner && !isTie &&
}
{/* Result header */}
-
+
+ Multiplayer
+
+
{isTie ? "It's a tie!" : isWinner ? "You win! 🎉" : "Game over"}
-
+
{isTie ? `${winnerNames} tied` : `${winnerNames} wins!`}
-
+
{/* Score list */}
@@ -48,35 +57,35 @@ export const MultiplayerScoreScreen = ({
return (
-
+
{index + 1}.
{player.user.name}
{isCurrentUser && (
-
+
(you)
)}
{isPlayerWinner && (
-
+
👑
)}
-
+
{player.score} pts
@@ -84,12 +93,12 @@ export const MultiplayerScoreScreen = ({
})}
-
+
{/* Actions */}