- Type response bodies in gameController.test.ts to fix no-unsafe-member-access - Replace async methods with Promise.resolve() in InMemoryGameSessionStore and InMemoryLobbyGameStore to satisfy require-await rule - Add argsIgnorePattern and varsIgnorePattern to eslint config so underscore-prefixed params are globally ignored - Fix no-misused-promises in ws/index.ts, lobbyHandlers, gameHandlers, __root.tsx, login.tsx and play.tsx by using void + .catch() - Fix no-floating-promises on navigate calls in login.tsx - Move API_URL outside Play component to fix useCallback dependency warning - Type fetch response bodies in play.tsx to fix no-unsafe-assignment - Add only-throw-error: off for route files (TanStack Router throw redirect) - Remove unused WebSocket import from express.d.ts - Fix unsafe return in connections.ts by typing empty Map constructor - Exclude scripts/ folder from eslint - Add targeted override for better-auth auth-client.ts (upstream typing issue)
136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
import { useState, useCallback } from "react";
|
|
import type { GameSession, GameRequest, AnswerResult } from "@lila/shared";
|
|
import { QuestionCard } from "../components/game/QuestionCard";
|
|
import { ScoreScreen } from "../components/game/ScoreScreen";
|
|
import { GameSetup } from "../components/game/GameSetup";
|
|
import { authClient } from "../lib/auth-client";
|
|
|
|
type GameStartResponse = { success: true; data: GameSession };
|
|
type GameAnswerResponse = { success: true; data: AnswerResult };
|
|
|
|
const API_URL = (import.meta.env["VITE_API_URL"] as string) || "";
|
|
|
|
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 = useCallback(async (settings: GameRequest) => {
|
|
setIsLoading(true);
|
|
|
|
const response = await fetch(`${API_URL}/api/v1/game/start`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify(settings),
|
|
});
|
|
|
|
const data = (await response.json()) as GameStartResponse;
|
|
setGameSession(data.data);
|
|
setCurrentQuestionIndex(0);
|
|
setResults([]);
|
|
setCurrentResult(null);
|
|
setIsLoading(false);
|
|
}, []);
|
|
|
|
const resetToSetup = useCallback(() => {
|
|
setGameSession(null);
|
|
setIsLoading(false);
|
|
setCurrentQuestionIndex(0);
|
|
setResults([]);
|
|
setCurrentResult(null);
|
|
}, []);
|
|
|
|
const handleAnswer = async (optionId: number) => {
|
|
if (!gameSession || currentResult) return;
|
|
|
|
const question = gameSession.questions[currentQuestionIndex];
|
|
if (!question) return;
|
|
|
|
const response = await fetch(`${API_URL}/api/v1/game/answer`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify({
|
|
sessionId: gameSession.sessionId,
|
|
questionId: question.questionId,
|
|
selectedOptionId: optionId,
|
|
}),
|
|
});
|
|
const data = (await response.json()) as GameAnswerResponse;
|
|
setCurrentResult(data.data);
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (!currentResult) return;
|
|
setResults((prev) => [...prev, currentResult]);
|
|
setCurrentQuestionIndex((prev) => prev + 1);
|
|
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={(settings) => {
|
|
void startGame(settings).catch((err) => {
|
|
console.error("Start game error:", err);
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Phase: loading
|
|
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>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Phase: finished
|
|
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={resetToSetup} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Phase: playing
|
|
const question = gameSession.questions[currentQuestionIndex]!;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-linear-to-b from-purple-100 to-pink-50 flex items-center justify-center p-6">
|
|
<QuestionCard
|
|
onAnswer={(optionId) => {
|
|
void handleAnswer(optionId).catch((err) => {
|
|
console.error("Answer error:", err);
|
|
});
|
|
}}
|
|
question={question}
|
|
questionNumber={currentQuestionIndex + 1}
|
|
totalQuestions={gameSession.questions.length}
|
|
currentResult={currentResult}
|
|
onNext={handleNext}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const Route = createFileRoute("/play")({
|
|
component: Play,
|
|
beforeLoad: async () => {
|
|
const { data: session } = await authClient.getSession();
|
|
if (!session) {
|
|
throw redirect({ to: "/login" });
|
|
}
|
|
},
|
|
});
|