Felix Zieger
commited on
Commit
·
a64b653
1
Parent(s):
b55b47d
daily challenges
Browse files- README.md +1 -1
- src/App.css +8 -2
- src/App.tsx +2 -0
- src/components/GameContainer.tsx +210 -117
- src/components/HighScoreBoard.tsx +59 -22
- src/components/admin/AdminHighScoresTable.tsx +2 -1
- src/components/admin/GameDetailsView.tsx +145 -40
- src/components/game/GameInvitation.tsx +43 -0
- src/components/game/GameReview.tsx +244 -0
- src/components/game/GuessDisplay.tsx +29 -49
- src/components/game/LanguageSelector.tsx +27 -12
- src/components/game/SentenceBuilder.tsx +8 -3
- src/components/game/WelcomeScreen.tsx +26 -12
- src/components/game/guess-display/ActionButtons.tsx +3 -13
- src/components/game/guess-display/GuessResult.tsx +6 -4
- src/components/game/leaderboard/ScoresTable.tsx +58 -11
- src/components/game/leaderboard/ThemeFilter.tsx +10 -10
- src/components/game/sentence-builder/RoundHeader.tsx +8 -6
- src/components/game/welcome/ContestSection.tsx +2 -2
- src/components/game/welcome/HuggingFaceLink.tsx +4 -4
- src/components/game/welcome/MainActions.tsx +14 -7
- src/hooks/useTranslation.ts +0 -1
- src/i18n/translations/de.ts +51 -3
- src/i18n/translations/en.ts +51 -4
- src/i18n/translations/es.ts +51 -3
- src/i18n/translations/fr.ts +52 -4
- src/i18n/translations/it.ts +50 -3
- src/integrations/supabase/types.ts +96 -2
- src/services/dailyGameService.ts +62 -0
- src/services/gameService.ts +78 -0
- supabase/config.toml +12 -0
- supabase/functions/create-session/index.ts +68 -0
- supabase/functions/generate-daily-challenge/index.ts +367 -0
- supabase/functions/generate-game/index.ts +85 -0
- supabase/functions/generate-word/index.ts +16 -11
- supabase/functions/guess-word/index.ts +14 -9
- supabase/functions/submit-high-score/index.ts +18 -6
README.md
CHANGED
@@ -3,7 +3,7 @@ title: Think in Sync
|
|
3 |
emoji: 🧠
|
4 |
colorFrom: blue
|
5 |
colorTo: pink
|
6 |
-
short_description: An addictive AI-powered word
|
7 |
sdk: docker
|
8 |
app_port: 8080
|
9 |
pinned: false
|
|
|
3 |
emoji: 🧠
|
4 |
colorFrom: blue
|
5 |
colorTo: pink
|
6 |
+
short_description: An addictive AI-powered word puzzle.
|
7 |
sdk: docker
|
8 |
app_port: 8080
|
9 |
pinned: false
|
src/App.css
CHANGED
@@ -1,10 +1,16 @@
|
|
1 |
#root {
|
2 |
max-width: 1280px;
|
3 |
margin: 0 auto;
|
4 |
-
padding:
|
5 |
text-align: center;
|
6 |
}
|
7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
.logo {
|
9 |
height: 6em;
|
10 |
padding: 1.5em;
|
@@ -39,4 +45,4 @@
|
|
39 |
|
40 |
.read-the-docs {
|
41 |
color: #888;
|
42 |
-
}
|
|
|
1 |
#root {
|
2 |
max-width: 1280px;
|
3 |
margin: 0 auto;
|
4 |
+
padding: 0.25rem;
|
5 |
text-align: center;
|
6 |
}
|
7 |
|
8 |
+
@media (min-width: 768px) {
|
9 |
+
#root {
|
10 |
+
padding: 2rem;
|
11 |
+
}
|
12 |
+
}
|
13 |
+
|
14 |
.logo {
|
15 |
height: 6em;
|
16 |
padding: 1.5em;
|
|
|
45 |
|
46 |
.read-the-docs {
|
47 |
color: #888;
|
48 |
+
}
|
src/App.tsx
CHANGED
@@ -13,6 +13,8 @@ function App() {
|
|
13 |
<Router>
|
14 |
<Routes>
|
15 |
<Route path="/" element={<Index />} />
|
|
|
|
|
16 |
<Route path="/admin" element={<AdminIndex />} />
|
17 |
<Route path="/admin/login" element={<AdminLogin />} />
|
18 |
</Routes>
|
|
|
13 |
<Router>
|
14 |
<Routes>
|
15 |
<Route path="/" element={<Index />} />
|
16 |
+
<Route path="/game" element={<Index />} />
|
17 |
+
<Route path="/game/:gameId" element={<Index />} />
|
18 |
<Route path="/admin" element={<AdminIndex />} />
|
19 |
<Route path="/admin/login" element={<AdminLogin />} />
|
20 |
</Routes>
|
src/components/GameContainer.tsx
CHANGED
@@ -1,115 +1,204 @@
|
|
1 |
import { useState, KeyboardEvent, useEffect, useContext } from "react";
|
2 |
-
import {
|
3 |
-
import { getRandomSportsWord } from "@/lib/words-sports";
|
4 |
-
import { getRandomFoodWord } from "@/lib/words-food";
|
5 |
import { motion } from "framer-motion";
|
6 |
import { generateAIResponse, guessWord } from "@/services/mistralService";
|
7 |
-
import {
|
|
|
8 |
import { useToast } from "@/components/ui/use-toast";
|
9 |
import { WelcomeScreen } from "./game/WelcomeScreen";
|
10 |
import { ThemeSelector } from "./game/ThemeSelector";
|
11 |
import { SentenceBuilder } from "./game/SentenceBuilder";
|
12 |
import { GuessDisplay } from "./game/GuessDisplay";
|
|
|
|
|
13 |
import { useTranslation } from "@/hooks/useTranslation";
|
14 |
import { LanguageContext } from "@/contexts/LanguageContext";
|
15 |
import { supabase } from "@/integrations/supabase/client";
|
|
|
16 |
|
17 |
-
type GameState = "welcome" | "theme-selection" | "building-sentence" | "showing-guess";
|
18 |
|
19 |
const normalizeWord = (word: string): string => {
|
20 |
return word.normalize('NFD')
|
21 |
.replace(/[\u0300-\u036f]/g, '')
|
22 |
.toLowerCase()
|
23 |
-
.replace(/[^a-z]/g, '')
|
24 |
.trim();
|
25 |
};
|
26 |
|
27 |
export const GameContainer = () => {
|
28 |
-
const [
|
29 |
-
const
|
|
|
|
|
|
|
|
|
|
|
30 |
const [currentTheme, setCurrentTheme] = useState<string>("standard");
|
31 |
-
const [
|
|
|
|
|
|
|
32 |
const [playerInput, setPlayerInput] = useState<string>("");
|
|
|
33 |
const [isAiThinking, setIsAiThinking] = useState(false);
|
34 |
const [aiGuess, setAiGuess] = useState<string>("");
|
35 |
const [successfulRounds, setSuccessfulRounds] = useState<number>(0);
|
36 |
-
const [
|
37 |
-
const [usedWords, setUsedWords] = useState<string[]>([]);
|
38 |
-
const [sessionId, setSessionId] = useState<string>("");
|
39 |
-
const [isHighScoreDialogOpen, setIsHighScoreDialogOpen] = useState(false);
|
40 |
const { toast } = useToast();
|
41 |
const t = useTranslation();
|
42 |
-
const { language } = useContext(LanguageContext);
|
|
|
|
|
43 |
|
44 |
useEffect(() => {
|
45 |
if (gameState === "theme-selection") {
|
46 |
-
|
|
|
|
|
|
|
47 |
}
|
48 |
}, [gameState]);
|
49 |
|
50 |
useEffect(() => {
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
}
|
63 |
-
};
|
64 |
|
65 |
-
|
66 |
-
|
67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
const handleStart = () => {
|
70 |
setGameState("theme-selection");
|
71 |
};
|
72 |
|
73 |
const handleBack = () => {
|
|
|
74 |
setGameState("welcome");
|
75 |
setSentence([]);
|
76 |
setAiGuess("");
|
77 |
-
setCurrentWord("");
|
78 |
setCurrentTheme("standard");
|
79 |
setSuccessfulRounds(0);
|
80 |
-
|
81 |
-
|
|
|
|
|
82 |
setSessionId("");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
};
|
84 |
|
85 |
const handleThemeSelect = async (theme: string) => {
|
86 |
setCurrentTheme(theme);
|
87 |
try {
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
|
|
|
|
103 |
setGameState("building-sentence");
|
104 |
setSuccessfulRounds(0);
|
105 |
-
|
106 |
-
|
107 |
-
console.log("Game started with word:", word, "theme:", theme, "language:", language);
|
108 |
} catch (error) {
|
109 |
-
console.error('Error
|
110 |
toast({
|
111 |
title: "Error",
|
112 |
-
description: "Failed to
|
113 |
variant: "destructive",
|
114 |
});
|
115 |
}
|
@@ -123,14 +212,12 @@ export const GameContainer = () => {
|
|
123 |
const newSentence = [...sentence, word];
|
124 |
setSentence(newSentence);
|
125 |
setPlayerInput("");
|
126 |
-
setTotalWords(prev => prev + 1);
|
127 |
|
128 |
setIsAiThinking(true);
|
129 |
try {
|
130 |
const aiWord = await generateAIResponse(currentWord, newSentence, language);
|
131 |
const newSentenceWithAi = [...newSentence, aiWord];
|
132 |
setSentence(newSentenceWithAi);
|
133 |
-
setTotalWords(prev => prev + 1);
|
134 |
} catch (error) {
|
135 |
console.error('Error in AI turn:', error);
|
136 |
toast({
|
@@ -143,15 +230,15 @@ export const GameContainer = () => {
|
|
143 |
}
|
144 |
};
|
145 |
|
146 |
-
const saveGameResult = async (
|
147 |
try {
|
148 |
const { error } = await supabase
|
149 |
.from('game_results')
|
150 |
.insert({
|
151 |
target_word: currentWord,
|
152 |
-
description:
|
153 |
ai_guess: aiGuess,
|
154 |
-
is_correct:
|
155 |
session_id: sessionId
|
156 |
});
|
157 |
|
@@ -173,7 +260,6 @@ export const GameContainer = () => {
|
|
173 |
finalSentence = [...sentence, playerInput.trim()];
|
174 |
setSentence(finalSentence);
|
175 |
setPlayerInput("");
|
176 |
-
setTotalWords(prev => prev + 1);
|
177 |
}
|
178 |
|
179 |
if (finalSentence.length === 0) return;
|
@@ -182,9 +268,13 @@ export const GameContainer = () => {
|
|
182 |
const guess = await guessWord(sentenceString, language);
|
183 |
setAiGuess(guess);
|
184 |
|
185 |
-
|
186 |
-
|
|
|
|
|
|
|
187 |
|
|
|
188 |
setGameState("showing-guess");
|
189 |
} catch (error) {
|
190 |
console.error('Error getting AI guess:', error);
|
@@ -199,81 +289,73 @@ export const GameContainer = () => {
|
|
199 |
};
|
200 |
|
201 |
const handleNextRound = () => {
|
202 |
-
if (
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
break;
|
216 |
-
default:
|
217 |
-
word = await getThemedWord(currentTheme, usedWords, language);
|
218 |
-
}
|
219 |
-
setCurrentWord(word);
|
220 |
-
setGameState("building-sentence");
|
221 |
-
setSentence([]);
|
222 |
-
setAiGuess("");
|
223 |
-
setUsedWords(prev => [...prev, word]);
|
224 |
-
console.log("Next round started with word:", word, "theme:", currentTheme);
|
225 |
-
} catch (error) {
|
226 |
-
console.error('Error getting new word:', error);
|
227 |
-
toast({
|
228 |
-
title: "Error",
|
229 |
-
description: "Failed to get a new word. Please try again.",
|
230 |
-
variant: "destructive",
|
231 |
-
});
|
232 |
-
}
|
233 |
-
};
|
234 |
-
getNewWord();
|
235 |
}
|
236 |
};
|
237 |
|
238 |
-
const handlePlayAgain = () => {
|
239 |
-
setGameState("theme-selection");
|
240 |
setSentence([]);
|
241 |
setAiGuess("");
|
242 |
-
setCurrentWord("");
|
243 |
-
setCurrentTheme("standard");
|
244 |
setSuccessfulRounds(0);
|
245 |
-
|
246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
247 |
};
|
248 |
|
|
|
|
|
|
|
|
|
249 |
const isGuessCorrect = () => {
|
250 |
return normalizeWord(aiGuess) === normalizeWord(currentWord);
|
251 |
};
|
252 |
|
253 |
-
const
|
254 |
-
if (isGuessCorrect()) {
|
255 |
-
setSuccessfulRounds(prev => prev + 1);
|
256 |
-
return true;
|
257 |
-
}
|
258 |
-
return false;
|
259 |
-
};
|
260 |
-
|
261 |
-
const getAverageWordsPerRound = () => {
|
262 |
if (successfulRounds === 0) return 0;
|
263 |
-
return
|
264 |
};
|
265 |
|
266 |
return (
|
267 |
-
<div className="flex min-h-screen items-center justify-center p-4">
|
268 |
<motion.div
|
269 |
initial={{ opacity: 0, y: 20 }}
|
270 |
animate={{ opacity: 1, y: 0 }}
|
271 |
-
className="w-full max-w-md rounded-xl bg-white p-8 shadow-lg"
|
272 |
>
|
273 |
{gameState === "welcome" ? (
|
274 |
-
<WelcomeScreen
|
275 |
) : gameState === "theme-selection" ? (
|
276 |
<ThemeSelector onThemeSelect={handleThemeSelect} onBack={handleBack} />
|
|
|
|
|
277 |
) : gameState === "building-sentence" ? (
|
278 |
<SentenceBuilder
|
279 |
currentWord={currentWord}
|
@@ -286,22 +368,33 @@ export const GameContainer = () => {
|
|
286 |
onMakeGuess={handleMakeGuess}
|
287 |
normalizeWord={normalizeWord}
|
288 |
onBack={handleBack}
|
|
|
289 |
/>
|
290 |
-
) : (
|
291 |
<GuessDisplay
|
292 |
sentence={sentence}
|
293 |
aiGuess={aiGuess}
|
294 |
currentWord={currentWord}
|
295 |
onNextRound={handleNextRound}
|
296 |
-
|
297 |
onBack={handleBack}
|
298 |
currentScore={successfulRounds}
|
299 |
-
avgWordsPerRound={
|
300 |
sessionId={sessionId}
|
301 |
currentTheme={currentTheme}
|
302 |
-
onHighScoreDialogChange={setIsHighScoreDialogOpen}
|
303 |
normalizeWord={normalizeWord}
|
304 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
305 |
)}
|
306 |
</motion.div>
|
307 |
</div>
|
|
|
1 |
import { useState, KeyboardEvent, useEffect, useContext } from "react";
|
2 |
+
import { useSearchParams, useParams, useNavigate, useLocation } from "react-router-dom";
|
|
|
|
|
3 |
import { motion } from "framer-motion";
|
4 |
import { generateAIResponse, guessWord } from "@/services/mistralService";
|
5 |
+
import { createGame, createSession } from "@/services/gameService";
|
6 |
+
import { getDailyGame } from "@/services/dailyGameService";
|
7 |
import { useToast } from "@/components/ui/use-toast";
|
8 |
import { WelcomeScreen } from "./game/WelcomeScreen";
|
9 |
import { ThemeSelector } from "./game/ThemeSelector";
|
10 |
import { SentenceBuilder } from "./game/SentenceBuilder";
|
11 |
import { GuessDisplay } from "./game/GuessDisplay";
|
12 |
+
import { GameReview } from "./game/GameReview";
|
13 |
+
import { GameInvitation } from "./game/GameInvitation";
|
14 |
import { useTranslation } from "@/hooks/useTranslation";
|
15 |
import { LanguageContext } from "@/contexts/LanguageContext";
|
16 |
import { supabase } from "@/integrations/supabase/client";
|
17 |
+
import { Language } from "@/i18n/translations";
|
18 |
|
19 |
+
type GameState = "welcome" | "theme-selection" | "building-sentence" | "showing-guess" | "game-review" | "invitation";
|
20 |
|
21 |
const normalizeWord = (word: string): string => {
|
22 |
return word.normalize('NFD')
|
23 |
.replace(/[\u0300-\u036f]/g, '')
|
24 |
.toLowerCase()
|
25 |
+
.replace(/[^a-z]/g, '')
|
26 |
.trim();
|
27 |
};
|
28 |
|
29 |
export const GameContainer = () => {
|
30 |
+
const [searchParams] = useSearchParams();
|
31 |
+
const { gameId: urlGameId } = useParams();
|
32 |
+
const navigate = useNavigate();
|
33 |
+
const location = useLocation();
|
34 |
+
const fromSessionParam = searchParams.get('from_session');
|
35 |
+
const [fromSession, setFromSession] = useState<string | null>(fromSessionParam);
|
36 |
+
const [gameState, setGameState] = useState<GameState>(fromSessionParam ? "invitation" : "welcome");
|
37 |
const [currentTheme, setCurrentTheme] = useState<string>("standard");
|
38 |
+
const [sessionId, setSessionId] = useState<string>("");
|
39 |
+
const [gameId, setGameId] = useState<string>("");
|
40 |
+
const [words, setWords] = useState<string[]>([]);
|
41 |
+
const [currentWordIndex, setCurrentWordIndex] = useState<number>(0);
|
42 |
const [playerInput, setPlayerInput] = useState<string>("");
|
43 |
+
const [sentence, setSentence] = useState<string[]>([]);
|
44 |
const [isAiThinking, setIsAiThinking] = useState(false);
|
45 |
const [aiGuess, setAiGuess] = useState<string>("");
|
46 |
const [successfulRounds, setSuccessfulRounds] = useState<number>(0);
|
47 |
+
const [totalWordsInSuccessfulRounds, setTotalWordsInSuccessfulRounds] = useState<number>(0);
|
|
|
|
|
|
|
48 |
const { toast } = useToast();
|
49 |
const t = useTranslation();
|
50 |
+
const { language, setLanguage } = useContext(LanguageContext);
|
51 |
+
|
52 |
+
const currentWord = words[currentWordIndex] || "";
|
53 |
|
54 |
useEffect(() => {
|
55 |
if (gameState === "theme-selection") {
|
56 |
+
setGameId("");
|
57 |
+
setSessionId("");
|
58 |
+
setWords([]);
|
59 |
+
setCurrentWordIndex(0);
|
60 |
}
|
61 |
}, [gameState]);
|
62 |
|
63 |
useEffect(() => {
|
64 |
+
if (urlGameId && !gameId) {
|
65 |
+
handleLoadGameFromUrl();
|
66 |
+
}
|
67 |
+
}, [urlGameId]);
|
68 |
+
|
69 |
+
useEffect(() => {
|
70 |
+
if (location.pathname === '/' && gameId) {
|
71 |
+
console.log("Location changed to root with active gameId, handling back navigation");
|
72 |
+
handleBack();
|
73 |
+
}
|
74 |
+
}, [location.pathname, gameId]);
|
75 |
+
|
76 |
+
const handleStartDaily = async () => {
|
77 |
+
try {
|
78 |
+
const dailyGameId = await getDailyGame(language);
|
79 |
+
handlePlayAgain(dailyGameId);
|
80 |
+
} catch (error) {
|
81 |
+
console.error('Error starting daily game:', error);
|
82 |
+
toast({
|
83 |
+
title: "Error",
|
84 |
+
description: "Failed to start the daily challenge. Please try again.",
|
85 |
+
variant: "destructive",
|
86 |
+
});
|
87 |
+
}
|
88 |
+
};
|
89 |
+
|
90 |
+
const handleLoadGameFromUrl = async () => {
|
91 |
+
if (!urlGameId) return;
|
92 |
+
|
93 |
+
try {
|
94 |
+
const { data: gameData, error: gameError } = await supabase
|
95 |
+
.from('games')
|
96 |
+
.select('theme, words, language')
|
97 |
+
.eq('id', urlGameId)
|
98 |
+
.single();
|
99 |
+
|
100 |
+
if (gameError) throw gameError;
|
101 |
+
|
102 |
+
const newSessionId = await createSession(urlGameId);
|
103 |
+
|
104 |
+
// Set the language to match the game's language
|
105 |
+
if (gameData.language) {
|
106 |
+
console.log("Setting language to match game's language:", gameData.language);
|
107 |
+
setLanguage(gameData.language as Language);
|
108 |
}
|
|
|
109 |
|
110 |
+
setCurrentTheme(gameData.theme);
|
111 |
+
setWords(gameData.words);
|
112 |
+
setCurrentWordIndex(0);
|
113 |
+
setGameId(urlGameId);
|
114 |
+
setSessionId(newSessionId);
|
115 |
+
setGameState("building-sentence");
|
116 |
+
console.log("Game started from URL with game ID:", urlGameId);
|
117 |
+
} catch (error) {
|
118 |
+
console.error('Error loading game from URL:', error);
|
119 |
+
toast({
|
120 |
+
title: "Error",
|
121 |
+
description: "Failed to load the game. Please try again.",
|
122 |
+
variant: "destructive",
|
123 |
+
});
|
124 |
+
navigate('/');
|
125 |
+
}
|
126 |
+
};
|
127 |
|
128 |
const handleStart = () => {
|
129 |
setGameState("theme-selection");
|
130 |
};
|
131 |
|
132 |
const handleBack = () => {
|
133 |
+
console.log("Handling back navigation, resetting game state");
|
134 |
setGameState("welcome");
|
135 |
setSentence([]);
|
136 |
setAiGuess("");
|
|
|
137 |
setCurrentTheme("standard");
|
138 |
setSuccessfulRounds(0);
|
139 |
+
setTotalWordsInSuccessfulRounds(0);
|
140 |
+
setWords([]);
|
141 |
+
setCurrentWordIndex(0);
|
142 |
+
setGameId("");
|
143 |
setSessionId("");
|
144 |
+
setFromSession(null);
|
145 |
+
navigate('/');
|
146 |
+
};
|
147 |
+
|
148 |
+
const handleInvitationContinue = async () => {
|
149 |
+
if (!fromSession) return;
|
150 |
+
|
151 |
+
try {
|
152 |
+
const { data: sessionData, error: sessionError } = await supabase
|
153 |
+
.from('sessions')
|
154 |
+
.select('game_id')
|
155 |
+
.eq('id', fromSession)
|
156 |
+
.single();
|
157 |
+
|
158 |
+
if (sessionError) throw sessionError;
|
159 |
+
|
160 |
+
navigate(`/game/${sessionData.game_id}`);
|
161 |
+
console.log("Redirecting to game with ID:", sessionData.game_id);
|
162 |
+
} catch (error) {
|
163 |
+
console.error('Error starting game from invitation:', error);
|
164 |
+
toast({
|
165 |
+
title: "Error",
|
166 |
+
description: "Failed to start the game. Please try again.",
|
167 |
+
variant: "destructive",
|
168 |
+
});
|
169 |
+
setGameState("welcome");
|
170 |
+
}
|
171 |
};
|
172 |
|
173 |
const handleThemeSelect = async (theme: string) => {
|
174 |
setCurrentTheme(theme);
|
175 |
try {
|
176 |
+
const newGameId = await createGame(theme, language);
|
177 |
+
const newSessionId = await createSession(newGameId);
|
178 |
+
|
179 |
+
const { data: gameData, error: gameError } = await supabase
|
180 |
+
.from('games')
|
181 |
+
.select('words')
|
182 |
+
.eq('id', newGameId)
|
183 |
+
.single();
|
184 |
+
|
185 |
+
if (gameError) throw gameError;
|
186 |
+
|
187 |
+
navigate(`/game/${newGameId}`);
|
188 |
+
|
189 |
+
setGameId(newGameId);
|
190 |
+
setSessionId(newSessionId);
|
191 |
+
setWords(gameData.words);
|
192 |
+
setCurrentWordIndex(0);
|
193 |
setGameState("building-sentence");
|
194 |
setSuccessfulRounds(0);
|
195 |
+
setTotalWordsInSuccessfulRounds(0);
|
196 |
+
console.log("Game started with theme:", theme, "language:", language);
|
|
|
197 |
} catch (error) {
|
198 |
+
console.error('Error starting new game:', error);
|
199 |
toast({
|
200 |
title: "Error",
|
201 |
+
description: "Failed to start the game. Please try again.",
|
202 |
variant: "destructive",
|
203 |
});
|
204 |
}
|
|
|
212 |
const newSentence = [...sentence, word];
|
213 |
setSentence(newSentence);
|
214 |
setPlayerInput("");
|
|
|
215 |
|
216 |
setIsAiThinking(true);
|
217 |
try {
|
218 |
const aiWord = await generateAIResponse(currentWord, newSentence, language);
|
219 |
const newSentenceWithAi = [...newSentence, aiWord];
|
220 |
setSentence(newSentenceWithAi);
|
|
|
221 |
} catch (error) {
|
222 |
console.error('Error in AI turn:', error);
|
223 |
toast({
|
|
|
230 |
}
|
231 |
};
|
232 |
|
233 |
+
const saveGameResult = async (sentenceString: string, aiGuess: string, isCorrect: boolean) => {
|
234 |
try {
|
235 |
const { error } = await supabase
|
236 |
.from('game_results')
|
237 |
.insert({
|
238 |
target_word: currentWord,
|
239 |
+
description: sentenceString,
|
240 |
ai_guess: aiGuess,
|
241 |
+
is_correct: isCorrect,
|
242 |
session_id: sessionId
|
243 |
});
|
244 |
|
|
|
260 |
finalSentence = [...sentence, playerInput.trim()];
|
261 |
setSentence(finalSentence);
|
262 |
setPlayerInput("");
|
|
|
263 |
}
|
264 |
|
265 |
if (finalSentence.length === 0) return;
|
|
|
268 |
const guess = await guessWord(sentenceString, language);
|
269 |
setAiGuess(guess);
|
270 |
|
271 |
+
const isCorrect = normalizeWord(guess) === normalizeWord(currentWord);
|
272 |
+
|
273 |
+
if (isCorrect) {
|
274 |
+
setTotalWordsInSuccessfulRounds(prev => prev + finalSentence.length);
|
275 |
+
}
|
276 |
|
277 |
+
await saveGameResult(sentenceString, guess, isCorrect);
|
278 |
setGameState("showing-guess");
|
279 |
} catch (error) {
|
280 |
console.error('Error getting AI guess:', error);
|
|
|
289 |
};
|
290 |
|
291 |
const handleNextRound = () => {
|
292 |
+
if (isGuessCorrect()) {
|
293 |
+
setSuccessfulRounds(prev => prev + 1);
|
294 |
+
if (currentWordIndex < words.length - 1) {
|
295 |
+
setCurrentWordIndex(prev => prev + 1);
|
296 |
+
setGameState("building-sentence");
|
297 |
+
setSentence([]);
|
298 |
+
setAiGuess("");
|
299 |
+
console.log("Next round started with word:", words[currentWordIndex + 1]);
|
300 |
+
} else {
|
301 |
+
handleGameReview();
|
302 |
+
}
|
303 |
+
} else {
|
304 |
+
setGameState("game-review");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
305 |
}
|
306 |
};
|
307 |
|
308 |
+
const handlePlayAgain = (gameId?: string, fromSession?: string) => {
|
|
|
309 |
setSentence([]);
|
310 |
setAiGuess("");
|
|
|
|
|
311 |
setSuccessfulRounds(0);
|
312 |
+
setTotalWordsInSuccessfulRounds(0);
|
313 |
+
setWords([]);
|
314 |
+
setCurrentWordIndex(0);
|
315 |
+
setSessionId("");
|
316 |
+
if (fromSession) {
|
317 |
+
setFromSession(fromSession);
|
318 |
+
} else {
|
319 |
+
setFromSession(null);
|
320 |
+
}
|
321 |
+
if (gameId) {
|
322 |
+
navigate(`/game/${gameId}`);
|
323 |
+
handleLoadGameFromUrl()
|
324 |
+
}
|
325 |
+
else {
|
326 |
+
setGameState("theme-selection");
|
327 |
+
setCurrentTheme("standard");
|
328 |
+
setGameId("");
|
329 |
+
navigate(`/`);
|
330 |
+
}
|
331 |
};
|
332 |
|
333 |
+
const handleGameReview = () => {
|
334 |
+
setGameState("game-review");
|
335 |
+
}
|
336 |
+
|
337 |
const isGuessCorrect = () => {
|
338 |
return normalizeWord(aiGuess) === normalizeWord(currentWord);
|
339 |
};
|
340 |
|
341 |
+
const getAverageWordsPerSuccessfulRound = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
342 |
if (successfulRounds === 0) return 0;
|
343 |
+
return totalWordsInSuccessfulRounds / successfulRounds;
|
344 |
};
|
345 |
|
346 |
return (
|
347 |
+
<div className="flex min-h-screen items-center justify-center p-1 md:p-4">
|
348 |
<motion.div
|
349 |
initial={{ opacity: 0, y: 20 }}
|
350 |
animate={{ opacity: 1, y: 0 }}
|
351 |
+
className="w-full md:max-w-md rounded-none md:rounded-xl bg-transparent md:bg-white p-4 md:p-8 md:shadow-lg"
|
352 |
>
|
353 |
{gameState === "welcome" ? (
|
354 |
+
<WelcomeScreen onStartDaily={handleStartDaily} onStartNew={handleStart} />
|
355 |
) : gameState === "theme-selection" ? (
|
356 |
<ThemeSelector onThemeSelect={handleThemeSelect} onBack={handleBack} />
|
357 |
+
) : gameState === "invitation" ? (
|
358 |
+
<GameInvitation onContinue={handleInvitationContinue} onBack={handleBack} />
|
359 |
) : gameState === "building-sentence" ? (
|
360 |
<SentenceBuilder
|
361 |
currentWord={currentWord}
|
|
|
368 |
onMakeGuess={handleMakeGuess}
|
369 |
normalizeWord={normalizeWord}
|
370 |
onBack={handleBack}
|
371 |
+
onClose={handleBack}
|
372 |
/>
|
373 |
+
) : gameState === "showing-guess" ? (
|
374 |
<GuessDisplay
|
375 |
sentence={sentence}
|
376 |
aiGuess={aiGuess}
|
377 |
currentWord={currentWord}
|
378 |
onNextRound={handleNextRound}
|
379 |
+
onGameReview={handleGameReview}
|
380 |
onBack={handleBack}
|
381 |
currentScore={successfulRounds}
|
382 |
+
avgWordsPerRound={getAverageWordsPerSuccessfulRound()}
|
383 |
sessionId={sessionId}
|
384 |
currentTheme={currentTheme}
|
|
|
385 |
normalizeWord={normalizeWord}
|
386 |
/>
|
387 |
+
) : (
|
388 |
+
<GameReview
|
389 |
+
currentScore={successfulRounds}
|
390 |
+
avgWordsPerRound={getAverageWordsPerSuccessfulRound()}
|
391 |
+
onPlayAgain={handlePlayAgain}
|
392 |
+
onBack={handleBack}
|
393 |
+
gameId={gameId}
|
394 |
+
sessionId={sessionId}
|
395 |
+
currentTheme={currentTheme}
|
396 |
+
fromSession={fromSession}
|
397 |
+
/>
|
398 |
)}
|
399 |
</motion.div>
|
400 |
</div>
|
src/components/HighScoreBoard.tsx
CHANGED
@@ -8,6 +8,8 @@ import { ScoreSubmissionForm } from "./game/leaderboard/ScoreSubmissionForm";
|
|
8 |
import { ScoresTable } from "./game/leaderboard/ScoresTable";
|
9 |
import { LeaderboardHeader } from "./game/leaderboard/LeaderboardHeader";
|
10 |
import { LeaderboardPagination } from "./game/leaderboard/LeaderboardPagination";
|
|
|
|
|
11 |
|
12 |
interface HighScore {
|
13 |
id: string;
|
@@ -17,13 +19,17 @@ interface HighScore {
|
|
17 |
created_at: string;
|
18 |
session_id: string;
|
19 |
theme: string;
|
|
|
|
|
|
|
|
|
20 |
}
|
21 |
|
22 |
interface HighScoreBoardProps {
|
23 |
currentScore?: number;
|
24 |
avgWordsPerRound?: number;
|
25 |
onClose?: () => void;
|
26 |
-
|
27 |
sessionId?: string;
|
28 |
onScoreSubmitted?: () => void;
|
29 |
showThemeFilter?: boolean;
|
@@ -31,12 +37,12 @@ interface HighScoreBoardProps {
|
|
31 |
}
|
32 |
|
33 |
const ITEMS_PER_PAGE = 5;
|
34 |
-
const STANDARD_THEMES = ['standard', 'sports', 'food'];
|
35 |
|
36 |
export const HighScoreBoard = ({
|
37 |
currentScore = 0,
|
38 |
avgWordsPerRound = 0,
|
39 |
onClose,
|
|
|
40 |
sessionId = "",
|
41 |
onScoreSubmitted,
|
42 |
showThemeFilter = true,
|
@@ -46,30 +52,32 @@ export const HighScoreBoard = ({
|
|
46 |
const [isSubmitting, setIsSubmitting] = useState(false);
|
47 |
const [hasSubmitted, setHasSubmitted] = useState(false);
|
48 |
const [currentPage, setCurrentPage] = useState(1);
|
49 |
-
const [
|
50 |
-
initialTheme as 'standard' | 'sports' | 'food' | 'custom'
|
51 |
-
);
|
52 |
const { toast } = useToast();
|
53 |
const t = useTranslation();
|
54 |
const queryClient = useQueryClient();
|
|
|
55 |
|
56 |
const showScoreInfo = sessionId !== "" && currentScore > 0;
|
57 |
|
58 |
const { data: highScores } = useQuery({
|
59 |
-
queryKey: ["highScores",
|
60 |
queryFn: async () => {
|
61 |
-
console.log("Fetching high scores for
|
62 |
let query = supabase
|
63 |
.from("high_scores")
|
64 |
-
.select("
|
65 |
.order("score", { ascending: false })
|
66 |
.order("avg_words_per_round", { ascending: true });
|
67 |
|
68 |
-
if (
|
69 |
-
|
70 |
-
|
71 |
-
} else {
|
72 |
-
|
|
|
|
|
|
|
73 |
}
|
74 |
|
75 |
const { data, error } = await query;
|
@@ -120,7 +128,8 @@ export const HighScoreBoard = ({
|
|
120 |
score: currentScore,
|
121 |
avgWordsPerRound,
|
122 |
sessionId,
|
123 |
-
theme:
|
|
|
124 |
}
|
125 |
});
|
126 |
|
@@ -130,13 +139,13 @@ export const HighScoreBoard = ({
|
|
130 |
}
|
131 |
|
132 |
console.log("Score submitted successfully:", data);
|
133 |
-
|
134 |
if (data.success) {
|
135 |
toast({
|
136 |
-
title: t.leaderboard.
|
137 |
-
description: t.leaderboard.
|
138 |
});
|
139 |
-
|
140 |
setHasSubmitted(true);
|
141 |
onScoreSubmitted?.();
|
142 |
setPlayerName("");
|
@@ -161,6 +170,32 @@ export const HighScoreBoard = ({
|
|
161 |
}
|
162 |
};
|
163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
const totalPages = highScores ? Math.ceil(highScores.length / ITEMS_PER_PAGE) : 0;
|
165 |
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
166 |
const paginatedScores = highScores?.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
@@ -173,10 +208,10 @@ export const HighScoreBoard = ({
|
|
173 |
showScoreInfo={showScoreInfo}
|
174 |
/>
|
175 |
|
176 |
-
{showThemeFilter && (
|
177 |
<ThemeFilter
|
178 |
-
|
179 |
-
|
180 |
/>
|
181 |
)}
|
182 |
|
@@ -194,7 +229,9 @@ export const HighScoreBoard = ({
|
|
194 |
<ScoresTable
|
195 |
scores={paginatedScores || []}
|
196 |
startIndex={startIndex}
|
197 |
-
showThemeColumn={
|
|
|
|
|
198 |
/>
|
199 |
|
200 |
<LeaderboardPagination
|
|
|
8 |
import { ScoresTable } from "./game/leaderboard/ScoresTable";
|
9 |
import { LeaderboardHeader } from "./game/leaderboard/LeaderboardHeader";
|
10 |
import { LeaderboardPagination } from "./game/leaderboard/LeaderboardPagination";
|
11 |
+
import { getDailyGames } from "@/services/dailyGameService";
|
12 |
+
import { useNavigate } from "react-router-dom";
|
13 |
|
14 |
interface HighScore {
|
15 |
id: string;
|
|
|
19 |
created_at: string;
|
20 |
session_id: string;
|
21 |
theme: string;
|
22 |
+
game?: {
|
23 |
+
language: string;
|
24 |
+
};
|
25 |
+
game_id?: string;
|
26 |
}
|
27 |
|
28 |
interface HighScoreBoardProps {
|
29 |
currentScore?: number;
|
30 |
avgWordsPerRound?: number;
|
31 |
onClose?: () => void;
|
32 |
+
gameId?: string;
|
33 |
sessionId?: string;
|
34 |
onScoreSubmitted?: () => void;
|
35 |
showThemeFilter?: boolean;
|
|
|
37 |
}
|
38 |
|
39 |
const ITEMS_PER_PAGE = 5;
|
|
|
40 |
|
41 |
export const HighScoreBoard = ({
|
42 |
currentScore = 0,
|
43 |
avgWordsPerRound = 0,
|
44 |
onClose,
|
45 |
+
gameId = "",
|
46 |
sessionId = "",
|
47 |
onScoreSubmitted,
|
48 |
showThemeFilter = true,
|
|
|
52 |
const [isSubmitting, setIsSubmitting] = useState(false);
|
53 |
const [hasSubmitted, setHasSubmitted] = useState(false);
|
54 |
const [currentPage, setCurrentPage] = useState(1);
|
55 |
+
const [selectedMode, setSelectedMode] = useState<'daily' | 'all-time'>('daily');
|
|
|
|
|
56 |
const { toast } = useToast();
|
57 |
const t = useTranslation();
|
58 |
const queryClient = useQueryClient();
|
59 |
+
const navigate = useNavigate();
|
60 |
|
61 |
const showScoreInfo = sessionId !== "" && currentScore > 0;
|
62 |
|
63 |
const { data: highScores } = useQuery({
|
64 |
+
queryKey: ["highScores", selectedMode, gameId],
|
65 |
queryFn: async () => {
|
66 |
+
console.log("Fetching high scores for mode:", selectedMode, "gameId:", gameId);
|
67 |
let query = supabase
|
68 |
.from("high_scores")
|
69 |
+
.select("*, game:games(language)")
|
70 |
.order("score", { ascending: false })
|
71 |
.order("avg_words_per_round", { ascending: true });
|
72 |
|
73 |
+
if (gameId) {
|
74 |
+
query = query.eq('game_id', gameId);
|
75 |
+
console.log("Filtering scores by game_id:", gameId);
|
76 |
+
} else if (selectedMode === 'daily') {
|
77 |
+
const dailyGames = await getDailyGames();
|
78 |
+
const dailyGameIds = dailyGames.map(game => game.game_id);
|
79 |
+
query = query.in('game_id', dailyGameIds);
|
80 |
+
console.log("Filtering scores by daily game_ids:", dailyGameIds);
|
81 |
}
|
82 |
|
83 |
const { data, error } = await query;
|
|
|
128 |
score: currentScore,
|
129 |
avgWordsPerRound,
|
130 |
sessionId,
|
131 |
+
theme: initialTheme,
|
132 |
+
gameId
|
133 |
}
|
134 |
});
|
135 |
|
|
|
139 |
}
|
140 |
|
141 |
console.log("Score submitted successfully:", data);
|
142 |
+
|
143 |
if (data.success) {
|
144 |
toast({
|
145 |
+
title: data.isUpdate ? t.leaderboard.scoreUpdated : t.leaderboard.scoreSubmitted,
|
146 |
+
description: data.isUpdate ? t.leaderboard.scoreUpdatedDesc : t.leaderboard.scoreSubmittedDesc,
|
147 |
});
|
148 |
+
|
149 |
setHasSubmitted(true);
|
150 |
onScoreSubmitted?.();
|
151 |
setPlayerName("");
|
|
|
170 |
}
|
171 |
};
|
172 |
|
173 |
+
const handlePlayGame = async (gameId: string) => {
|
174 |
+
try {
|
175 |
+
console.log("Creating new session for game:", gameId);
|
176 |
+
const { data: session, error } = await supabase
|
177 |
+
.from('sessions')
|
178 |
+
.insert({
|
179 |
+
game_id: gameId
|
180 |
+
})
|
181 |
+
.select()
|
182 |
+
.single();
|
183 |
+
|
184 |
+
if (error) throw error;
|
185 |
+
|
186 |
+
console.log("Session created:", session);
|
187 |
+
navigate(`/game/${gameId}`);
|
188 |
+
onClose?.();
|
189 |
+
} catch (error) {
|
190 |
+
console.error('Error creating session:', error);
|
191 |
+
toast({
|
192 |
+
title: t.game.error.title,
|
193 |
+
description: t.game.error.description,
|
194 |
+
variant: "destructive",
|
195 |
+
});
|
196 |
+
}
|
197 |
+
};
|
198 |
+
|
199 |
const totalPages = highScores ? Math.ceil(highScores.length / ITEMS_PER_PAGE) : 0;
|
200 |
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
201 |
const paginatedScores = highScores?.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
|
|
208 |
showScoreInfo={showScoreInfo}
|
209 |
/>
|
210 |
|
211 |
+
{showThemeFilter && !gameId && (
|
212 |
<ThemeFilter
|
213 |
+
selectedMode={selectedMode}
|
214 |
+
onModeChange={setSelectedMode}
|
215 |
/>
|
216 |
)}
|
217 |
|
|
|
229 |
<ScoresTable
|
230 |
scores={paginatedScores || []}
|
231 |
startIndex={startIndex}
|
232 |
+
showThemeColumn={selectedMode === 'daily'}
|
233 |
+
onPlayGame={handlePlayGame}
|
234 |
+
selectedMode={selectedMode}
|
235 |
/>
|
236 |
|
237 |
<LeaderboardPagination
|
src/components/admin/AdminHighScoresTable.tsx
CHANGED
@@ -27,6 +27,7 @@ interface HighScoreWithGames {
|
|
27 |
session_id: string;
|
28 |
theme: string;
|
29 |
game_results: {
|
|
|
30 |
target_word: string;
|
31 |
description: string;
|
32 |
ai_guess: string;
|
@@ -55,7 +56,7 @@ export const AdminHighScoresTable = () => {
|
|
55 |
highScoresData.map(async (score) => {
|
56 |
const { data: gameResults, error: gameResultsError } = await supabase
|
57 |
.from("game_results")
|
58 |
-
.select("target_word, description, ai_guess, is_correct")
|
59 |
.eq("session_id", score.session_id);
|
60 |
|
61 |
if (gameResultsError) {
|
|
|
27 |
session_id: string;
|
28 |
theme: string;
|
29 |
game_results: {
|
30 |
+
id: string;
|
31 |
target_word: string;
|
32 |
description: string;
|
33 |
ai_guess: string;
|
|
|
56 |
highScoresData.map(async (score) => {
|
57 |
const { data: gameResults, error: gameResultsError } = await supabase
|
58 |
.from("game_results")
|
59 |
+
.select("id, target_word, description, ai_guess, is_correct")
|
60 |
.eq("session_id", score.session_id);
|
61 |
|
62 |
if (gameResultsError) {
|
src/components/admin/GameDetailsView.tsx
CHANGED
@@ -1,55 +1,160 @@
|
|
|
|
|
|
|
|
1 |
import {
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
} from "@/components/
|
9 |
-
import { Check, X } from "lucide-react";
|
10 |
|
11 |
interface GameResult {
|
|
|
12 |
target_word: string;
|
13 |
description: string;
|
14 |
ai_guess: string;
|
15 |
is_correct: boolean;
|
16 |
}
|
17 |
|
18 |
-
interface
|
19 |
-
|
|
|
|
|
|
|
20 |
}
|
21 |
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
return (
|
24 |
-
<div className="
|
25 |
-
<
|
26 |
-
<
|
27 |
-
<
|
28 |
-
<
|
29 |
-
|
30 |
-
|
31 |
-
<
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
</
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
)}
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
</div>
|
54 |
);
|
55 |
-
};
|
|
|
1 |
+
import { useState, useEffect } from "react";
|
2 |
+
import { supabase } from "@/integrations/supabase/client";
|
3 |
+
import { Eye } from "lucide-react";
|
4 |
import {
|
5 |
+
Dialog,
|
6 |
+
DialogContent,
|
7 |
+
DialogHeader,
|
8 |
+
DialogTitle,
|
9 |
+
} from "@/components/ui/dialog";
|
10 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
11 |
+
import { GuessDescription } from "@/components/game/guess-display/GuessDescription";
|
|
|
12 |
|
13 |
interface GameResult {
|
14 |
+
id: string;
|
15 |
target_word: string;
|
16 |
description: string;
|
17 |
ai_guess: string;
|
18 |
is_correct: boolean;
|
19 |
}
|
20 |
|
21 |
+
interface ComparisonDialogProps {
|
22 |
+
isOpen: boolean;
|
23 |
+
onClose: () => void;
|
24 |
+
currentResult: GameResult | null;
|
25 |
+
friendResult: GameResult | null;
|
26 |
}
|
27 |
|
28 |
+
const ComparisonDialog = ({ isOpen, onClose, currentResult, friendResult }: ComparisonDialogProps) => {
|
29 |
+
const t = useTranslation();
|
30 |
+
|
31 |
+
return (
|
32 |
+
<Dialog open={isOpen} onOpenChange={onClose}>
|
33 |
+
<DialogContent>
|
34 |
+
<DialogHeader>
|
35 |
+
<DialogTitle>
|
36 |
+
{currentResult?.target_word}
|
37 |
+
</DialogTitle>
|
38 |
+
</DialogHeader>
|
39 |
+
<div className="space-y-6 mt-4">
|
40 |
+
<div>
|
41 |
+
{friendResult && (
|
42 |
+
<h3 className="font-semibold mb-2">{t.game.review.yourDescription}</h3>
|
43 |
+
)}
|
44 |
+
<GuessDescription
|
45 |
+
sentence={currentResult?.description?.split(' ') || []}
|
46 |
+
aiGuess={currentResult?.ai_guess || ''}
|
47 |
+
/>
|
48 |
+
<p className="text-sm text-gray-600 mt-2">
|
49 |
+
{t.guess.aiGuessedDescription}: <span className="font-medium">{currentResult?.ai_guess}</span>
|
50 |
+
</p>
|
51 |
+
</div>
|
52 |
+
{friendResult && (
|
53 |
+
<div>
|
54 |
+
<h3 className="font-semibold mb-2">{t.game.review.friendDescription}</h3>
|
55 |
+
<GuessDescription
|
56 |
+
sentence={friendResult.description?.split(' ') || []}
|
57 |
+
aiGuess={friendResult.ai_guess || ''}
|
58 |
+
/>
|
59 |
+
<p className="text-sm text-gray-600 mt-2">
|
60 |
+
{t.guess.aiGuessedDescription}: <span className="font-medium">{friendResult.ai_guess}</span>
|
61 |
+
</p>
|
62 |
+
</div>
|
63 |
+
)}
|
64 |
+
</div>
|
65 |
+
</DialogContent>
|
66 |
+
</Dialog>
|
67 |
+
);
|
68 |
+
};
|
69 |
+
|
70 |
+
export const GameDetailsView = ({ gameResults = [], fromSession }: { gameResults: GameResult[], fromSession?: string | null }) => {
|
71 |
+
const [friendResults, setFriendResults] = useState<GameResult[]>([]);
|
72 |
+
const [selectedResult, setSelectedResult] = useState<GameResult | null>(null);
|
73 |
+
const t = useTranslation();
|
74 |
+
|
75 |
+
useEffect(() => {
|
76 |
+
const fetchFriendResults = async () => {
|
77 |
+
if (!fromSession) return;
|
78 |
+
|
79 |
+
const { data, error } = await supabase
|
80 |
+
.from('game_results')
|
81 |
+
.select('*')
|
82 |
+
.eq('session_id', fromSession)
|
83 |
+
.order('created_at', { ascending: true });
|
84 |
+
|
85 |
+
if (!error && data) {
|
86 |
+
console.log('Friend results:', data);
|
87 |
+
setFriendResults(data);
|
88 |
+
}
|
89 |
+
};
|
90 |
+
|
91 |
+
fetchFriendResults();
|
92 |
+
}, [fromSession]);
|
93 |
+
|
94 |
+
const getFriendResult = (targetWord: string) => {
|
95 |
+
return friendResults.find(r => r.target_word === targetWord) || null;
|
96 |
+
};
|
97 |
+
|
98 |
+
const getWordCount = (description?: string) => {
|
99 |
+
return description?.split(' ').length || 0;
|
100 |
+
};
|
101 |
+
|
102 |
return (
|
103 |
+
<div className="relative overflow-x-auto rounded-lg border">
|
104 |
+
<table className="w-full text-sm text-left">
|
105 |
+
<thead className="text-xs uppercase bg-gray-50">
|
106 |
+
<tr>
|
107 |
+
<th className="px-6 py-3">
|
108 |
+
{t.game.round}
|
109 |
+
</th>
|
110 |
+
<th className="px-6 py-3">
|
111 |
+
{friendResults.length > 0 ? t.game.review.yourWords : t.game.review.words}
|
112 |
+
</th>
|
113 |
+
{friendResults.length > 0 && (
|
114 |
+
<th className="px-6 py-3">
|
115 |
+
{t.game.review.friendWords}
|
116 |
+
</th>
|
117 |
+
)}
|
118 |
+
<th className="px-6 py-3">
|
119 |
+
<span className="sr-only">{t.game.review.details}</span>
|
120 |
+
</th>
|
121 |
+
</tr>
|
122 |
+
</thead>
|
123 |
+
<tbody>
|
124 |
+
{gameResults.map((result) => {
|
125 |
+
const friendResult = getFriendResult(result.target_word);
|
126 |
+
return (
|
127 |
+
<tr
|
128 |
+
key={result.id}
|
129 |
+
className="bg-white border-b hover:bg-gray-50 cursor-pointer"
|
130 |
+
onClick={() => setSelectedResult(result)}
|
131 |
+
>
|
132 |
+
<td className="px-6 py-4 font-medium">
|
133 |
+
{result.target_word}
|
134 |
+
</td>
|
135 |
+
<td className="px-6 py-4">
|
136 |
+
{result.is_correct ? '✅' : '❌'} {getWordCount(result.description)}
|
137 |
+
</td>
|
138 |
+
{friendResults.length > 0 && (
|
139 |
+
<td className="px-6 py-4">
|
140 |
+
{friendResult ? `${friendResult.is_correct ? '✅' : '❌'} ${getWordCount(friendResult.description)}` : '-'}
|
141 |
+
</td>
|
142 |
)}
|
143 |
+
<td className="px-6 py-4">
|
144 |
+
<Eye className="h-4 w-4 text-gray-500" />
|
145 |
+
</td>
|
146 |
+
</tr>
|
147 |
+
);
|
148 |
+
})}
|
149 |
+
</tbody>
|
150 |
+
</table>
|
151 |
+
|
152 |
+
<ComparisonDialog
|
153 |
+
isOpen={!!selectedResult}
|
154 |
+
onClose={() => setSelectedResult(null)}
|
155 |
+
currentResult={selectedResult}
|
156 |
+
friendResult={selectedResult ? getFriendResult(selectedResult.target_word) : null}
|
157 |
+
/>
|
158 |
</div>
|
159 |
);
|
160 |
+
};
|
src/components/game/GameInvitation.tsx
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Button } from "@/components/ui/button";
|
2 |
+
import { motion } from "framer-motion";
|
3 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
4 |
+
import { ArrowLeft } from "lucide-react";
|
5 |
+
|
6 |
+
interface GameInvitationProps {
|
7 |
+
onContinue: () => void;
|
8 |
+
onBack: () => void;
|
9 |
+
}
|
10 |
+
|
11 |
+
export const GameInvitation = ({ onContinue, onBack }: GameInvitationProps) => {
|
12 |
+
const t = useTranslation();
|
13 |
+
|
14 |
+
return (
|
15 |
+
<motion.div
|
16 |
+
initial={{ opacity: 0 }}
|
17 |
+
animate={{ opacity: 1 }}
|
18 |
+
className="space-y-6"
|
19 |
+
>
|
20 |
+
<div className="flex items-center justify-between mb-4">
|
21 |
+
<Button
|
22 |
+
variant="ghost"
|
23 |
+
size="icon"
|
24 |
+
onClick={onBack}
|
25 |
+
className="hover:bg-gray-100"
|
26 |
+
>
|
27 |
+
<ArrowLeft className="h-4 w-4" />
|
28 |
+
</Button>
|
29 |
+
<h2 className="text-2xl font-bold text-gray-900">{t.game.invitation.title}</h2>
|
30 |
+
<div className="w-8" /> {/* Spacer for centering */}
|
31 |
+
</div>
|
32 |
+
|
33 |
+
<p className="text-gray-600 text-center">{t.game.invitation.description}</p>
|
34 |
+
|
35 |
+
<Button
|
36 |
+
onClick={onContinue}
|
37 |
+
className="w-full"
|
38 |
+
>
|
39 |
+
{t.themes.continue} ⏎
|
40 |
+
</Button>
|
41 |
+
</motion.div>
|
42 |
+
);
|
43 |
+
};
|
src/components/game/GameReview.tsx
ADDED
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from "react";
|
2 |
+
import { motion } from "framer-motion";
|
3 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
4 |
+
import { Button } from "@/components/ui/button";
|
5 |
+
import { Input } from "@/components/ui/input";
|
6 |
+
import { Copy } from "lucide-react";
|
7 |
+
import {
|
8 |
+
Dialog,
|
9 |
+
DialogContent,
|
10 |
+
DialogTrigger,
|
11 |
+
} from "@/components/ui/dialog";
|
12 |
+
import { HighScoreBoard } from "@/components/HighScoreBoard";
|
13 |
+
import { GameDetailsView } from "@/components/admin/GameDetailsView";
|
14 |
+
import { supabase } from "@/integrations/supabase/client";
|
15 |
+
import { useToast } from "@/components/ui/use-toast";
|
16 |
+
import { useSearchParams, useNavigate } from "react-router-dom";
|
17 |
+
import { RoundHeader } from "./sentence-builder/RoundHeader";
|
18 |
+
|
19 |
+
interface GameReviewProps {
|
20 |
+
currentScore: number;
|
21 |
+
avgWordsPerRound: number;
|
22 |
+
onPlayAgain: (game_id?: string, fromSession?: string) => void;
|
23 |
+
onBack?: () => void;
|
24 |
+
gameId?: string;
|
25 |
+
sessionId: string;
|
26 |
+
currentTheme: string;
|
27 |
+
fromSession?: string | null;
|
28 |
+
}
|
29 |
+
|
30 |
+
export const GameReview = ({
|
31 |
+
currentScore,
|
32 |
+
avgWordsPerRound,
|
33 |
+
onPlayAgain,
|
34 |
+
onBack,
|
35 |
+
gameId,
|
36 |
+
sessionId,
|
37 |
+
currentTheme,
|
38 |
+
fromSession,
|
39 |
+
}: GameReviewProps) => {
|
40 |
+
const t = useTranslation();
|
41 |
+
const { toast } = useToast();
|
42 |
+
const [searchParams] = useSearchParams();
|
43 |
+
const navigate = useNavigate();
|
44 |
+
const [showHighScores, setShowHighScores] = useState(false);
|
45 |
+
const [gameResults, setGameResults] = useState([]);
|
46 |
+
const [friendData, setFriendData] = useState<{ score: number; avgWords: number } | null>(null);
|
47 |
+
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
48 |
+
const shareUrl = `${window.location.origin}/?from_session=${sessionId}`;
|
49 |
+
|
50 |
+
useEffect(() => {
|
51 |
+
const fetchGameResults = async () => {
|
52 |
+
const { data, error } = await supabase
|
53 |
+
.from('game_results')
|
54 |
+
.select('*')
|
55 |
+
.eq('session_id', sessionId)
|
56 |
+
.order('created_at', { ascending: true });
|
57 |
+
|
58 |
+
if (!error && data) {
|
59 |
+
setGameResults(data);
|
60 |
+
}
|
61 |
+
};
|
62 |
+
|
63 |
+
const fetchFriendResults = async () => {
|
64 |
+
if (!fromSession) return;
|
65 |
+
|
66 |
+
const { data: friendResults, error } = await supabase
|
67 |
+
.from('game_results')
|
68 |
+
.select('target_word, is_correct, description, ai_guess')
|
69 |
+
.eq('session_id', fromSession);
|
70 |
+
|
71 |
+
if (error) {
|
72 |
+
console.error('Error fetching friend results:', error);
|
73 |
+
return;
|
74 |
+
}
|
75 |
+
|
76 |
+
if (friendResults) {
|
77 |
+
const successfulRounds = friendResults.filter(r => r.is_correct).length;
|
78 |
+
const totalWords = friendResults.reduce((acc, r) => acc + (r.description?.split(' ').length || 0), 0);
|
79 |
+
const avgWords = successfulRounds > 0 ? totalWords / successfulRounds : 0;
|
80 |
+
|
81 |
+
setFriendData({
|
82 |
+
score: successfulRounds,
|
83 |
+
avgWords: avgWords
|
84 |
+
});
|
85 |
+
}
|
86 |
+
};
|
87 |
+
|
88 |
+
fetchGameResults();
|
89 |
+
if (fromSession) {
|
90 |
+
fetchFriendResults();
|
91 |
+
}
|
92 |
+
}, [sessionId, fromSession]);
|
93 |
+
|
94 |
+
useEffect(() => {
|
95 |
+
const handleKeyPress = (e: KeyboardEvent) => {
|
96 |
+
// Only handle Enter key if high scores dialog is not open
|
97 |
+
if (e.key === 'Enter' && !showHighScores) {
|
98 |
+
handlePlayAgain();
|
99 |
+
}
|
100 |
+
};
|
101 |
+
|
102 |
+
window.addEventListener('keydown', handleKeyPress);
|
103 |
+
return () => window.removeEventListener('keydown', handleKeyPress);
|
104 |
+
}, [showHighScores]); // Add showHighScores to dependencies
|
105 |
+
|
106 |
+
const handleCopyUrl = async () => {
|
107 |
+
try {
|
108 |
+
await navigator.clipboard.writeText(shareUrl);
|
109 |
+
toast({
|
110 |
+
title: t.game.review.urlCopied,
|
111 |
+
description: t.game.review.urlCopiedDesc,
|
112 |
+
});
|
113 |
+
} catch (err) {
|
114 |
+
console.error('Failed to copy URL:', err);
|
115 |
+
toast({
|
116 |
+
title: t.game.review.urlCopyError,
|
117 |
+
description: t.game.review.urlCopyErrorDesc,
|
118 |
+
variant: "destructive",
|
119 |
+
});
|
120 |
+
}
|
121 |
+
};
|
122 |
+
|
123 |
+
const handlePlayAgain = async () => {
|
124 |
+
try {
|
125 |
+
const { data: session, error } = await supabase
|
126 |
+
.from('sessions')
|
127 |
+
.insert({
|
128 |
+
game_id: gameId
|
129 |
+
})
|
130 |
+
.select()
|
131 |
+
.single();
|
132 |
+
|
133 |
+
if (error) throw error;
|
134 |
+
|
135 |
+
onPlayAgain(gameId, fromSession);
|
136 |
+
} catch (error) {
|
137 |
+
console.error('Error creating new session:', error);
|
138 |
+
toast({
|
139 |
+
title: "Error",
|
140 |
+
description: "Failed to restart the game. Please try again.",
|
141 |
+
variant: "destructive",
|
142 |
+
});
|
143 |
+
}
|
144 |
+
};
|
145 |
+
|
146 |
+
const handlePlayNewWords = async () => {
|
147 |
+
onPlayAgain();
|
148 |
+
};
|
149 |
+
|
150 |
+
const renderComparisonResult = () => {
|
151 |
+
if (!friendData) return null;
|
152 |
+
|
153 |
+
const didWin = currentScore > friendData.score ||
|
154 |
+
(currentScore === friendData.score && avgWordsPerRound < friendData.avgWords);
|
155 |
+
|
156 |
+
return (
|
157 |
+
<div className="space-y-4 mt-4">
|
158 |
+
<p className="text-xl font-bold">
|
159 |
+
{didWin ? `${t.game.review.youWin} 🎉` : `${t.game.review.youLost} 🧘`}
|
160 |
+
</p>
|
161 |
+
<p className="text-sm text-gray-600">
|
162 |
+
{t.game.review.friendScore(friendData.score, friendData.avgWords.toFixed(1))}
|
163 |
+
</p>
|
164 |
+
</div>
|
165 |
+
);
|
166 |
+
};
|
167 |
+
|
168 |
+
return (
|
169 |
+
<motion.div
|
170 |
+
initial={{ opacity: 0 }}
|
171 |
+
animate={{ opacity: 1 }}
|
172 |
+
className="text-center space-y-6"
|
173 |
+
>
|
174 |
+
<RoundHeader
|
175 |
+
successfulRounds={currentScore}
|
176 |
+
onBack={onBack}
|
177 |
+
showConfirmDialog={showConfirmDialog}
|
178 |
+
setShowConfirmDialog={setShowConfirmDialog}
|
179 |
+
onCancel={() => setShowConfirmDialog(false)}
|
180 |
+
/>
|
181 |
+
|
182 |
+
<div className="space-y-4">
|
183 |
+
<div className="bg-gray-100 p-4 rounded-lg">
|
184 |
+
<p className="text-lg">
|
185 |
+
{t.game.review.successfulRounds}: <span className="font-bold">{currentScore}</span>
|
186 |
+
</p>
|
187 |
+
<p className="text-sm text-gray-600">
|
188 |
+
{t.leaderboard.wordsPerRound}: {avgWordsPerRound.toFixed(1)}
|
189 |
+
</p>
|
190 |
+
{renderComparisonResult()}
|
191 |
+
</div>
|
192 |
+
|
193 |
+
<GameDetailsView gameResults={gameResults} fromSession={fromSession} />
|
194 |
+
|
195 |
+
<div className="relative items-center bg-gray-100 p-4 rounded-lg">
|
196 |
+
<p className="text-sm">{t.game.review.urlCopiedDesc}</p>
|
197 |
+
<div className="relative flex items-center p-4 rounded-lg">
|
198 |
+
<Input
|
199 |
+
value={shareUrl}
|
200 |
+
readOnly
|
201 |
+
className="pr-10"
|
202 |
+
/>
|
203 |
+
<Button
|
204 |
+
variant="ghost"
|
205 |
+
size="icon"
|
206 |
+
onClick={handleCopyUrl}
|
207 |
+
className="absolute right-6"
|
208 |
+
>
|
209 |
+
<Copy className="h-4 w-4" />
|
210 |
+
</Button>
|
211 |
+
</div>
|
212 |
+
</div>
|
213 |
+
</div>
|
214 |
+
|
215 |
+
<div className="grid grid-cols-1 gap-4">
|
216 |
+
<Button onClick={() => setShowHighScores(true)} className="w-full bg-primary hover:bg-primary/90">
|
217 |
+
{t.game.review.saveScore} 🏆
|
218 |
+
</Button>
|
219 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
220 |
+
<Button onClick={handlePlayAgain} variant="secondary" className="text-white w-full">
|
221 |
+
{t.game.review.playAgain} ⏎
|
222 |
+
</Button>
|
223 |
+
<Button onClick={handlePlayNewWords} variant="secondary" className="text-white w-full">
|
224 |
+
{t.game.review.playNewWords}
|
225 |
+
</Button>
|
226 |
+
</div>
|
227 |
+
</div>
|
228 |
+
|
229 |
+
<Dialog open={showHighScores} onOpenChange={setShowHighScores}>
|
230 |
+
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-[600px]">
|
231 |
+
<HighScoreBoard
|
232 |
+
currentScore={currentScore}
|
233 |
+
avgWordsPerRound={avgWordsPerRound}
|
234 |
+
onClose={() => setShowHighScores(false)}
|
235 |
+
gameId={gameId}
|
236 |
+
sessionId={sessionId}
|
237 |
+
showThemeFilter={false}
|
238 |
+
initialTheme={currentTheme}
|
239 |
+
/>
|
240 |
+
</DialogContent>
|
241 |
+
</Dialog>
|
242 |
+
</motion.div>
|
243 |
+
);
|
244 |
+
};
|
src/components/game/GuessDisplay.tsx
CHANGED
@@ -1,70 +1,62 @@
|
|
1 |
import { motion } from "framer-motion";
|
2 |
import { useState, useEffect } from "react";
|
3 |
import { useTranslation } from "@/hooks/useTranslation";
|
4 |
-
import { Button } from "@/components/ui/button";
|
5 |
-
import {
|
6 |
-
Dialog,
|
7 |
-
DialogContent,
|
8 |
-
DialogTrigger,
|
9 |
-
} from "@/components/ui/dialog";
|
10 |
import { RoundHeader } from "./sentence-builder/RoundHeader";
|
11 |
import { WordDisplay } from "./sentence-builder/WordDisplay";
|
12 |
import { GuessDescription } from "./guess-display/GuessDescription";
|
13 |
import { GuessResult } from "./guess-display/GuessResult";
|
14 |
import { ActionButtons } from "./guess-display/ActionButtons";
|
15 |
-
import { HighScoreBoard } from "@/components/HighScoreBoard";
|
16 |
|
17 |
interface GuessDisplayProps {
|
|
|
|
|
18 |
sentence: string[];
|
19 |
aiGuess: string;
|
20 |
-
currentWord: string;
|
21 |
-
onNextRound: () => void;
|
22 |
-
onPlayAgain: () => void;
|
23 |
-
onBack?: () => void;
|
24 |
-
currentScore: number;
|
25 |
avgWordsPerRound: number;
|
26 |
sessionId: string;
|
27 |
currentTheme: string;
|
28 |
-
|
|
|
|
|
29 |
normalizeWord: (word: string) => string;
|
30 |
}
|
31 |
|
32 |
export const GuessDisplay = ({
|
|
|
|
|
33 |
sentence,
|
34 |
aiGuess,
|
35 |
-
currentWord,
|
36 |
-
onNextRound,
|
37 |
-
onPlayAgain,
|
38 |
-
onBack,
|
39 |
-
currentScore,
|
40 |
avgWordsPerRound,
|
41 |
sessionId,
|
42 |
currentTheme,
|
43 |
-
|
|
|
|
|
44 |
normalizeWord,
|
45 |
}: GuessDisplayProps) => {
|
46 |
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
47 |
-
const [hasSubmittedScore, setHasSubmittedScore] = useState(false);
|
48 |
-
const [showHighScores, setShowHighScores] = useState(false);
|
49 |
const t = useTranslation();
|
50 |
|
51 |
-
useEffect(() => {
|
52 |
-
onHighScoreDialogChange?.(showHighScores);
|
53 |
-
}, [showHighScores, onHighScoreDialogChange]);
|
54 |
-
|
55 |
const handleSetShowConfirmDialog = (show: boolean) => {
|
56 |
setShowConfirmDialog(show);
|
57 |
};
|
58 |
|
59 |
const isGuessCorrect = () => normalizeWord(aiGuess) === normalizeWord(currentWord);
|
60 |
|
61 |
-
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
|
65 |
-
|
66 |
-
|
67 |
-
};
|
68 |
|
69 |
return (
|
70 |
<motion.div
|
@@ -83,34 +75,22 @@ export const GuessDisplay = ({
|
|
83 |
|
84 |
<GuessDescription sentence={sentence} aiGuess={aiGuess} />
|
85 |
|
86 |
-
<GuessResult
|
|
|
|
|
|
|
|
|
87 |
|
88 |
<ActionButtons
|
89 |
isCorrect={isGuessCorrect()}
|
90 |
onNextRound={onNextRound}
|
91 |
-
|
92 |
currentScore={currentScore}
|
93 |
avgWordsPerRound={avgWordsPerRound}
|
94 |
sessionId={sessionId}
|
95 |
currentTheme={currentTheme}
|
96 |
-
onScoreSubmitted={handleScoreSubmitted}
|
97 |
-
onShowHighScores={handleShowHighScores}
|
98 |
/>
|
99 |
|
100 |
-
<Dialog open={showHighScores} onOpenChange={setShowHighScores}>
|
101 |
-
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-[600px]">
|
102 |
-
<HighScoreBoard
|
103 |
-
currentScore={currentScore}
|
104 |
-
avgWordsPerRound={avgWordsPerRound}
|
105 |
-
onClose={() => setShowHighScores(false)}
|
106 |
-
onPlayAgain={onPlayAgain}
|
107 |
-
sessionId={sessionId}
|
108 |
-
showThemeFilter={false}
|
109 |
-
initialTheme={currentTheme}
|
110 |
-
onScoreSubmitted={handleScoreSubmitted}
|
111 |
-
/>
|
112 |
-
</DialogContent>
|
113 |
-
</Dialog>
|
114 |
</motion.div>
|
115 |
);
|
116 |
};
|
|
|
1 |
import { motion } from "framer-motion";
|
2 |
import { useState, useEffect } from "react";
|
3 |
import { useTranslation } from "@/hooks/useTranslation";
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
import { RoundHeader } from "./sentence-builder/RoundHeader";
|
5 |
import { WordDisplay } from "./sentence-builder/WordDisplay";
|
6 |
import { GuessDescription } from "./guess-display/GuessDescription";
|
7 |
import { GuessResult } from "./guess-display/GuessResult";
|
8 |
import { ActionButtons } from "./guess-display/ActionButtons";
|
|
|
9 |
|
10 |
interface GuessDisplayProps {
|
11 |
+
currentScore: number;
|
12 |
+
currentWord: string;
|
13 |
sentence: string[];
|
14 |
aiGuess: string;
|
|
|
|
|
|
|
|
|
|
|
15 |
avgWordsPerRound: number;
|
16 |
sessionId: string;
|
17 |
currentTheme: string;
|
18 |
+
onNextRound: () => void;
|
19 |
+
onGameReview: () => void;
|
20 |
+
onBack?: () => void;
|
21 |
normalizeWord: (word: string) => string;
|
22 |
}
|
23 |
|
24 |
export const GuessDisplay = ({
|
25 |
+
currentScore,
|
26 |
+
currentWord,
|
27 |
sentence,
|
28 |
aiGuess,
|
|
|
|
|
|
|
|
|
|
|
29 |
avgWordsPerRound,
|
30 |
sessionId,
|
31 |
currentTheme,
|
32 |
+
onNextRound,
|
33 |
+
onBack,
|
34 |
+
onGameReview,
|
35 |
normalizeWord,
|
36 |
}: GuessDisplayProps) => {
|
37 |
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
|
|
|
38 |
const t = useTranslation();
|
39 |
|
|
|
|
|
|
|
|
|
40 |
const handleSetShowConfirmDialog = (show: boolean) => {
|
41 |
setShowConfirmDialog(show);
|
42 |
};
|
43 |
|
44 |
const isGuessCorrect = () => normalizeWord(aiGuess) === normalizeWord(currentWord);
|
45 |
|
46 |
+
useEffect(() => {
|
47 |
+
const handleKeyPress = (e: KeyboardEvent) => {
|
48 |
+
if (e.key === 'Enter') {
|
49 |
+
if (isGuessCorrect()) {
|
50 |
+
onNextRound();
|
51 |
+
} else {
|
52 |
+
onGameReview();
|
53 |
+
}
|
54 |
+
}
|
55 |
+
};
|
56 |
|
57 |
+
window.addEventListener('keydown', handleKeyPress);
|
58 |
+
return () => window.removeEventListener('keydown', handleKeyPress);
|
59 |
+
}, [isGuessCorrect, onNextRound, onGameReview]);
|
60 |
|
61 |
return (
|
62 |
<motion.div
|
|
|
75 |
|
76 |
<GuessDescription sentence={sentence} aiGuess={aiGuess} />
|
77 |
|
78 |
+
<GuessResult
|
79 |
+
aiGuess={aiGuess}
|
80 |
+
isCorrect={isGuessCorrect()}
|
81 |
+
onNextRound={onNextRound}
|
82 |
+
/>
|
83 |
|
84 |
<ActionButtons
|
85 |
isCorrect={isGuessCorrect()}
|
86 |
onNextRound={onNextRound}
|
87 |
+
onGameReview={onGameReview}
|
88 |
currentScore={currentScore}
|
89 |
avgWordsPerRound={avgWordsPerRound}
|
90 |
sessionId={sessionId}
|
91 |
currentTheme={currentTheme}
|
|
|
|
|
92 |
/>
|
93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
</motion.div>
|
95 |
);
|
96 |
};
|
src/components/game/LanguageSelector.tsx
CHANGED
@@ -2,6 +2,12 @@ import { Button } from "@/components/ui/button";
|
|
2 |
import { useContext } from "react";
|
3 |
import { LanguageContext } from "@/contexts/LanguageContext";
|
4 |
import { Language } from "@/i18n/translations";
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
const languages: { code: Language; name: string; flag: string }[] = [
|
7 |
{ code: 'en', name: 'English', flag: '🇬🇧' },
|
@@ -11,6 +17,7 @@ const languages: { code: Language; name: string; flag: string }[] = [
|
|
11 |
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
12 |
];
|
13 |
|
|
|
14 |
export const LanguageSelector = () => {
|
15 |
const { language, setLanguage } = useContext(LanguageContext);
|
16 |
|
@@ -21,19 +28,27 @@ export const LanguageSelector = () => {
|
|
21 |
setLanguage(code);
|
22 |
};
|
23 |
|
|
|
|
|
24 |
return (
|
25 |
-
<
|
26 |
-
|
27 |
-
<Button
|
28 |
-
|
29 |
-
variant={language === code ? "default" : "outline"}
|
30 |
-
onClick={() => handleLanguageChange(code)}
|
31 |
-
className="flex items-center gap-2"
|
32 |
-
>
|
33 |
-
<span>{flag}</span>
|
34 |
-
<span>{name}</span>
|
35 |
</Button>
|
36 |
-
|
37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
);
|
39 |
};
|
|
|
2 |
import { useContext } from "react";
|
3 |
import { LanguageContext } from "@/contexts/LanguageContext";
|
4 |
import { Language } from "@/i18n/translations";
|
5 |
+
import {
|
6 |
+
DropdownMenu,
|
7 |
+
DropdownMenuContent,
|
8 |
+
DropdownMenuItem,
|
9 |
+
DropdownMenuTrigger,
|
10 |
+
} from "@/components/ui/dropdown-menu";
|
11 |
|
12 |
const languages: { code: Language; name: string; flag: string }[] = [
|
13 |
{ code: 'en', name: 'English', flag: '🇬🇧' },
|
|
|
17 |
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
18 |
];
|
19 |
|
20 |
+
|
21 |
export const LanguageSelector = () => {
|
22 |
const { language, setLanguage } = useContext(LanguageContext);
|
23 |
|
|
|
28 |
setLanguage(code);
|
29 |
};
|
30 |
|
31 |
+
const currentLanguage = languages.find(lang => lang.code === language);
|
32 |
+
|
33 |
return (
|
34 |
+
<DropdownMenu>
|
35 |
+
<DropdownMenuTrigger asChild>
|
36 |
+
<Button variant="outline" size="sm" >
|
37 |
+
<span className="text-xl">{currentLanguage?.flag}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
</Button>
|
39 |
+
</DropdownMenuTrigger>
|
40 |
+
<DropdownMenuContent align="end">
|
41 |
+
{languages.map(({ code, name, flag }) => (
|
42 |
+
<DropdownMenuItem
|
43 |
+
key={code}
|
44 |
+
onClick={() => handleLanguageChange(code)}
|
45 |
+
className="cursor-pointer"
|
46 |
+
>
|
47 |
+
<span className="mr-2">{flag}</span>
|
48 |
+
<span>{name}</span>
|
49 |
+
</DropdownMenuItem>
|
50 |
+
))}
|
51 |
+
</DropdownMenuContent>
|
52 |
+
</DropdownMenu>
|
53 |
);
|
54 |
};
|
src/components/game/SentenceBuilder.tsx
CHANGED
@@ -15,6 +15,7 @@ import { RoundHeader } from "./sentence-builder/RoundHeader";
|
|
15 |
import { WordDisplay } from "./sentence-builder/WordDisplay";
|
16 |
import { SentenceDisplay } from "./sentence-builder/SentenceDisplay";
|
17 |
import { InputForm } from "./sentence-builder/InputForm";
|
|
|
18 |
|
19 |
interface SentenceBuilderProps {
|
20 |
currentWord: string;
|
@@ -25,8 +26,9 @@ interface SentenceBuilderProps {
|
|
25 |
onInputChange: (value: string) => void;
|
26 |
onSubmitWord: (e: React.FormEvent) => void;
|
27 |
onMakeGuess: () => void;
|
28 |
-
normalizeWord: (word: string) => string;
|
29 |
onBack?: () => void;
|
|
|
30 |
}
|
31 |
|
32 |
export const SentenceBuilder = ({
|
@@ -40,6 +42,7 @@ export const SentenceBuilder = ({
|
|
40 |
onMakeGuess,
|
41 |
normalizeWord,
|
42 |
onBack,
|
|
|
43 |
}: SentenceBuilderProps) => {
|
44 |
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
45 |
const [hasMultipleWords, setHasMultipleWords] = useState(false);
|
@@ -108,8 +111,10 @@ export const SentenceBuilder = ({
|
|
108 |
</AlertDialogDescription>
|
109 |
</AlertDialogHeader>
|
110 |
<AlertDialogFooter>
|
111 |
-
<AlertDialogCancel
|
112 |
-
|
|
|
|
|
113 |
{t.game.confirm}
|
114 |
</AlertDialogAction>
|
115 |
</AlertDialogFooter>
|
|
|
15 |
import { WordDisplay } from "./sentence-builder/WordDisplay";
|
16 |
import { SentenceDisplay } from "./sentence-builder/SentenceDisplay";
|
17 |
import { InputForm } from "./sentence-builder/InputForm";
|
18 |
+
import { Button } from "@/components/ui/button";
|
19 |
|
20 |
interface SentenceBuilderProps {
|
21 |
currentWord: string;
|
|
|
26 |
onInputChange: (value: string) => void;
|
27 |
onSubmitWord: (e: React.FormEvent) => void;
|
28 |
onMakeGuess: () => void;
|
29 |
+
normalizeWord: (word: string) => string;
|
30 |
onBack?: () => void;
|
31 |
+
onClose: () => void;
|
32 |
}
|
33 |
|
34 |
export const SentenceBuilder = ({
|
|
|
42 |
onMakeGuess,
|
43 |
normalizeWord,
|
44 |
onBack,
|
45 |
+
onClose,
|
46 |
}: SentenceBuilderProps) => {
|
47 |
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
48 |
const [hasMultipleWords, setHasMultipleWords] = useState(false);
|
|
|
111 |
</AlertDialogDescription>
|
112 |
</AlertDialogHeader>
|
113 |
<AlertDialogFooter>
|
114 |
+
<AlertDialogCancel onClick={() => setShowConfirmDialog(false)}>
|
115 |
+
{t.game.cancel}
|
116 |
+
</AlertDialogCancel>
|
117 |
+
<AlertDialogAction onClick={onBack}>
|
118 |
{t.game.confirm}
|
119 |
</AlertDialogAction>
|
120 |
</AlertDialogFooter>
|
src/components/game/WelcomeScreen.tsx
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
import { motion } from "framer-motion";
|
2 |
-
import { useState } from "react";
|
3 |
import { HighScoreBoard } from "../HighScoreBoard";
|
4 |
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
5 |
import { LanguageSelector } from "./LanguageSelector";
|
@@ -10,14 +10,26 @@ import { MainActions } from "./welcome/MainActions";
|
|
10 |
import { HowToPlayDialog } from "./welcome/HowToPlayDialog";
|
11 |
|
12 |
interface WelcomeScreenProps {
|
13 |
-
|
|
|
14 |
}
|
15 |
|
16 |
-
export const WelcomeScreen = ({
|
17 |
const [showHighScores, setShowHighScores] = useState(false);
|
18 |
const [showHowToPlay, setShowHowToPlay] = useState(false);
|
19 |
const t = useTranslation();
|
20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
return (
|
22 |
<>
|
23 |
<motion.div
|
@@ -25,22 +37,25 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
|
|
25 |
animate={{ opacity: 1 }}
|
26 |
className="max-w-2xl mx-auto text-center space-y-8"
|
27 |
>
|
28 |
-
|
29 |
-
|
30 |
-
<div>
|
31 |
<h1 className="mb-4 text-4xl font-bold text-gray-900">{t.welcome.title}</h1>
|
|
|
|
|
|
|
32 |
<p className="text-lg text-gray-600">
|
33 |
{t.welcome.subtitle}
|
34 |
</p>
|
35 |
</div>
|
36 |
|
37 |
-
<MainActions
|
38 |
-
|
|
|
39 |
onShowHowToPlay={() => setShowHowToPlay(true)}
|
40 |
onShowHighScores={() => setShowHighScores(true)}
|
41 |
/>
|
42 |
</motion.div>
|
43 |
-
|
44 |
<motion.div
|
45 |
initial={{ opacity: 0 }}
|
46 |
animate={{ opacity: 1 }}
|
@@ -64,13 +79,12 @@ export const WelcomeScreen = ({ onStart }: WelcomeScreenProps) => {
|
|
64 |
<HighScoreBoard
|
65 |
showThemeFilter={true}
|
66 |
onClose={() => setShowHighScores(false)}
|
67 |
-
onPlayAgain={onStart}
|
68 |
/>
|
69 |
</DialogContent>
|
70 |
</Dialog>
|
71 |
|
72 |
-
<HowToPlayDialog
|
73 |
-
open={showHowToPlay}
|
74 |
onOpenChange={setShowHowToPlay}
|
75 |
/>
|
76 |
</>
|
|
|
1 |
import { motion } from "framer-motion";
|
2 |
+
import { useEffect, useState } from "react";
|
3 |
import { HighScoreBoard } from "../HighScoreBoard";
|
4 |
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
5 |
import { LanguageSelector } from "./LanguageSelector";
|
|
|
10 |
import { HowToPlayDialog } from "./welcome/HowToPlayDialog";
|
11 |
|
12 |
interface WelcomeScreenProps {
|
13 |
+
onStartDaily: () => void;
|
14 |
+
onStartNew: () => void;
|
15 |
}
|
16 |
|
17 |
+
export const WelcomeScreen = ({ onStartDaily: onStartDaily, onStartNew: onStartNew }: WelcomeScreenProps) => {
|
18 |
const [showHighScores, setShowHighScores] = useState(false);
|
19 |
const [showHowToPlay, setShowHowToPlay] = useState(false);
|
20 |
const t = useTranslation();
|
21 |
|
22 |
+
useEffect(() => {
|
23 |
+
const handleKeyPress = (e: KeyboardEvent) => {
|
24 |
+
if (e.key === 'Enter') {
|
25 |
+
onStartDaily()
|
26 |
+
}
|
27 |
+
};
|
28 |
+
|
29 |
+
window.addEventListener('keydown', handleKeyPress);
|
30 |
+
return () => window.removeEventListener('keydown', handleKeyPress);
|
31 |
+
}, [onStartDaily]);
|
32 |
+
|
33 |
return (
|
34 |
<>
|
35 |
<motion.div
|
|
|
37 |
animate={{ opacity: 1 }}
|
38 |
className="max-w-2xl mx-auto text-center space-y-8"
|
39 |
>
|
40 |
+
|
41 |
+
<div className="relative">
|
|
|
42 |
<h1 className="mb-4 text-4xl font-bold text-gray-900">{t.welcome.title}</h1>
|
43 |
+
<div className="absolute top-0 right-0">
|
44 |
+
<LanguageSelector />
|
45 |
+
</div>
|
46 |
<p className="text-lg text-gray-600">
|
47 |
{t.welcome.subtitle}
|
48 |
</p>
|
49 |
</div>
|
50 |
|
51 |
+
<MainActions
|
52 |
+
onStartDaily={onStartDaily}
|
53 |
+
onStartNew={onStartNew}
|
54 |
onShowHowToPlay={() => setShowHowToPlay(true)}
|
55 |
onShowHighScores={() => setShowHighScores(true)}
|
56 |
/>
|
57 |
</motion.div>
|
58 |
+
|
59 |
<motion.div
|
60 |
initial={{ opacity: 0 }}
|
61 |
animate={{ opacity: 1 }}
|
|
|
79 |
<HighScoreBoard
|
80 |
showThemeFilter={true}
|
81 |
onClose={() => setShowHighScores(false)}
|
|
|
82 |
/>
|
83 |
</DialogContent>
|
84 |
</Dialog>
|
85 |
|
86 |
+
<HowToPlayDialog
|
87 |
+
open={showHowToPlay}
|
88 |
onOpenChange={setShowHowToPlay}
|
89 |
/>
|
90 |
</>
|
src/components/game/guess-display/ActionButtons.tsx
CHANGED
@@ -5,20 +5,17 @@ import { useTranslation } from "@/hooks/useTranslation";
|
|
5 |
interface ActionButtonsProps {
|
6 |
isCorrect: boolean;
|
7 |
onNextRound: () => void;
|
8 |
-
|
9 |
currentScore: number;
|
10 |
avgWordsPerRound: number;
|
11 |
sessionId: string;
|
12 |
currentTheme: string;
|
13 |
-
onScoreSubmitted?: () => void;
|
14 |
-
onShowHighScores: () => void;
|
15 |
}
|
16 |
|
17 |
export const ActionButtons = ({
|
18 |
isCorrect,
|
19 |
onNextRound,
|
20 |
-
|
21 |
-
onShowHighScores,
|
22 |
}: ActionButtonsProps) => {
|
23 |
const t = useTranslation();
|
24 |
|
@@ -27,14 +24,7 @@ export const ActionButtons = ({
|
|
27 |
{isCorrect ? (
|
28 |
<Button onClick={onNextRound} className="text-white">{t.game.nextRound} ⏎</Button>
|
29 |
) : (
|
30 |
-
|
31 |
-
<Button onClick={onPlayAgain} className="text-white">
|
32 |
-
{t.game.playAgain} ⏎
|
33 |
-
</Button>
|
34 |
-
<Button onClick={onShowHighScores} variant="secondary" className="text-white">
|
35 |
-
{t.game.saveScore}
|
36 |
-
</Button>
|
37 |
-
</>
|
38 |
)}
|
39 |
</div>
|
40 |
);
|
|
|
5 |
interface ActionButtonsProps {
|
6 |
isCorrect: boolean;
|
7 |
onNextRound: () => void;
|
8 |
+
onGameReview: () => void;
|
9 |
currentScore: number;
|
10 |
avgWordsPerRound: number;
|
11 |
sessionId: string;
|
12 |
currentTheme: string;
|
|
|
|
|
13 |
}
|
14 |
|
15 |
export const ActionButtons = ({
|
16 |
isCorrect,
|
17 |
onNextRound,
|
18 |
+
onGameReview,
|
|
|
19 |
}: ActionButtonsProps) => {
|
20 |
const t = useTranslation();
|
21 |
|
|
|
24 |
{isCorrect ? (
|
25 |
<Button onClick={onNextRound} className="text-white">{t.game.nextRound} ⏎</Button>
|
26 |
) : (
|
27 |
+
<Button onClick={onGameReview} className="text-white">{t.game.review.title} ⏎</Button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
)}
|
29 |
</div>
|
30 |
);
|
src/components/game/guess-display/GuessResult.tsx
CHANGED
@@ -1,18 +1,20 @@
|
|
1 |
import { useTranslation } from "@/hooks/useTranslation";
|
|
|
2 |
|
3 |
interface GuessResultProps {
|
4 |
aiGuess: string;
|
5 |
isCorrect: boolean;
|
|
|
6 |
}
|
7 |
|
8 |
-
export const GuessResult = ({ aiGuess, isCorrect }: GuessResultProps) => {
|
9 |
const t = useTranslation();
|
10 |
-
|
11 |
return (
|
12 |
-
<div className="space-y-
|
13 |
<p className="text-sm text-gray-600">
|
14 |
{t.guess.aiGuessedDescription}
|
15 |
-
</p>
|
16 |
<div className={`rounded-lg ${isCorrect ? 'bg-green-50' : 'bg-red-50'}`}>
|
17 |
<p className={`p-4 text-2xl font-bold tracking-wider ${isCorrect ? 'text-green-600' : 'text-red-600'}`}>
|
18 |
{aiGuess}
|
|
|
1 |
import { useTranslation } from "@/hooks/useTranslation";
|
2 |
+
import { Button } from "@/components/ui/button";
|
3 |
|
4 |
interface GuessResultProps {
|
5 |
aiGuess: string;
|
6 |
isCorrect: boolean;
|
7 |
+
onNextRound: () => void;
|
8 |
}
|
9 |
|
10 |
+
export const GuessResult = ({ aiGuess, isCorrect, onNextRound }: GuessResultProps) => {
|
11 |
const t = useTranslation();
|
12 |
+
|
13 |
return (
|
14 |
+
<div className="space-y-4">
|
15 |
<p className="text-sm text-gray-600">
|
16 |
{t.guess.aiGuessedDescription}
|
17 |
+
</p>
|
18 |
<div className={`rounded-lg ${isCorrect ? 'bg-green-50' : 'bg-red-50'}`}>
|
19 |
<p className={`p-4 text-2xl font-bold tracking-wider ${isCorrect ? 'text-green-600' : 'text-red-600'}`}>
|
20 |
{aiGuess}
|
src/components/game/leaderboard/ScoresTable.tsx
CHANGED
@@ -7,6 +7,8 @@ import {
|
|
7 |
TableRow,
|
8 |
} from "@/components/ui/table";
|
9 |
import { useTranslation } from "@/hooks/useTranslation";
|
|
|
|
|
10 |
|
11 |
interface HighScore {
|
12 |
id: string;
|
@@ -15,13 +17,18 @@ interface HighScore {
|
|
15 |
avg_words_per_round: number;
|
16 |
created_at: string;
|
17 |
session_id: string;
|
18 |
-
|
|
|
|
|
|
|
19 |
}
|
20 |
|
21 |
interface ScoresTableProps {
|
22 |
scores: HighScore[];
|
23 |
startIndex: number;
|
24 |
showThemeColumn?: boolean;
|
|
|
|
|
25 |
}
|
26 |
|
27 |
const getRankMedal = (rank: number) => {
|
@@ -37,7 +44,30 @@ const getRankMedal = (rank: number) => {
|
|
37 |
}
|
38 |
};
|
39 |
|
40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
const t = useTranslation();
|
42 |
|
43 |
return (
|
@@ -49,8 +79,10 @@ export const ScoresTable = ({ scores, startIndex, showThemeColumn = false }: Sco
|
|
49 |
<TableHead>{t.leaderboard.player}</TableHead>
|
50 |
<TableHead>{t.leaderboard.roundsColumn}</TableHead>
|
51 |
<TableHead>{t.leaderboard.avgWords}</TableHead>
|
52 |
-
{
|
53 |
-
<TableHead>
|
|
|
|
|
54 |
)}
|
55 |
</TableRow>
|
56 |
</TableHeader>
|
@@ -58,21 +90,36 @@ export const ScoresTable = ({ scores, startIndex, showThemeColumn = false }: Sco
|
|
58 |
{scores?.map((score, index) => {
|
59 |
const absoluteRank = startIndex + index + 1;
|
60 |
const medal = getRankMedal(absoluteRank);
|
|
|
61 |
return (
|
62 |
<TableRow key={score.id}>
|
63 |
-
<TableCell>{medal}</TableCell>
|
64 |
-
<TableCell>
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
)}
|
70 |
</TableRow>
|
71 |
);
|
72 |
})}
|
73 |
{!scores?.length && (
|
74 |
<TableRow>
|
75 |
-
<TableCell colSpan={
|
76 |
{t.leaderboard.noScores}
|
77 |
</TableCell>
|
78 |
</TableRow>
|
|
|
7 |
TableRow,
|
8 |
} from "@/components/ui/table";
|
9 |
import { useTranslation } from "@/hooks/useTranslation";
|
10 |
+
import { Button } from "@/components/ui/button";
|
11 |
+
import { Play } from "lucide-react";
|
12 |
|
13 |
interface HighScore {
|
14 |
id: string;
|
|
|
17 |
avg_words_per_round: number;
|
18 |
created_at: string;
|
19 |
session_id: string;
|
20 |
+
game?: {
|
21 |
+
language: string;
|
22 |
+
};
|
23 |
+
game_id?: string;
|
24 |
}
|
25 |
|
26 |
interface ScoresTableProps {
|
27 |
scores: HighScore[];
|
28 |
startIndex: number;
|
29 |
showThemeColumn?: boolean;
|
30 |
+
onPlayGame?: (gameId: string) => void;
|
31 |
+
selectedMode?: 'daily' | 'all-time';
|
32 |
}
|
33 |
|
34 |
const getRankMedal = (rank: number) => {
|
|
|
44 |
}
|
45 |
};
|
46 |
|
47 |
+
const getLanguageEmoji = (language: string) => {
|
48 |
+
switch (language) {
|
49 |
+
case 'en':
|
50 |
+
return '🇬🇧';
|
51 |
+
case 'de':
|
52 |
+
return '🇩🇪';
|
53 |
+
case 'fr':
|
54 |
+
return '🇫🇷';
|
55 |
+
case 'it':
|
56 |
+
return '🇮🇹';
|
57 |
+
case 'es':
|
58 |
+
return '🇪🇸';
|
59 |
+
default:
|
60 |
+
return '🌐';
|
61 |
+
}
|
62 |
+
};
|
63 |
+
|
64 |
+
export const ScoresTable = ({
|
65 |
+
scores,
|
66 |
+
startIndex,
|
67 |
+
showThemeColumn = false,
|
68 |
+
onPlayGame,
|
69 |
+
selectedMode = 'daily'
|
70 |
+
}: ScoresTableProps) => {
|
71 |
const t = useTranslation();
|
72 |
|
73 |
return (
|
|
|
79 |
<TableHead>{t.leaderboard.player}</TableHead>
|
80 |
<TableHead>{t.leaderboard.roundsColumn}</TableHead>
|
81 |
<TableHead>{t.leaderboard.avgWords}</TableHead>
|
82 |
+
{selectedMode === 'all-time' && (
|
83 |
+
<TableHead className="text-center">
|
84 |
+
{t.leaderboard.playSameWords}
|
85 |
+
</TableHead>
|
86 |
)}
|
87 |
</TableRow>
|
88 |
</TableHeader>
|
|
|
90 |
{scores?.map((score, index) => {
|
91 |
const absoluteRank = startIndex + index + 1;
|
92 |
const medal = getRankMedal(absoluteRank);
|
93 |
+
const language = score.game?.language || 'en';
|
94 |
return (
|
95 |
<TableRow key={score.id}>
|
96 |
+
<TableCell className="align-middle">{medal}</TableCell>
|
97 |
+
<TableCell className="flex items-center gap-2 h-full align-middle">
|
98 |
+
{score.player_name}
|
99 |
+
<span>{getLanguageEmoji(language)}</span>
|
100 |
+
</TableCell>
|
101 |
+
<TableCell className="align-middle">{score.score}</TableCell>
|
102 |
+
<TableCell className="align-middle">{score.avg_words_per_round.toFixed(1)}</TableCell>
|
103 |
+
{selectedMode === 'all-time' && (
|
104 |
+
<TableCell className="text-center align-middle">
|
105 |
+
{score.game_id && onPlayGame && (
|
106 |
+
<Button
|
107 |
+
variant="ghost"
|
108 |
+
size="sm"
|
109 |
+
onClick={() => onPlayGame(score.game_id!)}
|
110 |
+
className="gap-2 mx-auto"
|
111 |
+
>
|
112 |
+
<Play className="h-4 w-4" />
|
113 |
+
</Button>
|
114 |
+
)}
|
115 |
+
</TableCell>
|
116 |
)}
|
117 |
</TableRow>
|
118 |
);
|
119 |
})}
|
120 |
{!scores?.length && (
|
121 |
<TableRow>
|
122 |
+
<TableCell colSpan={5} className="text-center">
|
123 |
{t.leaderboard.noScores}
|
124 |
</TableCell>
|
125 |
</TableRow>
|
src/components/game/leaderboard/ThemeFilter.tsx
CHANGED
@@ -2,29 +2,29 @@ import { Button } from "@/components/ui/button";
|
|
2 |
import { useTranslation } from "@/hooks/useTranslation";
|
3 |
import { Filter } from "lucide-react";
|
4 |
|
5 |
-
type
|
6 |
|
7 |
interface ThemeFilterProps {
|
8 |
-
|
9 |
-
|
10 |
}
|
11 |
|
12 |
-
export const ThemeFilter = ({
|
13 |
const t = useTranslation();
|
14 |
|
15 |
-
const
|
16 |
|
17 |
return (
|
18 |
<div className="flex flex-wrap gap-2 mb-4 items-center">
|
19 |
<Filter className="h-4 w-4 text-gray-500" />
|
20 |
-
{
|
21 |
<Button
|
22 |
-
key={
|
23 |
-
variant={
|
24 |
size="sm"
|
25 |
-
onClick={() =>
|
26 |
>
|
27 |
-
{t.
|
28 |
</Button>
|
29 |
))}
|
30 |
</div>
|
|
|
2 |
import { useTranslation } from "@/hooks/useTranslation";
|
3 |
import { Filter } from "lucide-react";
|
4 |
|
5 |
+
type ViewMode = 'daily' | 'all-time';
|
6 |
|
7 |
interface ThemeFilterProps {
|
8 |
+
selectedMode: ViewMode;
|
9 |
+
onModeChange: (mode: ViewMode) => void;
|
10 |
}
|
11 |
|
12 |
+
export const ThemeFilter = ({ selectedMode, onModeChange }: ThemeFilterProps) => {
|
13 |
const t = useTranslation();
|
14 |
|
15 |
+
const modes: ViewMode[] = ['daily', 'all-time'];
|
16 |
|
17 |
return (
|
18 |
<div className="flex flex-wrap gap-2 mb-4 items-center">
|
19 |
<Filter className="h-4 w-4 text-gray-500" />
|
20 |
+
{modes.map((mode) => (
|
21 |
<Button
|
22 |
+
key={mode}
|
23 |
+
variant={selectedMode === mode ? "default" : "outline"}
|
24 |
size="sm"
|
25 |
+
onClick={() => onModeChange(mode)}
|
26 |
>
|
27 |
+
{t.leaderboard.modes[mode]}
|
28 |
</Button>
|
29 |
))}
|
30 |
</div>
|
src/components/game/sentence-builder/RoundHeader.tsx
CHANGED
@@ -17,16 +17,18 @@ interface RoundHeaderProps {
|
|
17 |
onBack?: () => void;
|
18 |
showConfirmDialog: boolean;
|
19 |
setShowConfirmDialog: (show: boolean) => void;
|
|
|
20 |
}
|
21 |
|
22 |
-
export const RoundHeader = ({
|
23 |
-
successfulRounds,
|
24 |
onBack,
|
25 |
showConfirmDialog,
|
26 |
-
setShowConfirmDialog
|
|
|
27 |
}: RoundHeaderProps) => {
|
28 |
const t = useTranslation();
|
29 |
-
|
30 |
const handleHomeClick = () => {
|
31 |
console.log("RoundHeader - Home button clicked, successful rounds:", successfulRounds);
|
32 |
if (successfulRounds > 0) {
|
@@ -58,7 +60,7 @@ export const RoundHeader = ({
|
|
58 |
<Button
|
59 |
variant="ghost"
|
60 |
size="icon"
|
61 |
-
className="absolute left-0 top-0 text-gray-600 hover:text-
|
62 |
onClick={handleHomeClick}
|
63 |
>
|
64 |
<House className="h-5 w-5" />
|
@@ -77,7 +79,7 @@ export const RoundHeader = ({
|
|
77 |
</AlertDialogDescription>
|
78 |
</AlertDialogHeader>
|
79 |
<AlertDialogFooter>
|
80 |
-
<AlertDialogCancel>{t.game.cancel}</AlertDialogCancel>
|
81 |
<AlertDialogAction>{t.game.confirm}</AlertDialogAction>
|
82 |
</AlertDialogFooter>
|
83 |
</AlertDialogContent>
|
|
|
17 |
onBack?: () => void;
|
18 |
showConfirmDialog: boolean;
|
19 |
setShowConfirmDialog: (show: boolean) => void;
|
20 |
+
onCancel?: () => void;
|
21 |
}
|
22 |
|
23 |
+
export const RoundHeader = ({
|
24 |
+
successfulRounds,
|
25 |
onBack,
|
26 |
showConfirmDialog,
|
27 |
+
setShowConfirmDialog,
|
28 |
+
onCancel
|
29 |
}: RoundHeaderProps) => {
|
30 |
const t = useTranslation();
|
31 |
+
|
32 |
const handleHomeClick = () => {
|
33 |
console.log("RoundHeader - Home button clicked, successful rounds:", successfulRounds);
|
34 |
if (successfulRounds > 0) {
|
|
|
60 |
<Button
|
61 |
variant="ghost"
|
62 |
size="icon"
|
63 |
+
className="absolute left-0 top-0 text-gray-600 hover:text-white"
|
64 |
onClick={handleHomeClick}
|
65 |
>
|
66 |
<House className="h-5 w-5" />
|
|
|
79 |
</AlertDialogDescription>
|
80 |
</AlertDialogHeader>
|
81 |
<AlertDialogFooter>
|
82 |
+
<AlertDialogCancel onClick={onCancel}>{t.game.cancel}</AlertDialogCancel>
|
83 |
<AlertDialogAction>{t.game.confirm}</AlertDialogAction>
|
84 |
</AlertDialogFooter>
|
85 |
</AlertDialogContent>
|
src/components/game/welcome/ContestSection.tsx
CHANGED
@@ -7,10 +7,10 @@ export const ContestSection = () => {
|
|
7 |
|
8 |
return (
|
9 |
<div className="flex flex-col items-center gap-2">
|
10 |
-
<p className="text-lg font-semibold text-
|
11 |
<Dialog>
|
12 |
<DialogTrigger asChild>
|
13 |
-
<button className="inline-flex items-center text-sm text-primary
|
14 |
{t.welcome.contest.terms} <Info className="h-4 w-4 ml-1" />
|
15 |
</button>
|
16 |
</DialogTrigger>
|
|
|
7 |
|
8 |
return (
|
9 |
<div className="flex flex-col items-center gap-2">
|
10 |
+
<p className="text-lg font-semibold text-gray-900">🕹️ {t.welcome.contest.prize} 🧑🍳</p>
|
11 |
<Dialog>
|
12 |
<DialogTrigger asChild>
|
13 |
+
<button className="inline-flex items-center text-sm hover:text-primary text-gray-600">
|
14 |
{t.welcome.contest.terms} <Info className="h-4 w-4 ml-1" />
|
15 |
</button>
|
16 |
</DialogTrigger>
|
src/components/game/welcome/HuggingFaceLink.tsx
CHANGED
@@ -3,13 +3,13 @@ import { useTranslation } from "@/hooks/useTranslation";
|
|
3 |
|
4 |
export const HuggingFaceLink = () => {
|
5 |
const t = useTranslation();
|
6 |
-
|
7 |
return (
|
8 |
<div className="flex flex-col items-center gap-2">
|
9 |
<p className="text-muted-foreground">{t.welcome.likeGameText}</p>
|
10 |
-
<a
|
11 |
-
href="https://huggingface.co/spaces/Mistral-AI-Game-Jam/description-improv/tree/main"
|
12 |
-
target="_blank"
|
13 |
rel="noopener noreferrer"
|
14 |
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-primary hover:text-primary/90 transition-colors border border-primary/20 rounded-md hover:border-primary/40"
|
15 |
>
|
|
|
3 |
|
4 |
export const HuggingFaceLink = () => {
|
5 |
const t = useTranslation();
|
6 |
+
|
7 |
return (
|
8 |
<div className="flex flex-col items-center gap-2">
|
9 |
<p className="text-muted-foreground">{t.welcome.likeGameText}</p>
|
10 |
+
<a
|
11 |
+
href="https://huggingface.co/spaces/Mistral-AI-Game-Jam/description-improv/tree/main"
|
12 |
+
target="_blank"
|
13 |
rel="noopener noreferrer"
|
14 |
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-primary hover:text-primary/90 transition-colors border border-primary/20 rounded-md hover:border-primary/40"
|
15 |
>
|
src/components/game/welcome/MainActions.tsx
CHANGED
@@ -2,34 +2,41 @@ import { Button } from "@/components/ui/button";
|
|
2 |
import { useTranslation } from "@/hooks/useTranslation";
|
3 |
|
4 |
interface MainActionsProps {
|
5 |
-
|
|
|
6 |
onShowHowToPlay: () => void;
|
7 |
onShowHighScores: () => void;
|
8 |
}
|
9 |
|
10 |
-
export const MainActions = ({
|
11 |
const t = useTranslation();
|
12 |
-
|
13 |
return (
|
14 |
<div className="space-y-4">
|
15 |
<Button
|
16 |
-
onClick={
|
17 |
className="w-full bg-primary text-lg hover:bg-primary/90"
|
18 |
>
|
19 |
-
{t.welcome.
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
</Button>
|
21 |
<div className="grid grid-cols-2 gap-4">
|
22 |
<Button
|
23 |
onClick={onShowHowToPlay}
|
24 |
variant="outline"
|
25 |
-
className="text-lg"
|
26 |
>
|
27 |
{t.welcome.howToPlay} 📖
|
28 |
</Button>
|
29 |
<Button
|
30 |
onClick={onShowHighScores}
|
31 |
variant="outline"
|
32 |
-
className="text-lg"
|
33 |
>
|
34 |
{t.welcome.leaderboard} 🏆
|
35 |
</Button>
|
|
|
2 |
import { useTranslation } from "@/hooks/useTranslation";
|
3 |
|
4 |
interface MainActionsProps {
|
5 |
+
onStartDaily: () => void;
|
6 |
+
onStartNew: () => void;
|
7 |
onShowHowToPlay: () => void;
|
8 |
onShowHighScores: () => void;
|
9 |
}
|
10 |
|
11 |
+
export const MainActions = ({ onStartDaily: onStartDaily, onStartNew: onStartNew, onShowHowToPlay, onShowHighScores }: MainActionsProps) => {
|
12 |
const t = useTranslation();
|
13 |
+
|
14 |
return (
|
15 |
<div className="space-y-4">
|
16 |
<Button
|
17 |
+
onClick={onStartDaily}
|
18 |
className="w-full bg-primary text-lg hover:bg-primary/90"
|
19 |
>
|
20 |
+
{t.welcome.startDailyButton} ⏎
|
21 |
+
</Button>
|
22 |
+
<Button
|
23 |
+
onClick={onStartNew}
|
24 |
+
className="w-full bg-secondary text-lg hover:bg-secondary/90"
|
25 |
+
>
|
26 |
+
{t.welcome.startNewButton}
|
27 |
</Button>
|
28 |
<div className="grid grid-cols-2 gap-4">
|
29 |
<Button
|
30 |
onClick={onShowHowToPlay}
|
31 |
variant="outline"
|
32 |
+
className="text-lg hover:text-white"
|
33 |
>
|
34 |
{t.welcome.howToPlay} 📖
|
35 |
</Button>
|
36 |
<Button
|
37 |
onClick={onShowHighScores}
|
38 |
variant="outline"
|
39 |
+
className="text-lg hover:text-white"
|
40 |
>
|
41 |
{t.welcome.leaderboard} 🏆
|
42 |
</Button>
|
src/hooks/useTranslation.ts
CHANGED
@@ -4,6 +4,5 @@ import { translations } from '@/i18n/translations';
|
|
4 |
|
5 |
export const useTranslation = () => {
|
6 |
const { language } = useContext(LanguageContext);
|
7 |
-
console.log('[useTranslation] Getting translations for language:', language);
|
8 |
return translations[language];
|
9 |
};
|
|
|
4 |
|
5 |
export const useTranslation = () => {
|
6 |
const { language } = useContext(LanguageContext);
|
|
|
7 |
return translations[language];
|
8 |
};
|
src/i18n/translations/de.ts
CHANGED
@@ -22,7 +22,42 @@ export const de = {
|
|
22 |
describeWord: "Dein Ziel ist es folgendes Wort zu beschreiben",
|
23 |
nextRound: "Nächste Runde",
|
24 |
playAgain: "Erneut spielen",
|
25 |
-
saveScore: "Punktzahl speichern"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
},
|
27 |
leaderboard: {
|
28 |
title: "Bestenliste",
|
@@ -41,6 +76,16 @@ export const de = {
|
|
41 |
next: "Nächste",
|
42 |
success: "Punktzahl erfolgreich übermittelt!",
|
43 |
theme: "Thema",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
error: {
|
45 |
invalidName: "Bitte gib einen gültigen Namen ein",
|
46 |
noRounds: "Du musst mindestens eine Runde abschließen",
|
@@ -55,7 +100,7 @@ export const de = {
|
|
55 |
title: "KI-Vermutung",
|
56 |
goalDescription: "Dein Ziel war es folgendes Wort zu beschreiben",
|
57 |
providedDescription: "Du hast folgende Beschreibung gegeben",
|
58 |
-
aiGuessedDescription: "Basierend auf
|
59 |
correct: "Das ist richtig!",
|
60 |
incorrect: "Das ist falsch.",
|
61 |
nextRound: "Nächste Runde",
|
@@ -81,6 +126,9 @@ export const de = {
|
|
81 |
title: "Think in Sync",
|
82 |
subtitle: "Arbeite mit KI zusammen, um einen Hinweis zu erstellen und lass eine andere KI dein geheimes Wort erraten!",
|
83 |
startButton: "Spiel starten",
|
|
|
|
|
|
|
84 |
howToPlay: "Spielanleitung",
|
85 |
leaderboard: "Bestenliste",
|
86 |
credits: "Erstellt während des",
|
@@ -127,4 +175,4 @@ export const de = {
|
|
127 |
]
|
128 |
}
|
129 |
}
|
130 |
-
};
|
|
|
22 |
describeWord: "Dein Ziel ist es folgendes Wort zu beschreiben",
|
23 |
nextRound: "Nächste Runde",
|
24 |
playAgain: "Erneut spielen",
|
25 |
+
saveScore: "Punktzahl speichern",
|
26 |
+
playNewWords: "Neue Wörter spielen",
|
27 |
+
review: {
|
28 |
+
title: "Spielübersicht",
|
29 |
+
successfulRounds: "Erfolgreiche Runden",
|
30 |
+
description: "Hier ist dein Ergebnis:",
|
31 |
+
playAgain: "Gleiche Wörter erneut spielen",
|
32 |
+
playNewWords: "Neue Wörter spielen",
|
33 |
+
saveScore: "Punktzahl speichern",
|
34 |
+
shareGame: "Teilen",
|
35 |
+
urlCopied: "URL kopiert!",
|
36 |
+
urlCopiedDesc: "Teile diese URL mit Freunden, damit sie mit den gleichen Wörtern spielen können",
|
37 |
+
urlCopyError: "URL konnte nicht kopiert werden",
|
38 |
+
urlCopyErrorDesc: "Bitte versuche die URL manuell zu kopieren",
|
39 |
+
youWin: "Du hast gewonnen!",
|
40 |
+
youLost: "Du hast verloren!",
|
41 |
+
friendScore: (score: number, avgWords: string) =>
|
42 |
+
`Die Person, die dich herausgefordert hat, hat ${score} Runden erfolgreich mit durchschnittlich ${avgWords} Wörtern abgeschlossen.`,
|
43 |
+
word: "Wort",
|
44 |
+
yourWords: "Du",
|
45 |
+
friendWords: "Freund",
|
46 |
+
result: "Ergebnis",
|
47 |
+
details: "Details",
|
48 |
+
yourDescription: "Deine Beschreibung",
|
49 |
+
friendDescription: "Beschreibung des Freundes",
|
50 |
+
aiGuessed: "KI hat geraten",
|
51 |
+
words: "Wörter"
|
52 |
+
},
|
53 |
+
invitation: {
|
54 |
+
title: "Spieleinladung",
|
55 |
+
description: "Hey, du wurdest zu einem Spiel eingeladen. Spiele jetzt und finde heraus, wie gut du mit denselben Wörtern abschneidest!"
|
56 |
+
},
|
57 |
+
error: {
|
58 |
+
title: "Spiel konnte nicht gestartet werden",
|
59 |
+
description: "Bitte versuche es in einem Moment erneut."
|
60 |
+
}
|
61 |
},
|
62 |
leaderboard: {
|
63 |
title: "Bestenliste",
|
|
|
76 |
next: "Nächste",
|
77 |
success: "Punktzahl erfolgreich übermittelt!",
|
78 |
theme: "Thema",
|
79 |
+
actions: "Aktionen",
|
80 |
+
playSameWords: "Gleiche Wörter spielen",
|
81 |
+
scoreUpdated: "Punktzahl aktualisiert!",
|
82 |
+
scoreUpdatedDesc: "Deine vorherige Punktzahl für dieses Spiel wurde aktualisiert",
|
83 |
+
scoreSubmitted: "Punktzahl eingereicht!",
|
84 |
+
scoreSubmittedDesc: "Deine Punktzahl wurde zur Bestenliste hinzugefügt",
|
85 |
+
modes: {
|
86 |
+
daily: "Tägliche Herausforderung",
|
87 |
+
"all-time": "Bestenliste"
|
88 |
+
},
|
89 |
error: {
|
90 |
invalidName: "Bitte gib einen gültigen Namen ein",
|
91 |
noRounds: "Du musst mindestens eine Runde abschließen",
|
|
|
100 |
title: "KI-Vermutung",
|
101 |
goalDescription: "Dein Ziel war es folgendes Wort zu beschreiben",
|
102 |
providedDescription: "Du hast folgende Beschreibung gegeben",
|
103 |
+
aiGuessedDescription: "Basierend auf dieser Beschreibung hat die KI geraten",
|
104 |
correct: "Das ist richtig!",
|
105 |
incorrect: "Das ist falsch.",
|
106 |
nextRound: "Nächste Runde",
|
|
|
126 |
title: "Think in Sync",
|
127 |
subtitle: "Arbeite mit KI zusammen, um einen Hinweis zu erstellen und lass eine andere KI dein geheimes Wort erraten!",
|
128 |
startButton: "Spiel starten",
|
129 |
+
startDailyButton: "Tägliche Herausforderung",
|
130 |
+
startNewButton: "Neues Spiel",
|
131 |
+
dailyLeaderboard: "Tagesranking",
|
132 |
howToPlay: "Spielanleitung",
|
133 |
leaderboard: "Bestenliste",
|
134 |
credits: "Erstellt während des",
|
|
|
175 |
]
|
176 |
}
|
177 |
}
|
178 |
+
};
|
src/i18n/translations/en.ts
CHANGED
@@ -22,7 +22,41 @@ export const en = {
|
|
22 |
describeWord: "Your goal is to describe the word",
|
23 |
nextRound: "Next Round",
|
24 |
playAgain: "Play Again",
|
25 |
-
saveScore: "Save Score"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
},
|
27 |
leaderboard: {
|
28 |
title: "High Scores",
|
@@ -41,6 +75,16 @@ export const en = {
|
|
41 |
next: "Next",
|
42 |
success: "Score submitted successfully!",
|
43 |
theme: "Theme",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
error: {
|
45 |
invalidName: "Please enter a valid name",
|
46 |
noRounds: "You need to complete at least one round",
|
@@ -55,13 +99,13 @@ export const en = {
|
|
55 |
title: "AI's Guess",
|
56 |
goalDescription: "Your goal was to describe the word",
|
57 |
providedDescription: "You provided the description",
|
58 |
-
aiGuessedDescription: "Based on
|
59 |
correct: "This is right!",
|
60 |
incorrect: "This is wrong.",
|
61 |
nextRound: "Next Round",
|
62 |
playAgain: "Play Again",
|
63 |
viewLeaderboard: "Save your score",
|
64 |
-
cheatingDetected: "Cheating detected!"
|
65 |
},
|
66 |
themes: {
|
67 |
title: "Choose a Theme",
|
@@ -81,6 +125,9 @@ export const en = {
|
|
81 |
title: "Think in Sync",
|
82 |
subtitle: "Team up with AI to craft a clue and have a different AI guess your secret word!",
|
83 |
startButton: "Start Game",
|
|
|
|
|
|
|
84 |
howToPlay: "How to Play",
|
85 |
leaderboard: "Leaderboard",
|
86 |
credits: "Created during the",
|
@@ -127,4 +174,4 @@ export const en = {
|
|
127 |
]
|
128 |
}
|
129 |
}
|
130 |
-
};
|
|
|
22 |
describeWord: "Your goal is to describe the word",
|
23 |
nextRound: "Next Round",
|
24 |
playAgain: "Play Again",
|
25 |
+
saveScore: "Save Score",
|
26 |
+
review: {
|
27 |
+
title: "Game Review",
|
28 |
+
successfulRounds: "Successful Rounds",
|
29 |
+
description: "Here's how you did:",
|
30 |
+
playAgain: "Play same words again",
|
31 |
+
playNewWords: "Play new words",
|
32 |
+
saveScore: "Save Score",
|
33 |
+
shareGame: "Share",
|
34 |
+
urlCopied: "URL Copied!",
|
35 |
+
urlCopiedDesc: "Share this URL with friends to let them play with the same words",
|
36 |
+
urlCopyError: "Failed to copy URL",
|
37 |
+
urlCopyErrorDesc: "Please try copying the URL manually",
|
38 |
+
youWin: "You Won!",
|
39 |
+
youLost: "You Lost!",
|
40 |
+
friendScore: (score: number, avgWords: string) =>
|
41 |
+
`The person that challenged you completed ${score} rounds successfully with an average of ${avgWords} words.`,
|
42 |
+
word: "Word",
|
43 |
+
yourWords: "You",
|
44 |
+
friendWords: "Friend",
|
45 |
+
result: "Result",
|
46 |
+
details: "Details",
|
47 |
+
yourDescription: "Your Description",
|
48 |
+
friendDescription: "Friend's Description",
|
49 |
+
aiGuessed: "AI guessed",
|
50 |
+
words: "Words"
|
51 |
+
},
|
52 |
+
invitation: {
|
53 |
+
title: "Game Invitation",
|
54 |
+
description: "Hey, you got invited to play a game. Play now to find out how well you do on the same set of words!"
|
55 |
+
},
|
56 |
+
error: {
|
57 |
+
title: "Game could not be started",
|
58 |
+
description: "Please try again in a moment."
|
59 |
+
}
|
60 |
},
|
61 |
leaderboard: {
|
62 |
title: "High Scores",
|
|
|
75 |
next: "Next",
|
76 |
success: "Score submitted successfully!",
|
77 |
theme: "Theme",
|
78 |
+
actions: "Actions",
|
79 |
+
playSameWords: "Play same words",
|
80 |
+
scoreUpdated: "Score Updated!",
|
81 |
+
scoreUpdatedDesc: "Your previous score for this game has been updated",
|
82 |
+
scoreSubmitted: "Score Submitted!",
|
83 |
+
scoreSubmittedDesc: "Your score has been added to the leaderboard",
|
84 |
+
modes: {
|
85 |
+
daily: "Daily Challenge",
|
86 |
+
"all-time": "All Time"
|
87 |
+
},
|
88 |
error: {
|
89 |
invalidName: "Please enter a valid name",
|
90 |
noRounds: "You need to complete at least one round",
|
|
|
99 |
title: "AI's Guess",
|
100 |
goalDescription: "Your goal was to describe the word",
|
101 |
providedDescription: "You provided the description",
|
102 |
+
aiGuessedDescription: "Based on this description, the AI guessed",
|
103 |
correct: "This is right!",
|
104 |
incorrect: "This is wrong.",
|
105 |
nextRound: "Next Round",
|
106 |
playAgain: "Play Again",
|
107 |
viewLeaderboard: "Save your score",
|
108 |
+
cheatingDetected: "Cheating detected!",
|
109 |
},
|
110 |
themes: {
|
111 |
title: "Choose a Theme",
|
|
|
125 |
title: "Think in Sync",
|
126 |
subtitle: "Team up with AI to craft a clue and have a different AI guess your secret word!",
|
127 |
startButton: "Start Game",
|
128 |
+
startDailyButton: "Daily Challenge",
|
129 |
+
startNewButton: "New Game",
|
130 |
+
dailyLeaderboard: "Today's Ranking",
|
131 |
howToPlay: "How to Play",
|
132 |
leaderboard: "Leaderboard",
|
133 |
credits: "Created during the",
|
|
|
174 |
]
|
175 |
}
|
176 |
}
|
177 |
+
};
|
src/i18n/translations/es.ts
CHANGED
@@ -22,7 +22,42 @@ export const es = {
|
|
22 |
describeWord: "Tu objetivo es describir la palabra",
|
23 |
nextRound: "Siguiente Ronda",
|
24 |
playAgain: "Jugar de Nuevo",
|
25 |
-
saveScore: "Guardar Puntuación"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
},
|
27 |
leaderboard: {
|
28 |
title: "Puntuaciones Más Altas",
|
@@ -41,6 +76,16 @@ export const es = {
|
|
41 |
next: "Siguiente",
|
42 |
success: "¡Puntuación enviada con éxito!",
|
43 |
theme: "Tema",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
error: {
|
45 |
invalidName: "Por favor, ingresa un nombre válido",
|
46 |
noRounds: "Debes completar al menos una ronda",
|
@@ -55,7 +100,7 @@ export const es = {
|
|
55 |
title: "Suposición de la IA",
|
56 |
goalDescription: "Tu objetivo era describir la palabra",
|
57 |
providedDescription: "Proporcionaste la descripción",
|
58 |
-
aiGuessedDescription: "
|
59 |
correct: "¡Esto es correcto!",
|
60 |
incorrect: "Esto es incorrecto.",
|
61 |
nextRound: "Siguiente Ronda",
|
@@ -81,6 +126,9 @@ export const es = {
|
|
81 |
title: "Think in Sync",
|
82 |
subtitle: "¡Forma equipo con la IA para crear una pista y deja que otra IA adivine tu palabra secreta!",
|
83 |
startButton: "Comenzar juego",
|
|
|
|
|
|
|
84 |
howToPlay: "Cómo jugar",
|
85 |
leaderboard: "Clasificación",
|
86 |
credits: "Creado durante el",
|
@@ -127,4 +175,4 @@ export const es = {
|
|
127 |
]
|
128 |
}
|
129 |
}
|
130 |
-
};
|
|
|
22 |
describeWord: "Tu objetivo es describir la palabra",
|
23 |
nextRound: "Siguiente Ronda",
|
24 |
playAgain: "Jugar de Nuevo",
|
25 |
+
saveScore: "Guardar Puntuación",
|
26 |
+
playNewWords: "Jugar nuevas palabras",
|
27 |
+
review: {
|
28 |
+
title: "Resumen del Juego",
|
29 |
+
successfulRounds: "Rondas Exitosas",
|
30 |
+
description: "Aquí están tus resultados:",
|
31 |
+
playAgain: "Jugar las mismas palabras de nuevo",
|
32 |
+
playNewWords: "Jugar nuevas palabras",
|
33 |
+
saveScore: "Guardar Puntuación",
|
34 |
+
shareGame: "Compartir",
|
35 |
+
urlCopied: "¡URL copiada!",
|
36 |
+
urlCopiedDesc: "Comparte esta URL con amigos para que jueguen con las mismas palabras",
|
37 |
+
urlCopyError: "Error al copiar la URL",
|
38 |
+
urlCopyErrorDesc: "Por favor, intenta copiar la URL manualmente",
|
39 |
+
youWin: "¡Has ganado!",
|
40 |
+
youLost: "¡Has perdido!",
|
41 |
+
friendScore: (score: number, avgWords: string) =>
|
42 |
+
`La persona que te desafió completó ${score} rondas exitosamente con un promedio de ${avgWords} palabras.`,
|
43 |
+
word: "Palabra",
|
44 |
+
yourWords: "Tú",
|
45 |
+
friendWords: "Amigo",
|
46 |
+
result: "Resultado",
|
47 |
+
details: "Detalles",
|
48 |
+
yourDescription: "Tu Descripción",
|
49 |
+
friendDescription: "Descripción del Amigo",
|
50 |
+
aiGuessed: "La IA adivinó",
|
51 |
+
words: "Palabras"
|
52 |
+
},
|
53 |
+
invitation: {
|
54 |
+
title: "Invitación al Juego",
|
55 |
+
description: "¡Hey, has sido invitado a jugar! ¡Juega ahora para descubrir qué tan bien lo haces con las mismas palabras!"
|
56 |
+
},
|
57 |
+
error: {
|
58 |
+
title: "No se pudo iniciar el juego",
|
59 |
+
description: "Por favor, inténtalo de nuevo en un momento."
|
60 |
+
}
|
61 |
},
|
62 |
leaderboard: {
|
63 |
title: "Puntuaciones Más Altas",
|
|
|
76 |
next: "Siguiente",
|
77 |
success: "¡Puntuación enviada con éxito!",
|
78 |
theme: "Tema",
|
79 |
+
actions: "Acciones",
|
80 |
+
playSameWords: "Jugar con las mismas palabras",
|
81 |
+
scoreUpdated: "¡Puntuación actualizada!",
|
82 |
+
scoreUpdatedDesc: "Tu puntuación anterior para este juego ha sido actualizada",
|
83 |
+
scoreSubmitted: "¡Puntuación enviada!",
|
84 |
+
scoreSubmittedDesc: "Tu puntuación ha sido añadida a la tabla de clasificación",
|
85 |
+
modes: {
|
86 |
+
daily: "Desafío Diario",
|
87 |
+
"all-time": "Histórico"
|
88 |
+
},
|
89 |
error: {
|
90 |
invalidName: "Por favor, ingresa un nombre válido",
|
91 |
noRounds: "Debes completar al menos una ronda",
|
|
|
100 |
title: "Suposición de la IA",
|
101 |
goalDescription: "Tu objetivo era describir la palabra",
|
102 |
providedDescription: "Proporcionaste la descripción",
|
103 |
+
aiGuessedDescription: "Basándose en esta descripción, la IA adivinó",
|
104 |
correct: "¡Esto es correcto!",
|
105 |
incorrect: "Esto es incorrecto.",
|
106 |
nextRound: "Siguiente Ronda",
|
|
|
126 |
title: "Think in Sync",
|
127 |
subtitle: "¡Forma equipo con la IA para crear una pista y deja que otra IA adivine tu palabra secreta!",
|
128 |
startButton: "Comenzar juego",
|
129 |
+
startDailyButton: "Desafío Diario",
|
130 |
+
startNewButton: "Nuevo Juego",
|
131 |
+
dailyLeaderboard: "Ranking diario",
|
132 |
howToPlay: "Cómo jugar",
|
133 |
leaderboard: "Clasificación",
|
134 |
credits: "Creado durante el",
|
|
|
175 |
]
|
176 |
}
|
177 |
}
|
178 |
+
};
|
src/i18n/translations/fr.ts
CHANGED
@@ -3,6 +3,7 @@ export const fr = {
|
|
3 |
title: "Think in Sync",
|
4 |
round: "Tour",
|
5 |
buildDescription: "Construisez une phrase ensemble",
|
|
|
6 |
startSentence: "Commencez à construire votre phrase...",
|
7 |
inputPlaceholder: "Entrez UN mot...",
|
8 |
addWord: "Ajouter un mot",
|
@@ -21,7 +22,41 @@ export const fr = {
|
|
21 |
describeWord: "Votre objectif est de décrire le mot",
|
22 |
nextRound: "Tour Suivant",
|
23 |
playAgain: "Rejouer",
|
24 |
-
saveScore: "Sauvegarder le Score"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
},
|
26 |
leaderboard: {
|
27 |
title: "Meilleurs Scores",
|
@@ -40,6 +75,16 @@ export const fr = {
|
|
40 |
next: "Suivant",
|
41 |
success: "Score soumis avec succès !",
|
42 |
theme: "Thème",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
error: {
|
44 |
invalidName: "Veuillez entrer un nom valide",
|
45 |
noRounds: "Vous devez compléter au moins un tour",
|
@@ -54,7 +99,7 @@ export const fr = {
|
|
54 |
title: "Devinette de l'IA",
|
55 |
goalDescription: "Votre objectif était de décrire le mot",
|
56 |
providedDescription: "Vous avez fourni la description",
|
57 |
-
aiGuessedDescription: "
|
58 |
correct: "C'est correct !",
|
59 |
incorrect: "C'est incorrect.",
|
60 |
nextRound: "Tour Suivant",
|
@@ -78,8 +123,11 @@ export const fr = {
|
|
78 |
},
|
79 |
welcome: {
|
80 |
title: "Think in Sync",
|
81 |
-
subtitle: "
|
82 |
startButton: "Commencer",
|
|
|
|
|
|
|
83 |
howToPlay: "Comment Jouer",
|
84 |
leaderboard: "Classement",
|
85 |
credits: "Créé pendant le",
|
@@ -126,4 +174,4 @@ export const fr = {
|
|
126 |
]
|
127 |
}
|
128 |
}
|
129 |
-
};
|
|
|
3 |
title: "Think in Sync",
|
4 |
round: "Tour",
|
5 |
buildDescription: "Construisez une phrase ensemble",
|
6 |
+
buildSubtitle: "Ajoutez des mots à tour de rôle pour créer une phrase",
|
7 |
startSentence: "Commencez à construire votre phrase...",
|
8 |
inputPlaceholder: "Entrez UN mot...",
|
9 |
addWord: "Ajouter un mot",
|
|
|
22 |
describeWord: "Votre objectif est de décrire le mot",
|
23 |
nextRound: "Tour Suivant",
|
24 |
playAgain: "Rejouer",
|
25 |
+
saveScore: "Sauvegarder le Score",
|
26 |
+
review: {
|
27 |
+
title: "Résumé de la Partie",
|
28 |
+
successfulRounds: "Manches Réussies",
|
29 |
+
description: "Voici vos résultats :",
|
30 |
+
playAgain: "Rejouer avec les mêmes mots",
|
31 |
+
playNewWords: "Jouer avec de nouveaux mots",
|
32 |
+
saveScore: "Sauvegarder le Score",
|
33 |
+
shareGame: "Partager",
|
34 |
+
urlCopied: "URL copiée !",
|
35 |
+
urlCopiedDesc: "Partagez cette URL avec vos amis pour qu'ils jouent avec les mêmes mots",
|
36 |
+
urlCopyError: "Échec de la copie de l'URL",
|
37 |
+
urlCopyErrorDesc: "Veuillez essayer de copier l'URL manuellement",
|
38 |
+
youWin: "Vous avez gagné !",
|
39 |
+
youLost: "Vous avez perdu !",
|
40 |
+
friendScore: (score: number, avgWords: string) =>
|
41 |
+
`La personne qui vous a défié a complété ${score} manches avec une moyenne de ${avgWords} mots.`,
|
42 |
+
word: "Mot",
|
43 |
+
yourWords: "Vous",
|
44 |
+
friendWords: "Ami",
|
45 |
+
result: "Résultat",
|
46 |
+
details: "Détails",
|
47 |
+
yourDescription: "Votre Description",
|
48 |
+
friendDescription: "Description de l'Ami",
|
49 |
+
aiGuessed: "L'IA a deviné",
|
50 |
+
words: "Mots"
|
51 |
+
},
|
52 |
+
invitation: {
|
53 |
+
title: "Invitation au Jeu",
|
54 |
+
description: "Hey, tu as été invité à jouer. Joue maintenant pour découvrir comment tu te débrouilles avec les mêmes mots !"
|
55 |
+
},
|
56 |
+
error: {
|
57 |
+
title: "Le jeu n'a pas pu être démarré",
|
58 |
+
description: "Veuillez réessayer dans un moment."
|
59 |
+
}
|
60 |
},
|
61 |
leaderboard: {
|
62 |
title: "Meilleurs Scores",
|
|
|
75 |
next: "Suivant",
|
76 |
success: "Score soumis avec succès !",
|
77 |
theme: "Thème",
|
78 |
+
actions: "Actions",
|
79 |
+
playSameWords: "Jouer avec les mêmes mots",
|
80 |
+
scoreUpdated: "Score mis à jour !",
|
81 |
+
scoreUpdatedDesc: "Votre score précédent pour ce jeu a été mis à jour",
|
82 |
+
scoreSubmitted: "Score soumis !",
|
83 |
+
scoreSubmittedDesc: "Votre score a été ajouté au classement",
|
84 |
+
modes: {
|
85 |
+
daily: "Défi du Jour",
|
86 |
+
"all-time": "Historique"
|
87 |
+
},
|
88 |
error: {
|
89 |
invalidName: "Veuillez entrer un nom valide",
|
90 |
noRounds: "Vous devez compléter au moins un tour",
|
|
|
99 |
title: "Devinette de l'IA",
|
100 |
goalDescription: "Votre objectif était de décrire le mot",
|
101 |
providedDescription: "Vous avez fourni la description",
|
102 |
+
aiGuessedDescription: "Sur la base de cette description, l'IA a deviné",
|
103 |
correct: "C'est correct !",
|
104 |
incorrect: "C'est incorrect.",
|
105 |
nextRound: "Tour Suivant",
|
|
|
123 |
},
|
124 |
welcome: {
|
125 |
title: "Think in Sync",
|
126 |
+
subtitle: "Collaborez avec une IA pour créer un indice, puis laissez-en une autre deviner votre mot secret !",
|
127 |
startButton: "Commencer",
|
128 |
+
startDailyButton: "Défi du Jour",
|
129 |
+
startNewButton: "Nouvelle Partie",
|
130 |
+
dailyLeaderboard: "Classement du jour",
|
131 |
howToPlay: "Comment Jouer",
|
132 |
leaderboard: "Classement",
|
133 |
credits: "Créé pendant le",
|
|
|
174 |
]
|
175 |
}
|
176 |
}
|
177 |
+
};
|
src/i18n/translations/it.ts
CHANGED
@@ -22,7 +22,41 @@ export const it = {
|
|
22 |
describeWord: "Il tuo obiettivo è descrivere la parola",
|
23 |
nextRound: "Prossimo Turno",
|
24 |
playAgain: "Gioca Ancora",
|
25 |
-
saveScore: "Salva Punteggio"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
},
|
27 |
leaderboard: {
|
28 |
title: "Punteggi Migliori",
|
@@ -41,6 +75,16 @@ export const it = {
|
|
41 |
next: "Successivo",
|
42 |
success: "Punteggio inviato con successo!",
|
43 |
theme: "Tema",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
error: {
|
45 |
invalidName: "Inserisci un nome valido",
|
46 |
noRounds: "Devi completare almeno un turno",
|
@@ -57,7 +101,7 @@ export const it = {
|
|
57 |
aiGuessed: "L'IA ha indovinato",
|
58 |
goalDescription: "Il tuo obiettivo era descrivere la parola",
|
59 |
providedDescription: "Hai fornito la descrizione",
|
60 |
-
aiGuessedDescription: "Basandosi
|
61 |
correct: "Corretto! L'IA ha indovinato la parola!",
|
62 |
incorrect: "Sbagliato. Riprova!",
|
63 |
nextRound: "Prossimo Turno",
|
@@ -83,6 +127,9 @@ export const it = {
|
|
83 |
title: "Think in Sync",
|
84 |
subtitle: "Fai squadra con l'IA per creare un indizio e lascia che un'altra IA indovini la tua parola segreta!",
|
85 |
startButton: "Inizia gioco",
|
|
|
|
|
|
|
86 |
howToPlay: "Come giocare",
|
87 |
leaderboard: "Classifica",
|
88 |
credits: "Creato durante il",
|
@@ -129,4 +176,4 @@ export const it = {
|
|
129 |
]
|
130 |
}
|
131 |
}
|
132 |
-
};
|
|
|
22 |
describeWord: "Il tuo obiettivo è descrivere la parola",
|
23 |
nextRound: "Prossimo Turno",
|
24 |
playAgain: "Gioca Ancora",
|
25 |
+
saveScore: "Salva Punteggio",
|
26 |
+
review: {
|
27 |
+
title: "Riepilogo Partita",
|
28 |
+
successfulRounds: "Turni Riusciti",
|
29 |
+
description: "Ecco i tuoi risultati:",
|
30 |
+
playAgain: "Gioca di nuovo le stesse parole",
|
31 |
+
playNewWords: "Gioca nuove parole",
|
32 |
+
saveScore: "Salva Punteggio",
|
33 |
+
shareGame: "Condividi",
|
34 |
+
urlCopied: "URL copiato!",
|
35 |
+
urlCopiedDesc: "Condividi questo URL con gli amici per farli giocare con le stesse parole",
|
36 |
+
urlCopyError: "Impossibile copiare l'URL",
|
37 |
+
urlCopyErrorDesc: "Prova a copiare l'URL manualmente",
|
38 |
+
youWin: "Hai vinto!",
|
39 |
+
youLost: "Hai perso!",
|
40 |
+
friendScore: (score: number, avgWords: string) =>
|
41 |
+
`La persona che ti ha sfidato ha completato ${score} turni con una media di ${avgWords} parole.`,
|
42 |
+
word: "Parola",
|
43 |
+
yourWords: "Tu",
|
44 |
+
friendWords: "Amico",
|
45 |
+
result: "Risultato",
|
46 |
+
details: "Dettagli",
|
47 |
+
yourDescription: "La Tua Descrizione",
|
48 |
+
friendDescription: "Descrizione dell'Amico",
|
49 |
+
aiGuessed: "L'IA ha indovinato",
|
50 |
+
words: "Parole"
|
51 |
+
},
|
52 |
+
invitation: {
|
53 |
+
title: "Invito al Gioco",
|
54 |
+
description: "Ehi, sei stato invitato a giocare. Gioca ora per scoprire come te la cavi con le stesse parole!"
|
55 |
+
},
|
56 |
+
error: {
|
57 |
+
title: "Impossibile avviare il gioco",
|
58 |
+
description: "Per favore riprova tra un momento."
|
59 |
+
}
|
60 |
},
|
61 |
leaderboard: {
|
62 |
title: "Punteggi Migliori",
|
|
|
75 |
next: "Successivo",
|
76 |
success: "Punteggio inviato con successo!",
|
77 |
theme: "Tema",
|
78 |
+
actions: "Azioni",
|
79 |
+
playSameWords: "Gioca le stesse parole",
|
80 |
+
scoreUpdated: "Punteggio aggiornato!",
|
81 |
+
scoreUpdatedDesc: "Il tuo punteggio precedente per questo gioco è stato aggiornato",
|
82 |
+
scoreSubmitted: "Punteggio inviato!",
|
83 |
+
scoreSubmittedDesc: "Il tuo punteggio è stato aggiunto alla classifica",
|
84 |
+
modes: {
|
85 |
+
daily: "Sfida Giornaliera",
|
86 |
+
"all-time": "Classifica Generale"
|
87 |
+
},
|
88 |
error: {
|
89 |
invalidName: "Inserisci un nome valido",
|
90 |
noRounds: "Devi completare almeno un turno",
|
|
|
101 |
aiGuessed: "L'IA ha indovinato",
|
102 |
goalDescription: "Il tuo obiettivo era descrivere la parola",
|
103 |
providedDescription: "Hai fornito la descrizione",
|
104 |
+
aiGuessedDescription: "Basandosi su questa descrizione, l'IA ha indovinato",
|
105 |
correct: "Corretto! L'IA ha indovinato la parola!",
|
106 |
incorrect: "Sbagliato. Riprova!",
|
107 |
nextRound: "Prossimo Turno",
|
|
|
127 |
title: "Think in Sync",
|
128 |
subtitle: "Fai squadra con l'IA per creare un indizio e lascia che un'altra IA indovini la tua parola segreta!",
|
129 |
startButton: "Inizia gioco",
|
130 |
+
startDailyButton: "Sfida Giornaliera",
|
131 |
+
startNewButton: "Nuova Partita",
|
132 |
+
dailyLeaderboard: "Classifica di oggi",
|
133 |
howToPlay: "Come giocare",
|
134 |
leaderboard: "Classifica",
|
135 |
credits: "Creato durante il",
|
|
|
176 |
]
|
177 |
}
|
178 |
}
|
179 |
+
};
|
src/integrations/supabase/types.ts
CHANGED
@@ -9,6 +9,35 @@ export type Json =
|
|
9 |
export type Database = {
|
10 |
public: {
|
11 |
Tables: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
game_results: {
|
13 |
Row: {
|
14 |
ai_guess: string
|
@@ -39,10 +68,35 @@ export type Database = {
|
|
39 |
}
|
40 |
Relationships: []
|
41 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
high_scores: {
|
43 |
Row: {
|
44 |
avg_words_per_round: number
|
45 |
created_at: string
|
|
|
46 |
id: string
|
47 |
player_name: string
|
48 |
score: number
|
@@ -52,6 +106,7 @@ export type Database = {
|
|
52 |
Insert: {
|
53 |
avg_words_per_round: number
|
54 |
created_at?: string
|
|
|
55 |
id?: string
|
56 |
player_name: string
|
57 |
score: number
|
@@ -61,13 +116,48 @@ export type Database = {
|
|
61 |
Update: {
|
62 |
avg_words_per_round?: number
|
63 |
created_at?: string
|
|
|
64 |
id?: string
|
65 |
player_name?: string
|
66 |
score?: number
|
67 |
session_id?: string
|
68 |
theme?: string
|
69 |
}
|
70 |
-
Relationships: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
}
|
72 |
user_roles: {
|
73 |
Row: {
|
@@ -102,8 +192,12 @@ export type Database = {
|
|
102 |
p_avg_words_per_round: number
|
103 |
p_session_id: string
|
104 |
p_theme?: string
|
|
|
105 |
}
|
106 |
-
Returns:
|
|
|
|
|
|
|
107 |
}
|
108 |
is_admin: {
|
109 |
Args: {
|
|
|
9 |
export type Database = {
|
10 |
public: {
|
11 |
Tables: {
|
12 |
+
daily_challenges: {
|
13 |
+
Row: {
|
14 |
+
created_at: string
|
15 |
+
game_id: string
|
16 |
+
id: string
|
17 |
+
is_active: boolean
|
18 |
+
}
|
19 |
+
Insert: {
|
20 |
+
created_at?: string
|
21 |
+
game_id: string
|
22 |
+
id?: string
|
23 |
+
is_active?: boolean
|
24 |
+
}
|
25 |
+
Update: {
|
26 |
+
created_at?: string
|
27 |
+
game_id?: string
|
28 |
+
id?: string
|
29 |
+
is_active?: boolean
|
30 |
+
}
|
31 |
+
Relationships: [
|
32 |
+
{
|
33 |
+
foreignKeyName: "daily_challenges_game_id_fkey"
|
34 |
+
columns: ["game_id"]
|
35 |
+
isOneToOne: true
|
36 |
+
referencedRelation: "games"
|
37 |
+
referencedColumns: ["id"]
|
38 |
+
},
|
39 |
+
]
|
40 |
+
}
|
41 |
game_results: {
|
42 |
Row: {
|
43 |
ai_guess: string
|
|
|
68 |
}
|
69 |
Relationships: []
|
70 |
}
|
71 |
+
games: {
|
72 |
+
Row: {
|
73 |
+
created_at: string
|
74 |
+
id: string
|
75 |
+
language: string | null
|
76 |
+
theme: string
|
77 |
+
words: string[]
|
78 |
+
}
|
79 |
+
Insert: {
|
80 |
+
created_at?: string
|
81 |
+
id?: string
|
82 |
+
language?: string | null
|
83 |
+
theme: string
|
84 |
+
words: string[]
|
85 |
+
}
|
86 |
+
Update: {
|
87 |
+
created_at?: string
|
88 |
+
id?: string
|
89 |
+
language?: string | null
|
90 |
+
theme?: string
|
91 |
+
words?: string[]
|
92 |
+
}
|
93 |
+
Relationships: []
|
94 |
+
}
|
95 |
high_scores: {
|
96 |
Row: {
|
97 |
avg_words_per_round: number
|
98 |
created_at: string
|
99 |
+
game_id: string | null
|
100 |
id: string
|
101 |
player_name: string
|
102 |
score: number
|
|
|
106 |
Insert: {
|
107 |
avg_words_per_round: number
|
108 |
created_at?: string
|
109 |
+
game_id?: string | null
|
110 |
id?: string
|
111 |
player_name: string
|
112 |
score: number
|
|
|
116 |
Update: {
|
117 |
avg_words_per_round?: number
|
118 |
created_at?: string
|
119 |
+
game_id?: string | null
|
120 |
id?: string
|
121 |
player_name?: string
|
122 |
score?: number
|
123 |
session_id?: string
|
124 |
theme?: string
|
125 |
}
|
126 |
+
Relationships: [
|
127 |
+
{
|
128 |
+
foreignKeyName: "high_scores_game_id_fkey"
|
129 |
+
columns: ["game_id"]
|
130 |
+
isOneToOne: false
|
131 |
+
referencedRelation: "games"
|
132 |
+
referencedColumns: ["id"]
|
133 |
+
},
|
134 |
+
]
|
135 |
+
}
|
136 |
+
sessions: {
|
137 |
+
Row: {
|
138 |
+
created_at: string
|
139 |
+
game_id: string
|
140 |
+
id: string
|
141 |
+
}
|
142 |
+
Insert: {
|
143 |
+
created_at?: string
|
144 |
+
game_id: string
|
145 |
+
id?: string
|
146 |
+
}
|
147 |
+
Update: {
|
148 |
+
created_at?: string
|
149 |
+
game_id?: string
|
150 |
+
id?: string
|
151 |
+
}
|
152 |
+
Relationships: [
|
153 |
+
{
|
154 |
+
foreignKeyName: "sessions_game_id_fkey"
|
155 |
+
columns: ["game_id"]
|
156 |
+
isOneToOne: false
|
157 |
+
referencedRelation: "games"
|
158 |
+
referencedColumns: ["id"]
|
159 |
+
},
|
160 |
+
]
|
161 |
}
|
162 |
user_roles: {
|
163 |
Row: {
|
|
|
192 |
p_avg_words_per_round: number
|
193 |
p_session_id: string
|
194 |
p_theme?: string
|
195 |
+
p_game_id?: string
|
196 |
}
|
197 |
+
Returns: {
|
198 |
+
success: boolean
|
199 |
+
is_update: boolean
|
200 |
+
}[]
|
201 |
}
|
202 |
is_admin: {
|
203 |
Args: {
|
src/services/dailyGameService.ts
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { supabase } from "@/integrations/supabase/client";
|
2 |
+
|
3 |
+
export const getDailyGame = async (language: string = 'en'): Promise<string> => {
|
4 |
+
console.log('Fetching daily game for language:', language);
|
5 |
+
|
6 |
+
try {
|
7 |
+
// First try to get a daily challenge in the user's language
|
8 |
+
let { data: dailyChallenge, error } = await supabase
|
9 |
+
.from('daily_challenges')
|
10 |
+
.select('game_id, games!inner(language)')
|
11 |
+
.eq('is_active', true)
|
12 |
+
.eq('games.language', language)
|
13 |
+
.maybeSingle();
|
14 |
+
|
15 |
+
// If no challenge exists in user's language, fall back to English
|
16 |
+
if (!dailyChallenge) {
|
17 |
+
console.log('No daily challenge found for language:', language, 'falling back to English');
|
18 |
+
const { data: englishChallenge, error: englishError } = await supabase
|
19 |
+
.from('daily_challenges')
|
20 |
+
.select('game_id, games!inner(language)')
|
21 |
+
.eq('is_active', true)
|
22 |
+
.eq('games.language', 'en')
|
23 |
+
.maybeSingle();
|
24 |
+
|
25 |
+
if (englishError) throw englishError;
|
26 |
+
if (!englishChallenge) throw new Error('No active daily challenge found');
|
27 |
+
|
28 |
+
dailyChallenge = englishChallenge;
|
29 |
+
}
|
30 |
+
|
31 |
+
console.log('Found daily game:', dailyChallenge.game_id);
|
32 |
+
return dailyChallenge.game_id;
|
33 |
+
} catch (error) {
|
34 |
+
console.error('Error fetching daily game:', error);
|
35 |
+
throw error;
|
36 |
+
}
|
37 |
+
};
|
38 |
+
|
39 |
+
interface DailyGameInfo {
|
40 |
+
game_id: string;
|
41 |
+
language: string;
|
42 |
+
}
|
43 |
+
|
44 |
+
export const getDailyGames = async (): Promise<DailyGameInfo[]> => {
|
45 |
+
try {
|
46 |
+
const { data: dailyChallenges, error } = await supabase
|
47 |
+
.from('daily_challenges')
|
48 |
+
.select('game_id, games!inner(language)')
|
49 |
+
.eq('is_active', true);
|
50 |
+
|
51 |
+
if (error) throw error;
|
52 |
+
if (!dailyChallenges) return [];
|
53 |
+
|
54 |
+
return dailyChallenges.map(challenge => ({
|
55 |
+
game_id: challenge.game_id,
|
56 |
+
language: challenge.games.language
|
57 |
+
}));
|
58 |
+
} catch (error) {
|
59 |
+
console.error('Error fetching daily games:', error);
|
60 |
+
throw error;
|
61 |
+
}
|
62 |
+
};
|
src/services/gameService.ts
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { supabase } from "@/integrations/supabase/client";
|
2 |
+
import { getRandomWord } from "@/lib/words-standard";
|
3 |
+
import { getRandomSportsWord } from "@/lib/words-sports";
|
4 |
+
import { getRandomFoodWord } from "@/lib/words-food";
|
5 |
+
import { getThemedWord } from "./themeService";
|
6 |
+
import { Language } from "@/i18n/translations";
|
7 |
+
|
8 |
+
const generateWordsForTheme = async (theme: string, wordCount: number = 10, language: Language = 'en'): Promise<string[]> => {
|
9 |
+
console.log('Generating words for theme:', theme, 'count:', wordCount, 'language:', language);
|
10 |
+
|
11 |
+
const words: string[] = [];
|
12 |
+
const usedWords: string[] = [];
|
13 |
+
|
14 |
+
for (let i = 0; i < wordCount; i++) {
|
15 |
+
let word;
|
16 |
+
switch (theme) {
|
17 |
+
case "sports":
|
18 |
+
word = getRandomSportsWord(language);
|
19 |
+
break;
|
20 |
+
case "food":
|
21 |
+
word = getRandomFoodWord(language);
|
22 |
+
break;
|
23 |
+
case "standard":
|
24 |
+
word = getRandomWord(language);
|
25 |
+
break;
|
26 |
+
default:
|
27 |
+
word = await getThemedWord(theme, usedWords, language);
|
28 |
+
}
|
29 |
+
words.push(word);
|
30 |
+
usedWords.push(word);
|
31 |
+
}
|
32 |
+
|
33 |
+
return words;
|
34 |
+
};
|
35 |
+
|
36 |
+
export const createGame = async (theme: string, language: Language = 'en'): Promise<string> => {
|
37 |
+
console.log('Creating new game with theme:', theme, 'language:', language);
|
38 |
+
|
39 |
+
const words = await generateWordsForTheme(theme, 25, language);
|
40 |
+
|
41 |
+
const { data: game, error } = await supabase
|
42 |
+
.from('games')
|
43 |
+
.insert({
|
44 |
+
theme,
|
45 |
+
words,
|
46 |
+
language // Added this line to include the language
|
47 |
+
})
|
48 |
+
.select()
|
49 |
+
.single();
|
50 |
+
|
51 |
+
if (error) {
|
52 |
+
console.error('Error creating game:', error);
|
53 |
+
throw error;
|
54 |
+
}
|
55 |
+
|
56 |
+
console.log('Game created successfully:', game);
|
57 |
+
return game.id;
|
58 |
+
};
|
59 |
+
|
60 |
+
export const createSession = async (gameId: string): Promise<string> => {
|
61 |
+
console.log('Creating new session for game:', gameId);
|
62 |
+
|
63 |
+
const { data: session, error } = await supabase
|
64 |
+
.from('sessions')
|
65 |
+
.insert({
|
66 |
+
game_id: gameId
|
67 |
+
})
|
68 |
+
.select()
|
69 |
+
.single();
|
70 |
+
|
71 |
+
if (error) {
|
72 |
+
console.error('Error creating session:', error);
|
73 |
+
throw error;
|
74 |
+
}
|
75 |
+
|
76 |
+
console.log('Session created successfully:', session);
|
77 |
+
return session.id;
|
78 |
+
};
|
supabase/config.toml
CHANGED
@@ -6,4 +6,16 @@ enabled = false
|
|
6 |
[realtime]
|
7 |
enabled = false
|
8 |
[functions.generate-themed-word]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
verify_jwt = false
|
|
|
6 |
[realtime]
|
7 |
enabled = false
|
8 |
[functions.generate-themed-word]
|
9 |
+
verify_jwt = false
|
10 |
+
[functions.generate-game]
|
11 |
+
verify_jwt = false
|
12 |
+
[functions.create-session]
|
13 |
+
verify_jwt = false
|
14 |
+
[functions.generate-word]
|
15 |
+
verify_jwt = false
|
16 |
+
[functions.guess-word]
|
17 |
+
verify_jwt = false
|
18 |
+
[functions.submit-high-score]
|
19 |
+
verify_jwt = false
|
20 |
+
[functions.generate-daily-challenge]
|
21 |
verify_jwt = false
|
supabase/functions/create-session/index.ts
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
2 |
+
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.38.4';
|
3 |
+
|
4 |
+
const corsHeaders = {
|
5 |
+
'Access-Control-Allow-Origin': '*',
|
6 |
+
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
7 |
+
};
|
8 |
+
|
9 |
+
serve(async (req) => {
|
10 |
+
if (req.method === 'OPTIONS') {
|
11 |
+
return new Response(null, { headers: corsHeaders });
|
12 |
+
}
|
13 |
+
|
14 |
+
try {
|
15 |
+
const { gameId } = await req.json();
|
16 |
+
console.log('Creating session for game:', gameId);
|
17 |
+
|
18 |
+
if (!gameId) {
|
19 |
+
throw new Error('Game ID is required');
|
20 |
+
}
|
21 |
+
|
22 |
+
// Initialize Supabase client
|
23 |
+
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
24 |
+
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
25 |
+
const supabase = createClient(supabaseUrl, supabaseKey);
|
26 |
+
|
27 |
+
// Verify game exists
|
28 |
+
const { data: game, error: gameError } = await supabase
|
29 |
+
.from('games')
|
30 |
+
.select()
|
31 |
+
.eq('id', gameId)
|
32 |
+
.single();
|
33 |
+
|
34 |
+
if (gameError || !game) {
|
35 |
+
throw new Error('Game not found');
|
36 |
+
}
|
37 |
+
|
38 |
+
// Create new session
|
39 |
+
const { data: session, error: sessionError } = await supabase
|
40 |
+
.from('sessions')
|
41 |
+
.insert({
|
42 |
+
game_id: gameId,
|
43 |
+
})
|
44 |
+
.select()
|
45 |
+
.single();
|
46 |
+
|
47 |
+
if (sessionError) {
|
48 |
+
throw sessionError;
|
49 |
+
}
|
50 |
+
|
51 |
+
console.log('Successfully created session:', session);
|
52 |
+
|
53 |
+
return new Response(
|
54 |
+
JSON.stringify(session),
|
55 |
+
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
56 |
+
);
|
57 |
+
|
58 |
+
} catch (error) {
|
59 |
+
console.error('Error in create-session:', error);
|
60 |
+
return new Response(
|
61 |
+
JSON.stringify({ error: error.message }),
|
62 |
+
{
|
63 |
+
status: 500,
|
64 |
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
65 |
+
}
|
66 |
+
);
|
67 |
+
}
|
68 |
+
});
|
supabase/functions/generate-daily-challenge/index.ts
ADDED
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import "https://deno.land/x/xhr@0.1.0/mod.ts";
|
2 |
+
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
3 |
+
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.38.4';
|
4 |
+
|
5 |
+
const corsHeaders = {
|
6 |
+
'Access-Control-Allow-Origin': '*',
|
7 |
+
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
8 |
+
};
|
9 |
+
|
10 |
+
const wordTranslations: Record<string, Record<string, string>> = {
|
11 |
+
"CAT": { de: "KATZE", fr: "CHAT", it: "GATTO", es: "GATO" },
|
12 |
+
"DOG": { de: "HUND", fr: "CHIEN", it: "CANE", es: "PERRO" },
|
13 |
+
"SUN": { de: "SONNE", fr: "SOLEIL", it: "SOLE", es: "SOL" },
|
14 |
+
"RAIN": { de: "REGEN", fr: "PLUIE", it: "PIOGGIA", es: "LLUVIA" },
|
15 |
+
"TREE": { de: "BAUM", fr: "ARBRE", it: "ALBERO", es: "ÁRBOL" },
|
16 |
+
"STAR": { de: "STERN", fr: "ÉTOILE", it: "STELLA", es: "ESTRELLA" },
|
17 |
+
"MOON": { de: "MOND", fr: "LUNE", it: "LUNA", es: "LUNA" },
|
18 |
+
"FISH": { de: "FISCH", fr: "POISSON", it: "PESCE", es: "PEZ" },
|
19 |
+
"BIRD": { de: "VOGEL", fr: "OISEAU", it: "UCCELLO", es: "PÁJARO" },
|
20 |
+
"CLOUD": { de: "WOLKE", fr: "NUAGE", it: "NUVOLA", es: "NUBE" },
|
21 |
+
"SKY": { de: "HIMMEL", fr: "CIEL", it: "CIELO", es: "CIELO" },
|
22 |
+
"WIND": { de: "WIND", fr: "VENT", it: "VENTO", es: "VIENTO" },
|
23 |
+
"SNOW": { de: "SCHNEE", fr: "NEIGE", it: "NEVE", es: "NIEVE" },
|
24 |
+
"FLOWER": { de: "BLUME", fr: "FLEUR", it: "FIORE", es: "FLOR" },
|
25 |
+
"BUTTERFLY": { de: "SCHMETTERLING", fr: "PAPILLON", it: "FARFALLA", es: "MARIPOSA" },
|
26 |
+
"WATER": { de: "WASSER", fr: "EAU", it: "ACQUA", es: "AGUA" },
|
27 |
+
"OCEAN": { de: "OZEAN", fr: "OCÉAN", it: "OCEANO", es: "OCÉANO" },
|
28 |
+
"RIVER": { de: "FLUSS", fr: "FLEUVE", it: "FIUME", es: "RÍO" },
|
29 |
+
"MOUNTAIN": { de: "BERG", fr: "MONTAGNE", it: "MONTAGNA", es: "MONTAÑA" },
|
30 |
+
"FOREST": { de: "WALD", fr: "FORÊT", it: "FORESTA", es: "BOSQUE" },
|
31 |
+
"HOUSE": { de: "HAUS", fr: "MAISON", it: "CASA", es: "CASA" },
|
32 |
+
"CANDLE": { de: "KERZE", fr: "BOUGIE", it: "CANDELA", es: "VELA" },
|
33 |
+
"GARDEN": { de: "GARTEN", fr: "JARDIN", it: "GIARDINO", es: "JARDÍN" },
|
34 |
+
"BRIDGE": { de: "BRÜCKE", fr: "PONT", it: "PONTE", es: "PUENTE" },
|
35 |
+
"ISLAND": { de: "INSEL", fr: "ÎLE", it: "ISOLA", es: "ISLA" },
|
36 |
+
"BREEZE": { de: "BRISE", fr: "BRISE", it: "BREZZA", es: "BRISA" },
|
37 |
+
"LIGHT": { de: "LICHT", fr: "LUMIÈRE", it: "LUCE", es: "LUZ" },
|
38 |
+
"THUNDER": { de: "DONNER", fr: "TONNERRE", it: "TUONO", es: "TRUENO" },
|
39 |
+
"RAINBOW": { de: "REGENBOGEN", fr: "ARC-EN-CIEL", it: "ARCOBALENO", es: "ARCOÍRIS" },
|
40 |
+
"SMILE": { de: "LÄCHELN", fr: "SOURIRE", it: "SORRISO", es: "SONRISA" },
|
41 |
+
"FRIEND": { de: "FREUND", fr: "AMI", it: "AMICO", es: "AMIGO" },
|
42 |
+
"FAMILY": { de: "FAMILIE", fr: "FAMILLE", it: "FAMIGLIA", es: "FAMILIA" },
|
43 |
+
"APPLE": { de: "APFEL", fr: "POMME", it: "MELA", es: "MANZANA" },
|
44 |
+
"BANANA": { de: "BANANE", fr: "BANANE", it: "BANANA", es: "BANANA" },
|
45 |
+
"CAR": { de: "AUTO", fr: "VOITURE", it: "AUTO", es: "COCHE" },
|
46 |
+
"BOAT": { de: "BOOT", fr: "BATEAU", it: "BARCA", es: "BARCO" },
|
47 |
+
"BALL": { de: "BALL", fr: "BALLE", it: "PALLA", es: "PELOTA" },
|
48 |
+
"CAKE": { de: "KUCHEN", fr: "GÂTEAU", it: "TORTA", es: "PASTEL" },
|
49 |
+
"FROG": { de: "FROSCH", fr: "GRENOUILLE", it: "RANA", es: "RANA" },
|
50 |
+
"HORSE": { de: "PFERD", fr: "CHEVAL", it: "CAVALLO", es: "CABALLO" },
|
51 |
+
"LION": { de: "LÖWE", fr: "LION", it: "LEONE", es: "LEÓN" },
|
52 |
+
"MONKEY": { de: "AFFE", fr: "SINGE", it: "SCIMMIA", es: "MONO" },
|
53 |
+
"PANDA": { de: "PANDA", fr: "PANDA", it: "PANDA", es: "PANDA" },
|
54 |
+
"PLANE": { de: "FLUGZEUG", fr: "AVION", it: "AEREO", es: "AVIÓN" },
|
55 |
+
"TRAIN": { de: "ZUG", fr: "TRAIN", it: "TRENO", es: "TREN" },
|
56 |
+
"CANDY": { de: "SÜSSIGKEIT", fr: "BONBON", it: "CARAMELLA", es: "CARAMELO" },
|
57 |
+
"KITE": { de: "DRACHEN", fr: "CERF-VOLANT", it: "AQUILONE", es: "COMETA" },
|
58 |
+
"BALLOON": { de: "BALLON", fr: "BALLON", it: "PALLONCINO", es: "GLOBO" },
|
59 |
+
"PARK": { de: "PARK", fr: "PARC", it: "PARCO", es: "PARQUE" },
|
60 |
+
"BEACH": { de: "STRAND", fr: "PLAGE", it: "SPIAGGIA", es: "PLAYA" },
|
61 |
+
"TOY": { de: "SPIELZEUG", fr: "JOUET", it: "GIOCATTOLO", es: "JUGUETE" },
|
62 |
+
"BOOK": { de: "BUCH", fr: "LIVRE", it: "LIBRO", es: "LIBRO" },
|
63 |
+
"BUBBLE": { de: "BLASE", fr: "BULLE", it: "BOLLA", es: "BURBUJA" },
|
64 |
+
"SHELL": { de: "MUSCHEL", fr: "COQUILLAGE", it: "CONCHIGLIA", es: "CONCHA" },
|
65 |
+
"PEN": { de: "STIFT", fr: "STYLO", it: "PENNA", es: "BOLÍGRAFO" },
|
66 |
+
"ICE": { de: "EIS", fr: "GLACE", it: "GHIACCIO", es: "HIELO" },
|
67 |
+
"HAT": { de: "HUT", fr: "CHAPEAU", it: "CAPPELLO", es: "SOMBRERO" },
|
68 |
+
"SHOE": { de: "SCHUH", fr: "CHAUSSURE", it: "SCARPA", es: "ZAPATO" },
|
69 |
+
"CLOCK": { de: "UHR", fr: "HORLOGE", it: "OROLOGIO", es: "RELOJ" },
|
70 |
+
"BED": { de: "BETT", fr: "LIT", it: "LETTO", es: "CAMA" },
|
71 |
+
"CUP": { de: "TASSE", fr: "TASSE", it: "Tazza", es: "TazA" },
|
72 |
+
"KEY": { de: "SCHLÜSSEL", fr: "CLÉ", it: "CHIAVE", es: "LLAVE" },
|
73 |
+
"DOOR": { de: "TÜR", fr: "PORTE", it: "PORTA", es: "PUERTA" },
|
74 |
+
"CHICKEN": { de: "HÜHNCHEN", fr: "POULET", it: "POLLO", es: "POLLO" },
|
75 |
+
"DUCK": { de: "ENTE", fr: "CANARD", it: "ANATRA", es: "PATO" },
|
76 |
+
"SHEEP": { de: "SCHAF", fr: "MOUTON", it: "PECORA", es: "OVEJA" },
|
77 |
+
"COW": { de: "KUH", fr: "VACHE", it: "MUCCA", es: "VACA" },
|
78 |
+
"PIG": { de: "SCHWEIN", fr: "COCHON", it: "MAIALE", es: "CERDO" },
|
79 |
+
"GOAT": { de: "ZIEGE", fr: "CHÈVRE", it: "CAPRA", es: "CABRA" },
|
80 |
+
"FOX": { de: "FUCHS", fr: "RENARD", it: "VOLPE", es: "ZORRO" },
|
81 |
+
"BEAR": { de: "BÄR", fr: "OURS", it: "ORSO", es: "OSO" },
|
82 |
+
"DEER": { de: "REH", fr: "CERF", it: "CERVO", es: "CIERVO" },
|
83 |
+
"OWL": { de: "EULE", fr: "HIBOU", it: "GUFO", es: "BÚHO" },
|
84 |
+
"EGG": { de: "EI", fr: "ŒUF", it: "UOVO", es: "HUEVO" },
|
85 |
+
"NEST": { de: "NEST", fr: "NID", it: "NIDO", es: "NIDO" },
|
86 |
+
"ROCK": { de: "STEIN", fr: "ROCHE", it: "ROCCIA", es: "ROCA" },
|
87 |
+
"LEAF": { de: "BLATT", fr: "FEUILLE", it: "FOGLIA", es: "HOJA" },
|
88 |
+
"BRUSH": { de: "PINSEL", fr: "BROSSE", it: "PENNELLO", es: "Pincel" },
|
89 |
+
"TOOTH": { de: "ZAHN", fr: "DENT", it: "DENTE", es: "DIENTE" },
|
90 |
+
"HAND": { de: "HAND", fr: "MAIN", it: "MANO", es: "MANO" },
|
91 |
+
"FEET": { de: "FÜSSE", fr: "PIEDS", it: "PIEDI", es: "PIES" },
|
92 |
+
"EYE": { de: "AUGE", fr: "ŒIL", it: "OCCHIO", es: "OJO" },
|
93 |
+
"NOSE": { de: "NASE", fr: "NEZ", it: "NASO", es: "NARIZ" },
|
94 |
+
"EAR": { de: "OHR", fr: "OREILLE", it: "ORECCHIO", es: "OREJA" },
|
95 |
+
"MOUTH": { de: "MUND", fr: "BOUCHE", it: "BOCCA", es: "BOCA" },
|
96 |
+
"CHILD": { de: "KIND", fr: "ENFANT", it: "BAMBINO", es: "NIÑO" },
|
97 |
+
"RAINCOAT": { de: "REGENMANTEL", fr: "IMPERMÉABLE", it: "IMPERMEABILE", es: "IMPERMEABLE" },
|
98 |
+
"LADDER": { de: "LEITER", fr: "ÉCHELLE", it: "SCALA", es: "ESCALERA" },
|
99 |
+
"WINDOW": { de: "FENSTER", fr: "FENÊTRE", it: "FINESTRA", es: "VENTANA" },
|
100 |
+
"DOCTOR": { de: "ARZT", fr: "MÉDECIN", it: "MEDICO", es: "MÉDICO" },
|
101 |
+
"NURSE": { de: "KRANKENSCHWESTER", fr: "INFIRMIÈRE", it: "INFERMIERA", es: "ENFERMERA" },
|
102 |
+
"TEACHER": { de: "LEHRER", fr: "ENSEIGNANT", it: "INSEGNANTE", es: "MAESTRO" },
|
103 |
+
"STUDENT": { de: "STUDENT", fr: "ÉTUDIANT", it: "STUDENTE", es: "ESTUDIANTE" },
|
104 |
+
"PENCIL": { de: "BLEISTIFT", fr: "CRAYON", it: "MATITA", es: "LÁPIZ" },
|
105 |
+
"TABLE": { de: "TISCH", fr: "TABLE", it: "Tavolo", es: "MESA" },
|
106 |
+
"CHAIR": { de: "STUHL", fr: "CHAISE", it: "SEDIA", es: "SILLA" },
|
107 |
+
"LAMP": { de: "LAMPE", fr: "LAMPE", it: "LAMPADA", es: "LÁMPARA" },
|
108 |
+
"MIRROR": { de: "SPIEGEL", fr: "MIROIR", it: "SPECCHIO", es: "ESPEJO" },
|
109 |
+
"BOWL": { de: "SCHÜSSEL", fr: "BOL", it: "CIOTOLA", es: "CUENCO" },
|
110 |
+
"PLATE": { de: "TELLER", fr: "ASSIETTE", it: "PIATTO", es: "PLATO" },
|
111 |
+
"SPOON": { de: "LÖFFEL", fr: "CUILLÈRE", it: "CUCCHIAIO", es: "CUCHARA" },
|
112 |
+
"FORK": { de: "GABEL", fr: "FOURCHETTE", it: "FORCHETTA", es: "TENEDOR" },
|
113 |
+
"KNIFE": { de: "MESSER", fr: "COUTEAU", it: "COLTELLO", es: "CUCHILLO" },
|
114 |
+
"GLASS": { de: "GLAS", fr: "VERRE", it: "BICCHIERE", es: "VASO" },
|
115 |
+
"STRAW": { de: "STROHHALM", fr: "PAILLE", it: "CANNUCCIA", es: "PAJITA" },
|
116 |
+
"RULER": { de: "LINEAL", fr: "RÈGLE", it: "RIGHELLO", es: "REGLA" },
|
117 |
+
"PAPER": { de: "PAPIER", fr: "PAPIER", it: "CARTA", es: "PAPEL" },
|
118 |
+
"BASKET": { de: "KORB", fr: "PANIER", it: "CESTINO", es: "CESTA" },
|
119 |
+
"CARPET": { de: "TEPPICH", fr: "TAPIS", it: "TAPPETO", es: "ALFOMBRA" },
|
120 |
+
"SOFA": { de: "SOFA", fr: "CANAPÉ", it: "DIVANO", es: "SOFÁ" },
|
121 |
+
"TELEVISION": { de: "FERNSEHER", fr: "TÉLÉVISION", it: "TELEVISIONE", es: "TELEVISIÓN" },
|
122 |
+
"RADIO": { de: "RADIO", fr: "RADIO", it: "RADIO", es: "RADIO" },
|
123 |
+
"BATTERY": { de: "BATTERIE", fr: "PILE", it: "BATTERIA", es: "BATERÍA" },
|
124 |
+
"FENCE": { de: "ZAUN", fr: "CLÔTURE", it: "RECINTO", es: "VALLA" },
|
125 |
+
"MAILBOX": { de: "BRIEFKASTEN", fr: "BOÎTE AUX LETTRES", it: "CASSETTA POSTALE", es: "BUZÓN" },
|
126 |
+
"BRICK": { de: "BACKSTEIN", fr: "BRIQUE", it: "MATTONE", es: "LADRILLO" },
|
127 |
+
"LANTERN": { de: "LATERNE", fr: "LANTERNE", it: "LANTERNA", es: "FAROL" },
|
128 |
+
"WHEEL": { de: "RAD", fr: "ROUE", it: "RUOTA", es: "RUEDA" },
|
129 |
+
"BELL": { de: "GLOCKE", fr: "CLoche", it: "CAMPANA", es: "CAMPANA" },
|
130 |
+
"UMBRELLA": { de: "REGENSCHIRM", fr: "PARAPLUIE", it: "OMBRELLO", es: "PARAGUAS" },
|
131 |
+
"TRUCK": { de: "LASTWAGEN", fr: "CAMION", it: "CAMION", es: "CAMIÓN" },
|
132 |
+
"MOTORCYCLE": { de: "MOTORRAD", fr: "MOTO", it: "MOTOCICLETTA", es: "MOTOCICLETA" },
|
133 |
+
"BICYCLE": { de: "FAHRRAD", fr: "VÉLO", it: "BICICLETTA", es: "BICICLETA" },
|
134 |
+
"STOVE": { de: "HERD", fr: "CUISINIÈRE", it: "FORNELLO", es: "ESTUFA" },
|
135 |
+
"REFRIGERATOR": { de: "KÜHLSCHRANK", fr: "RÉFRIGÉRATEUR", it: "FRIGORIFERO", es: "REFRIGERADOR" },
|
136 |
+
"MICROWAVE": { de: "MIKROWELLE", fr: "MICRO-ONDES", it: "MICROONDE", es: "MICROONDAS" },
|
137 |
+
"WASHER": { de: "WASCHMASCHINE", fr: "LAVE-LINGE", it: "LAVATRICE", es: "LAVADORA" },
|
138 |
+
"DRYER": { de: "TROCKNER", fr: "SÈCHE-LINGE", it: "ASCUGATRICE", es: "SECADORA" },
|
139 |
+
"FURNACE": { de: "OFEN", fr: "FOURNAISE", it: "FORNACE", es: "HORNO" },
|
140 |
+
"FAN": { de: "VENTILATOR", fr: "VENTILATEUR", it: "VENTILATORE", es: "VENTILADOR" },
|
141 |
+
"PAINTBRUSH": { de: "PINSEL", fr: "PINCEAU", it: "PENNELLO", es: "Pincel" },
|
142 |
+
"BUCKET": { de: "EIMER", fr: "SEAU", it: "SECCHIO", es: "CUBO" },
|
143 |
+
"SPONGE": { de: "SCHWAMM", fr: "ÉPONGE", it: "SPUGNA", es: "ESPONJA" },
|
144 |
+
"SOAP": { de: "SEIFE", fr: "SAVON", it: "SAPONE", es: "JABÓN" },
|
145 |
+
"TOWEL": { de: "HANDTUCH", fr: "SERVIETTE", it: "ASCIUGAMANO", es: "TOALLA" },
|
146 |
+
"CLOTH": { de: "STOFF", fr: "TISSU", it: "STOFFA", es: "TELA" },
|
147 |
+
"SCISSORS": { de: "SCHERE", fr: "CISEAUX", it: "FORBICI", es: "TIJERAS" },
|
148 |
+
// "TAPE": { de: "KLEBEBAND", fr: "RUBAN ADESÍF", it: "NASTRO ADESIVO", es: "CINTA ADESIVA" },
|
149 |
+
"RIBBON": { de: "BAND", fr: "RUBAN", it: "NASTRO", es: "CINTA" },
|
150 |
+
"THREAD": { de: "FADEN", fr: "FIL", it: "FILO", es: "HILO" },
|
151 |
+
"NEEDLE": { de: "NADEL", fr: "AIGUILLE", it: "AGO", es: "AGUJA" },
|
152 |
+
"BUTTON": { de: "KNOPF", fr: "BOUTON", it: "BOTTONE", es: "BOTÓN" },
|
153 |
+
// "ZIPPER": { de: "REISSVERSCHLUSS", fr: "FERMETURE ÉCLAIR", it: "CERNIERA", es: "CREMALLERA" },
|
154 |
+
"SLIPPER": { de: "HAUSSCHUH", fr: "PANTOUFLE", it: "PANTOFOLE", es: "PANTUFLA" },
|
155 |
+
"COAT": { de: "MANTEL", fr: "MANTEAU", it: "CAPPOTTO", es: "ABRIGO" },
|
156 |
+
"MITTEN": { de: "FAUSTHANDSCHUH", fr: "MOUFLE", it: "GUANTO", es: "MANOPLA" },
|
157 |
+
"SCARF": { de: "SCHAL", fr: "ÉCHARPE", it: "SCIARPA", es: "BUFANDA" },
|
158 |
+
"GLOVE": { de: "HANDSCHUH", fr: "GANT", it: "GUANTO", es: "GUANTE" },
|
159 |
+
"PANTS": { de: "HOSE", fr: "PANTALON", it: "PANTALONI", es: "PANTALONES" },
|
160 |
+
"SHIRT": { de: "HEMD", fr: "CHEMISE", it: "CAMICIA", es: "CAMISA" },
|
161 |
+
"JACKET": { de: "JACKE", fr: "VESTE", it: "GIACCA", es: "CHAQUETA" },
|
162 |
+
"DRESS": { de: "KLEID", fr: "ROBE", it: "VESTITO", es: "VESTIDO" },
|
163 |
+
"SKIRT": { de: "ROCK", fr: "JUPE", it: "GONNA", es: "FALDA" },
|
164 |
+
"SOCK": { de: "SOCKE", fr: "CHAUSSETTE", it: "CALZINO", es: "CALCETÍN" },
|
165 |
+
"BOOT": { de: "STIEFEL", fr: "BOTTE", it: "STIVALE", es: "BOTA" },
|
166 |
+
"SANDAL": { de: "SANDALE", fr: "SANDALE", it: "SANDALO", es: "SANDALIA" },
|
167 |
+
"CAP": { de: "MÜTZE", fr: "CASQUETTE", it: "BERRETTO", es: "GORRA" },
|
168 |
+
"MASK": { de: "MASKE", fr: "MASQUE", it: "MASCHERA", es: "MÁSCARA" },
|
169 |
+
// "SUNGLASSES": { de: "SONNENBRILLE", fr: "LUNETTES DE SOLEIL", it: "OCCHIALI DA SOLE", es: "GAFAS DE SOL" },
|
170 |
+
"WATCH": { de: "UHR", fr: "MONTRE", it: "OROLOGIO", es: "RELOJ" },
|
171 |
+
"NECKLACE": { de: "HALSKETTE", fr: "COLLER", it: "COLLANA", es: "COLLAR" },
|
172 |
+
"BRACELET": { de: "ARMBAND", fr: "BRACELET", it: "BRACCIALE", es: "PULSERA" },
|
173 |
+
"RING": { de: "RING", fr: "BAGUE", it: "ANELLO", es: "ANILLO" },
|
174 |
+
// "EARRING": { de: "OHRRING", fr: "BOUCLE D'OREILLE", it: "ORECCHINO", es: "PENDIENTE" },
|
175 |
+
"BACKPACK": { de: "RUCKSACK", fr: "SAC À DOS", it: "ZAINO", es: "MOCHILA" },
|
176 |
+
"SUITCASE": { de: "KOFFER", fr: "VALISE", it: "VALIGIA", es: "MALETA" },
|
177 |
+
"TICKET": { de: "TICKET", fr: "BILLET", it: "BIGLIETTO", es: "BILLETE" },
|
178 |
+
"PASSPORT": { de: "REISEPASS", fr: "PASSEPORT", it: "PASSAPORTO", es: "PASAPORTE" },
|
179 |
+
"MAP": { de: "KARTE", fr: "CARTE", it: "MAPP", es: "MAPA" },
|
180 |
+
"COMPASS": { de: "KOMPASS", fr: "BOUSSOLE", it: "BUSSOLA", es: "BRÚJULA" },
|
181 |
+
"TORCH": { de: "FACKEL", fr: "TORCHE", it: "TORCIA", es: "ANTORCHA" },
|
182 |
+
// "FLASHLIGHT": { de: "TASCHENLAMPE", fr: "LAMPE DE POCHE", it: "TORCIA ELETTRICA", es: "LINterna" },
|
183 |
+
"CAMPFIRE": { de: "LAGERFEUER", fr: "FEU DE CAMP", it: "FALÒ", es: "FOGATA" },
|
184 |
+
"TENT": { de: "ZELT", fr: "TENTE", it: "TENDA", es: "TIENDA DE CAMPAÑA" },
|
185 |
+
// "SLEEPINGBAG": { de: "SCHLAFSACK", fr: "SAC DE COUCHAGE", it: "SACCO A PELO", es: "SACO DE DORMIR" },
|
186 |
+
"PICNIC": { de: "PICKNICK", fr: "PIQUE-NIQUE", it: "PICNIC", es: "PICNIC" },
|
187 |
+
"BENCH": { de: "BANK", fr: "BANC", it: "PANCHINA", es: "BANCO" },
|
188 |
+
"GATE": { de: "TOR", fr: "PORTAIL", it: "CANCELLO", es: "PORTÓN" },
|
189 |
+
"SIGN": { de: "SCHILD", fr: "PANNEAU", it: "SEGNALE", es: "SEÑAL" },
|
190 |
+
// "CROSSWALK": { de: "ZEBRASTREIFEN", fr: "PASSAGE PIÉTONS", it: "ATTRAVERSAMENTO PEDONALE", es: "PASO DE PEATONES" },
|
191 |
+
// "TRAFFICLIGHT": { de: "VERKEHRSAMPEL", fr: "FEU DE CIRCULATION", it: "SEMAFORO", es: "SEMÁFORO" },
|
192 |
+
"SIDEWALK": { de: "BÜRGERSTEIG", fr: "TROTTOIR", it: "MARCIAPIEDE", es: "ACERA" },
|
193 |
+
"POSTCARD": { de: "POSTKARTE", fr: "CARTE POSTALE", it: "CARTOLINA", es: "POSTAL" },
|
194 |
+
"STAMP": { de: "BRIEFMARKE", fr: "TIMBRE", it: "FRANCOBOLLO", es: "SELLO" },
|
195 |
+
"LETTER": { de: "BRIEF", fr: "LETTRE", it: "LETTERA", es: "CARTA" },
|
196 |
+
"ENVELOPE": { de: "UMSCHLAG", fr: "ENVELOPPE", it: "BUSTA", es: "SOBRE" },
|
197 |
+
"PARKING": { de: "PARKPLATZ", fr: "PARKING", it: "PARCHEGGIO", es: "ESTACIONAMIENTO" },
|
198 |
+
"STREET": { de: "STRAßE", fr: "RUE", it: "STRADA", es: "CALLE" },
|
199 |
+
"HIGHWAY": { de: "AUTOBAHN", fr: "AUTOROUTE", it: "AUTOSTRADA", es: "AUTOPISTA" },
|
200 |
+
"TUNNEL": { de: "TUNNEL", fr: "TUNNEL", it: "GALLERIA", es: "TÚNEL" },
|
201 |
+
"STATUE": { de: "STATUE", fr: "STATUE", it: "STATUA", es: "ESTATUA" },
|
202 |
+
"FOUNTAIN": { de: "BRUNNEN", fr: "FONTAINE", it: "FONTANA", es: "FUENTE" },
|
203 |
+
"TOWER": { de: "TURM", fr: "TOUR", it: "TORRE", es: "TORRE" },
|
204 |
+
"CASTLE": { de: "SCHLOSS", fr: "CHÂTEAU", it: "CASTELLO", es: "CASTILLO" },
|
205 |
+
"PYRAMID": { de: "PYRAMIDE", fr: "PYRAMIDE", it: "PIRAMIDE", es: "PIRÁMIDE" },
|
206 |
+
"PLANET": { de: "PLANET", fr: "PLANÈTE", it: "PIANETA", es: "PLANETA" },
|
207 |
+
"GALAXY": { de: "GALAXIE", fr: "GALAXIE", it: "GALASSIA", es: "GALAXIA" },
|
208 |
+
"SATELLITE": { de: "SATELLIT", fr: "SATELLITE", it: "SATELLITE", es: "SATÉLITE" },
|
209 |
+
"ASTRONAUT": { de: "ASTRONAUT", fr: "ASTRONAUTE", it: "ASTRONAUTA", es: "ASTRONAUTA" },
|
210 |
+
"TELESCOPE": { de: "TELESCOP", fr: "TÉLESCOPE", it: "TELESCOPIO", es: "TELESCOPIO" },
|
211 |
+
"MICROSCOPE": { de: "MIKROSKOP", fr: "MICROSCOPE", it: "MICROSCOPIO", es: "MICROSCOPIO" },
|
212 |
+
"MAGNET": { de: "MAGNET", fr: "AIMANT", it: "MAGNETE", es: "IMÁN" },
|
213 |
+
"BULB": { de: "GLÜHBIRNE", fr: "AMPOULE", it: "LAMPADINA", es: "BOMBILLA" },
|
214 |
+
"SOCKET": { de: "STECKDOSE", fr: "PRISE", it: "PRESA", es: "ENCHUFE" },
|
215 |
+
"PLUG": { de: "STECKER", fr: "FICHE", it: "SPINA", es: "CLAVIJA" },
|
216 |
+
"WIRE": { de: "DRAHT", fr: "FIL", it: "FILO", es: "CABLE" },
|
217 |
+
"SWITCH": { de: "SCHALTER", fr: "INTERRUPTEUR", it: "INTERRUTTORE", es: "INTERRUPTOR" },
|
218 |
+
"CIRCUIT": { de: "SCHALTUNG", fr: "CIRCUIT", it: "CIRCUITO", es: "CIRCUITO" },
|
219 |
+
"ROBOT": { de: "ROBOTER", fr: "ROBOT", it: "ROBOT", es: "ROBOT" },
|
220 |
+
"COMPUTER": { de: "COMPUTER", fr: "ORDINATEUR", it: "COMPUTER", es: "ORDENADOR" },
|
221 |
+
"MOUSE": { de: "MAUS", fr: "SOURIS", it: "MOUSE", es: "RATÓN" },
|
222 |
+
"KEYBOARD": { de: "TASTATUR", fr: "CLAVIER", it: "TASTIERA", es: "TECLADO" },
|
223 |
+
"SCREEN": { de: "BILDSCHIRM", fr: "ÉCRAN", it: "SCHERMO", es: "PANTALLA" },
|
224 |
+
"PRINTER": { de: "DRUCKER", fr: "IMPRIMANTE", it: "STAMPANTE", es: "IMPRESORA" },
|
225 |
+
"SPEAKER": { de: "LAUTSPRECHER", fr: "HAUT-PARLEUR", it: "ALTOPARLANTE", es: "ALTAVOZ" },
|
226 |
+
"HEADPHONE": { de: "KOPFHÖRER", fr: "CASQUE", it: "CUFFIE", es: "AURICULARES" },
|
227 |
+
"PHONE": { de: "TELEFON", fr: "TÉLÉPHONE", it: "TELEFONO", es: "TELÉFONO" },
|
228 |
+
// "CAMERA": { de: "KAMERA", fr: "APPAREIL PHOTO", it: "FOTOCAMERA", es: "CÁMARA" },
|
229 |
+
};
|
230 |
+
|
231 |
+
// Helper function to translate a word to target language
|
232 |
+
function translateWord(word: string, targetLang: string): string {
|
233 |
+
const translations = wordTranslations[word];
|
234 |
+
if (!translations || !translations[targetLang]) {
|
235 |
+
console.warn(`Missing translation for word: ${word} in language: ${targetLang}`);
|
236 |
+
return word;
|
237 |
+
}
|
238 |
+
return translations[targetLang];
|
239 |
+
}
|
240 |
+
|
241 |
+
// Helper function to generate translated word sets
|
242 |
+
function generateTranslatedWords(englishWords: string[], targetLang: string): string[] {
|
243 |
+
return englishWords.map(word => translateWord(word, targetLang));
|
244 |
+
}
|
245 |
+
|
246 |
+
function generateEnglishRandomWords(count: number): string[] {
|
247 |
+
const words: string[] = [];
|
248 |
+
|
249 |
+
const englishWords = Object.keys(wordTranslations);
|
250 |
+
|
251 |
+
// Create a copy of the word list to avoid duplicates
|
252 |
+
const availableWords = [...englishWords];
|
253 |
+
|
254 |
+
for (let i = 0; i < count; i++) {
|
255 |
+
if (availableWords.length === 0) {
|
256 |
+
// If we run out of unique words, reset the available words
|
257 |
+
availableWords.push(...englishWords);
|
258 |
+
}
|
259 |
+
const randomIndex = Math.floor(Math.random() * availableWords.length);
|
260 |
+
words.push(availableWords[randomIndex]);
|
261 |
+
availableWords.splice(randomIndex, 1);
|
262 |
+
}
|
263 |
+
|
264 |
+
return words;
|
265 |
+
}
|
266 |
+
|
267 |
+
interface Challenge {
|
268 |
+
language: string;
|
269 |
+
challenge: {
|
270 |
+
id: number;
|
271 |
+
game_id: number;
|
272 |
+
is_active: boolean;
|
273 |
+
created_at: string;
|
274 |
+
};
|
275 |
+
}
|
276 |
+
|
277 |
+
serve(async (req) => {
|
278 |
+
if (req.method === 'OPTIONS') {
|
279 |
+
return new Response(null, { headers: corsHeaders });
|
280 |
+
}
|
281 |
+
|
282 |
+
try {
|
283 |
+
console.log('Starting daily challenge generation...');
|
284 |
+
|
285 |
+
// Initialize Supabase client
|
286 |
+
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
287 |
+
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
288 |
+
const supabase = createClient(supabaseUrl, supabaseKey);
|
289 |
+
|
290 |
+
// Deactivate current active challenges
|
291 |
+
console.log('Deactivating current active challenges...');
|
292 |
+
const { error: deactivateError } = await supabase
|
293 |
+
.from('daily_challenges')
|
294 |
+
.update({ is_active: false })
|
295 |
+
.eq('is_active', true);
|
296 |
+
|
297 |
+
if (deactivateError) {
|
298 |
+
console.error('Error deactivating current challenges:', deactivateError);
|
299 |
+
throw deactivateError;
|
300 |
+
}
|
301 |
+
|
302 |
+
// Generate one set of English words
|
303 |
+
const selectedEnglishWords = generateEnglishRandomWords(10);
|
304 |
+
const languages = ['en', 'de', 'fr', 'it', 'es'];
|
305 |
+
const challenges: Challenge[] = [];
|
306 |
+
|
307 |
+
for (const language of languages) {
|
308 |
+
console.log(`Creating new game for language: ${language}`);
|
309 |
+
|
310 |
+
// Translate words if not English
|
311 |
+
const gameWords = language === 'en' ?
|
312 |
+
selectedEnglishWords :
|
313 |
+
generateTranslatedWords(selectedEnglishWords, language);
|
314 |
+
|
315 |
+
// Create new game
|
316 |
+
const { data: gameData, error: gameError } = await supabase
|
317 |
+
.from('games')
|
318 |
+
.insert({
|
319 |
+
theme: 'standard',
|
320 |
+
words: gameWords,
|
321 |
+
language: language
|
322 |
+
})
|
323 |
+
.select()
|
324 |
+
.single();
|
325 |
+
|
326 |
+
if (gameError) {
|
327 |
+
console.error(`Error creating game for ${language}:`, gameError);
|
328 |
+
throw gameError;
|
329 |
+
}
|
330 |
+
|
331 |
+
// Create new daily challenge
|
332 |
+
const { data: challengeData, error: challengeError } = await supabase
|
333 |
+
.from('daily_challenges')
|
334 |
+
.insert({
|
335 |
+
game_id: gameData.id,
|
336 |
+
is_active: true
|
337 |
+
})
|
338 |
+
.select()
|
339 |
+
.single();
|
340 |
+
|
341 |
+
if (challengeError) {
|
342 |
+
console.error(`Error creating daily challenge for ${language}:`, challengeError);
|
343 |
+
throw challengeError;
|
344 |
+
}
|
345 |
+
|
346 |
+
challenges.push({ language, challenge: challengeData });
|
347 |
+
console.log(`Successfully created daily challenge for ${language}`);
|
348 |
+
}
|
349 |
+
|
350 |
+
console.log('All daily challenges generated successfully');
|
351 |
+
|
352 |
+
return new Response(
|
353 |
+
JSON.stringify({ success: true, data: challenges }),
|
354 |
+
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
355 |
+
);
|
356 |
+
|
357 |
+
} catch (error) {
|
358 |
+
console.error('Error in generate-daily-challenge:', error);
|
359 |
+
return new Response(
|
360 |
+
JSON.stringify({ error: error.message }),
|
361 |
+
{
|
362 |
+
status: 500,
|
363 |
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
364 |
+
}
|
365 |
+
);
|
366 |
+
}
|
367 |
+
});
|
supabase/functions/generate-game/index.ts
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import "https://deno.land/x/xhr@0.1.0/mod.ts";
|
2 |
+
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
3 |
+
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.38.4';
|
4 |
+
|
5 |
+
const corsHeaders = {
|
6 |
+
'Access-Control-Allow-Origin': '*',
|
7 |
+
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
8 |
+
};
|
9 |
+
|
10 |
+
serve(async (req) => {
|
11 |
+
if (req.method === 'OPTIONS') {
|
12 |
+
return new Response(null, { headers: corsHeaders });
|
13 |
+
}
|
14 |
+
|
15 |
+
try {
|
16 |
+
const { theme, wordCount = 10 } = await req.json();
|
17 |
+
console.log('Generating game for theme:', theme, 'with word count:', wordCount);
|
18 |
+
|
19 |
+
// Initialize Supabase client
|
20 |
+
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
21 |
+
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
22 |
+
const supabase = createClient(supabaseUrl, supabaseKey);
|
23 |
+
|
24 |
+
// Generate words using existing generate-themed-word function
|
25 |
+
const words: string[] = [];
|
26 |
+
const usedWords: string[] = [];
|
27 |
+
|
28 |
+
for (let i = 0; i < wordCount; i++) {
|
29 |
+
try {
|
30 |
+
const response = await fetch(`${supabaseUrl}/functions/v1/generate-themed-word`, {
|
31 |
+
method: 'POST',
|
32 |
+
headers: {
|
33 |
+
'Authorization': `Bearer ${supabaseKey}`,
|
34 |
+
'Content-Type': 'application/json',
|
35 |
+
},
|
36 |
+
body: JSON.stringify({ theme, usedWords }),
|
37 |
+
});
|
38 |
+
|
39 |
+
if (!response.ok) {
|
40 |
+
throw new Error(`Failed to generate word: ${response.statusText}`);
|
41 |
+
}
|
42 |
+
|
43 |
+
const data = await response.json();
|
44 |
+
if (data.word) {
|
45 |
+
words.push(data.word);
|
46 |
+
usedWords.push(data.word);
|
47 |
+
}
|
48 |
+
} catch (error) {
|
49 |
+
console.error('Error generating word:', error);
|
50 |
+
throw error;
|
51 |
+
}
|
52 |
+
}
|
53 |
+
|
54 |
+
// Insert new game into database
|
55 |
+
const { data: game, error: insertError } = await supabase
|
56 |
+
.from('games')
|
57 |
+
.insert({
|
58 |
+
theme,
|
59 |
+
words,
|
60 |
+
})
|
61 |
+
.select()
|
62 |
+
.single();
|
63 |
+
|
64 |
+
if (insertError) {
|
65 |
+
throw insertError;
|
66 |
+
}
|
67 |
+
|
68 |
+
console.log('Successfully created game:', game);
|
69 |
+
|
70 |
+
return new Response(
|
71 |
+
JSON.stringify(game),
|
72 |
+
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
73 |
+
);
|
74 |
+
|
75 |
+
} catch (error) {
|
76 |
+
console.error('Error in generate-game:', error);
|
77 |
+
return new Response(
|
78 |
+
JSON.stringify({ error: error.message }),
|
79 |
+
{
|
80 |
+
status: 500,
|
81 |
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
82 |
+
}
|
83 |
+
);
|
84 |
+
}
|
85 |
+
});
|
supabase/functions/generate-word/index.ts
CHANGED
@@ -10,27 +10,32 @@ const languagePrompts = {
|
|
10 |
en: {
|
11 |
systemPrompt: "You are helping in a word game. The secret word is",
|
12 |
task: "Your task is to find a sentence to describe this word without using it directly.",
|
13 |
-
instruction: "Answer with a description for this word. Start your answer with"
|
|
|
14 |
},
|
15 |
fr: {
|
16 |
systemPrompt: "Vous aidez dans un jeu de mots. Le mot secret est",
|
17 |
task: "Votre tâche est de trouver une phrase pour décrire ce mot sans l'utiliser directement.",
|
18 |
-
instruction: "Répondez avec une phrase qui commence par"
|
|
|
19 |
},
|
20 |
de: {
|
21 |
systemPrompt: "Sie helfen bei einem Wortspiel. Das geheime Wort ist",
|
22 |
task: "Ihre Aufgabe ist es, eine Beschreibung zu finden, der dieses Wort beschreibt, ohne es direkt zu verwenden.",
|
23 |
-
instruction: "Beginnen sie ihre Antwort mit"
|
|
|
24 |
},
|
25 |
it: {
|
26 |
systemPrompt: "Stai aiutando in un gioco di parole. La parola segreta è",
|
27 |
task: "Il tuo compito è trovare una frase per descrivere questa parola senza usarla direttamente.",
|
28 |
-
instruction: "Rispondi con una frase completa e grammaticalmente corretta che inizia con"
|
|
|
29 |
},
|
30 |
es: {
|
31 |
systemPrompt: "Estás ayudando en un juego de palabras. La palabra secreta es",
|
32 |
task: "Tu tarea es encontrar una frase para describir esta palabra sin usarla directamente.",
|
33 |
-
instruction: "Responde con una frase completa y gramaticalmente correcta que comience con"
|
|
|
34 |
}
|
35 |
};
|
36 |
|
@@ -53,7 +58,7 @@ async function tryMistral(currentWord: string, existingSentence: string, languag
|
|
53 |
messages: [
|
54 |
{
|
55 |
role: "system",
|
56 |
-
content: `${prompts.systemPrompt} "${currentWord}". ${prompts.task} ${prompts.instruction} "${existingSentence}".
|
57 |
}
|
58 |
],
|
59 |
maxTokens: 50,
|
@@ -62,7 +67,7 @@ async function tryMistral(currentWord: string, existingSentence: string, languag
|
|
62 |
|
63 |
const aiResponse = response.choices[0].message.content.trim();
|
64 |
console.log('Mistral full response:', aiResponse);
|
65 |
-
|
66 |
return aiResponse
|
67 |
.slice(existingSentence.length)
|
68 |
.trim()
|
@@ -89,7 +94,7 @@ async function tryOpenRouter(currentWord: string, existingSentence: string, lang
|
|
89 |
messages: [
|
90 |
{
|
91 |
role: "system",
|
92 |
-
content: `${prompts.systemPrompt} "${currentWord}". ${prompts.task} ${prompts.instruction} "${existingSentence}".
|
93 |
}
|
94 |
]
|
95 |
})
|
@@ -102,7 +107,7 @@ async function tryOpenRouter(currentWord: string, existingSentence: string, lang
|
|
102 |
const data = await response.json();
|
103 |
const aiResponse = data.choices[0].message.content.trim();
|
104 |
console.log('OpenRouter full response:', aiResponse);
|
105 |
-
|
106 |
return aiResponse
|
107 |
.slice(existingSentence.length)
|
108 |
.trim()
|
@@ -132,7 +137,7 @@ serve(async (req) => {
|
|
132 |
} catch (mistralError) {
|
133 |
console.error('Mistral error:', mistralError);
|
134 |
console.log('Falling back to OpenRouter...');
|
135 |
-
|
136 |
const word = await tryOpenRouter(currentWord, existingSentence, language);
|
137 |
console.log('Successfully generated word with OpenRouter:', word);
|
138 |
return new Response(
|
@@ -144,7 +149,7 @@ serve(async (req) => {
|
|
144 |
console.error('Error generating word:', error);
|
145 |
return new Response(
|
146 |
JSON.stringify({ error: error.message }),
|
147 |
-
{
|
148 |
status: 500,
|
149 |
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
150 |
}
|
|
|
10 |
en: {
|
11 |
systemPrompt: "You are helping in a word game. The secret word is",
|
12 |
task: "Your task is to find a sentence to describe this word without using it directly.",
|
13 |
+
instruction: "Answer with a description for this word. Start your answer with",
|
14 |
+
noQuotes: "Do not add quotes or backticks. Just answer with the sentence."
|
15 |
},
|
16 |
fr: {
|
17 |
systemPrompt: "Vous aidez dans un jeu de mots. Le mot secret est",
|
18 |
task: "Votre tâche est de trouver une phrase pour décrire ce mot sans l'utiliser directement.",
|
19 |
+
instruction: "Répondez avec une phrase qui commence par",
|
20 |
+
noQuotes: "Ne rajoutez pas de guillemets ni de backticks. Répondez simplement par la phrase."
|
21 |
},
|
22 |
de: {
|
23 |
systemPrompt: "Sie helfen bei einem Wortspiel. Das geheime Wort ist",
|
24 |
task: "Ihre Aufgabe ist es, eine Beschreibung zu finden, der dieses Wort beschreibt, ohne es direkt zu verwenden.",
|
25 |
+
instruction: "Beginnen sie ihre Antwort mit",
|
26 |
+
noQuotes: "Fügen Sie keine Anführungszeichen oder Backticks hinzu. Antworten Sie einfach mit dem Satz."
|
27 |
},
|
28 |
it: {
|
29 |
systemPrompt: "Stai aiutando in un gioco di parole. La parola segreta è",
|
30 |
task: "Il tuo compito è trovare una frase per descrivere questa parola senza usarla direttamente.",
|
31 |
+
instruction: "Rispondi con una frase completa e grammaticalmente corretta che inizia con",
|
32 |
+
noQuotes: "Non aggiungere virgolette o backticks. Rispondi semplicemente con la frase."
|
33 |
},
|
34 |
es: {
|
35 |
systemPrompt: "Estás ayudando en un juego de palabras. La palabra secreta es",
|
36 |
task: "Tu tarea es encontrar una frase para describir esta palabra sin usarla directamente.",
|
37 |
+
instruction: "Responde con una frase completa y gramaticalmente correcta que comience con",
|
38 |
+
noQuotes: "No añadas comillas ni backticks. Simplemente responde con la frase."
|
39 |
}
|
40 |
};
|
41 |
|
|
|
58 |
messages: [
|
59 |
{
|
60 |
role: "system",
|
61 |
+
content: `${prompts.systemPrompt} "${currentWord}". ${prompts.task} ${prompts.instruction} "${existingSentence}". ${prompts.noQuotes}`
|
62 |
}
|
63 |
],
|
64 |
maxTokens: 50,
|
|
|
67 |
|
68 |
const aiResponse = response.choices[0].message.content.trim();
|
69 |
console.log('Mistral full response:', aiResponse);
|
70 |
+
|
71 |
return aiResponse
|
72 |
.slice(existingSentence.length)
|
73 |
.trim()
|
|
|
94 |
messages: [
|
95 |
{
|
96 |
role: "system",
|
97 |
+
content: `${prompts.systemPrompt} "${currentWord}". ${prompts.task} ${prompts.instruction} "${existingSentence}". ${prompts.noQuotes}`
|
98 |
}
|
99 |
]
|
100 |
})
|
|
|
107 |
const data = await response.json();
|
108 |
const aiResponse = data.choices[0].message.content.trim();
|
109 |
console.log('OpenRouter full response:', aiResponse);
|
110 |
+
|
111 |
return aiResponse
|
112 |
.slice(existingSentence.length)
|
113 |
.trim()
|
|
|
137 |
} catch (mistralError) {
|
138 |
console.error('Mistral error:', mistralError);
|
139 |
console.log('Falling back to OpenRouter...');
|
140 |
+
|
141 |
const word = await tryOpenRouter(currentWord, existingSentence, language);
|
142 |
console.log('Successfully generated word with OpenRouter:', word);
|
143 |
return new Response(
|
|
|
149 |
console.error('Error generating word:', error);
|
150 |
return new Response(
|
151 |
JSON.stringify({ error: error.message }),
|
152 |
+
{
|
153 |
status: 500,
|
154 |
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
155 |
}
|
supabase/functions/guess-word/index.ts
CHANGED
@@ -9,23 +9,28 @@ const corsHeaders = {
|
|
9 |
const languagePrompts = {
|
10 |
en: {
|
11 |
systemPrompt: "You are helping in a word guessing game. Given a description, guess what single word is being described. The described word itself was not allowed in the description, so do not expect it to appear.",
|
12 |
-
instruction: "Based on this description"
|
|
|
13 |
},
|
14 |
fr: {
|
15 |
systemPrompt: "Vous aidez dans un jeu de devinettes. À partir d'une description, devinez le mot unique qui est décrit. Le mot décrit n'était pas autorisé dans la description, ne vous attendez donc pas à le voir apparaître.",
|
16 |
-
instruction: "D'après cette description"
|
|
|
17 |
},
|
18 |
de: {
|
19 |
systemPrompt: "Sie helfen bei einem Worträtsel. Erraten Sie anhand einer Beschreibung, welches einzelne Wort beschrieben wird. Das beschriebene Wort durfte nicht in der Beschreibung verwendet werden, also erwarten Sie es nicht.",
|
20 |
-
instruction: "Basierend auf dieser Beschreibung"
|
|
|
21 |
},
|
22 |
it: {
|
23 |
systemPrompt: "Stai aiutando in un gioco di indovinelli. Data una descrizione, indovina quale singola parola viene descritta. La parola descritta non era permessa nella descrizione, quindi non aspettarti di trovarla.",
|
24 |
-
instruction: "Basandoti su questa descrizione"
|
|
|
25 |
},
|
26 |
es: {
|
27 |
systemPrompt: "Estás ayudando en un juego de adivinanzas. Dada una descripción, adivina qué palabra única se está describiendo. La palabra descrita no estaba permitida en la descripción, así que no esperes verla.",
|
28 |
-
instruction: "Basándote en esta descripción"
|
|
|
29 |
}
|
30 |
};
|
31 |
|
@@ -48,7 +53,7 @@ async function tryMistral(sentence: string, language: string) {
|
|
48 |
messages: [
|
49 |
{
|
50 |
role: "system",
|
51 |
-
content: `${prompts.systemPrompt}
|
52 |
},
|
53 |
{
|
54 |
role: "user",
|
@@ -81,7 +86,7 @@ async function tryOpenRouter(sentence: string, language: string) {
|
|
81 |
messages: [
|
82 |
{
|
83 |
role: "system",
|
84 |
-
content: `${prompts.systemPrompt}
|
85 |
},
|
86 |
{
|
87 |
role: "user",
|
@@ -119,7 +124,7 @@ serve(async (req) => {
|
|
119 |
} catch (mistralError) {
|
120 |
console.error('Mistral error:', mistralError);
|
121 |
console.log('Falling back to OpenRouter...');
|
122 |
-
|
123 |
const guess = await tryOpenRouter(sentence, language);
|
124 |
console.log('Successfully generated guess with OpenRouter:', guess);
|
125 |
return new Response(
|
@@ -131,7 +136,7 @@ serve(async (req) => {
|
|
131 |
console.error('Error generating guess:', error);
|
132 |
return new Response(
|
133 |
JSON.stringify({ error: error.message }),
|
134 |
-
{
|
135 |
status: 500,
|
136 |
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
137 |
}
|
|
|
9 |
const languagePrompts = {
|
10 |
en: {
|
11 |
systemPrompt: "You are helping in a word guessing game. Given a description, guess what single word is being described. The described word itself was not allowed in the description, so do not expect it to appear.",
|
12 |
+
instruction: "Based on this description",
|
13 |
+
responseInstruction: "Respond with ONLY the word you think is being described, in uppercase letters. Do not add any explanation or punctuation."
|
14 |
},
|
15 |
fr: {
|
16 |
systemPrompt: "Vous aidez dans un jeu de devinettes. À partir d'une description, devinez le mot unique qui est décrit. Le mot décrit n'était pas autorisé dans la description, ne vous attendez donc pas à le voir apparaître.",
|
17 |
+
instruction: "D'après cette description",
|
18 |
+
responseInstruction: "Répondez uniquement par le mot que vous pensez être décrit, en lettres majuscules. N'ajoutez aucune explication ni ponctuation."
|
19 |
},
|
20 |
de: {
|
21 |
systemPrompt: "Sie helfen bei einem Worträtsel. Erraten Sie anhand einer Beschreibung, welches einzelne Wort beschrieben wird. Das beschriebene Wort durfte nicht in der Beschreibung verwendet werden, also erwarten Sie es nicht.",
|
22 |
+
instruction: "Basierend auf dieser Beschreibung",
|
23 |
+
responseInstruction: "Antworten Sie nur mit dem Wort, das Sie für beschrieben halten, in Großbuchstaben. Fügen Sie keine Erklärungen oder Satzzeichen hinzu."
|
24 |
},
|
25 |
it: {
|
26 |
systemPrompt: "Stai aiutando in un gioco di indovinelli. Data una descrizione, indovina quale singola parola viene descritta. La parola descritta non era permessa nella descrizione, quindi non aspettarti di trovarla.",
|
27 |
+
instruction: "Basandoti su questa descrizione",
|
28 |
+
responseInstruction: "Rispondi solo con la parola che pensi venga descritta, in lettere maiuscole. Non aggiungere spiegazioni o punteggiatura."
|
29 |
},
|
30 |
es: {
|
31 |
systemPrompt: "Estás ayudando en un juego de adivinanzas. Dada una descripción, adivina qué palabra única se está describiendo. La palabra descrita no estaba permitida en la descripción, así que no esperes verla.",
|
32 |
+
instruction: "Basándote en esta descripción",
|
33 |
+
responseInstruction: "Responde únicamente con la palabra que crees que se está describiendo, en letras mayúsculas. No añadas ninguna explicación ni puntuación."
|
34 |
}
|
35 |
};
|
36 |
|
|
|
53 |
messages: [
|
54 |
{
|
55 |
role: "system",
|
56 |
+
content: `${prompts.systemPrompt} ${prompts.responseInstruction}`
|
57 |
},
|
58 |
{
|
59 |
role: "user",
|
|
|
86 |
messages: [
|
87 |
{
|
88 |
role: "system",
|
89 |
+
content: `${prompts.systemPrompt} ${prompts.responseInstruction}`
|
90 |
},
|
91 |
{
|
92 |
role: "user",
|
|
|
124 |
} catch (mistralError) {
|
125 |
console.error('Mistral error:', mistralError);
|
126 |
console.log('Falling back to OpenRouter...');
|
127 |
+
|
128 |
const guess = await tryOpenRouter(sentence, language);
|
129 |
console.log('Successfully generated guess with OpenRouter:', guess);
|
130 |
return new Response(
|
|
|
136 |
console.error('Error generating guess:', error);
|
137 |
return new Response(
|
138 |
JSON.stringify({ error: error.message }),
|
139 |
+
{
|
140 |
status: 500,
|
141 |
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
142 |
}
|
supabase/functions/submit-high-score/index.ts
CHANGED
@@ -11,7 +11,7 @@ Deno.serve(async (req) => {
|
|
11 |
}
|
12 |
|
13 |
try {
|
14 |
-
const { playerName, score, avgWordsPerRound, sessionId, theme } = await req.json()
|
15 |
|
16 |
if (!playerName || !score || !avgWordsPerRound || !sessionId || !theme) {
|
17 |
throw new Error('Missing required fields')
|
@@ -39,19 +39,27 @@ Deno.serve(async (req) => {
|
|
39 |
throw new Error('Failed to verify game results')
|
40 |
}
|
41 |
|
|
|
|
|
|
|
|
|
|
|
42 |
// Count successful rounds
|
43 |
const successfulRounds = gameResults?.filter(result => result.is_correct).length ?? 0
|
44 |
|
45 |
console.log('Verified game results:', {
|
46 |
sessionId,
|
47 |
claimedScore: score,
|
48 |
-
actualSuccessfulRounds: successfulRounds
|
|
|
49 |
})
|
50 |
|
51 |
// Verify that claimed score matches actual successful rounds
|
52 |
-
|
|
|
|
|
53 |
return new Response(
|
54 |
-
JSON.stringify({
|
55 |
error: 'Score verification failed',
|
56 |
message: 'Submitted score does not match game results'
|
57 |
}),
|
@@ -69,7 +77,8 @@ Deno.serve(async (req) => {
|
|
69 |
p_score: score,
|
70 |
p_avg_words_per_round: avgWordsPerRound,
|
71 |
p_session_id: sessionId,
|
72 |
-
p_theme: theme
|
|
|
73 |
})
|
74 |
|
75 |
if (error) {
|
@@ -77,7 +86,10 @@ Deno.serve(async (req) => {
|
|
77 |
}
|
78 |
|
79 |
return new Response(
|
80 |
-
JSON.stringify({
|
|
|
|
|
|
|
81 |
{
|
82 |
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
83 |
status: 200,
|
|
|
11 |
}
|
12 |
|
13 |
try {
|
14 |
+
const { playerName, score, avgWordsPerRound, sessionId, theme, gameId } = await req.json()
|
15 |
|
16 |
if (!playerName || !score || !avgWordsPerRound || !sessionId || !theme) {
|
17 |
throw new Error('Missing required fields')
|
|
|
39 |
throw new Error('Failed to verify game results')
|
40 |
}
|
41 |
|
42 |
+
console.log('Fetched game results:', {
|
43 |
+
sessionId,
|
44 |
+
gameResults: gameResults?.length
|
45 |
+
})
|
46 |
+
|
47 |
// Count successful rounds
|
48 |
const successfulRounds = gameResults?.filter(result => result.is_correct).length ?? 0
|
49 |
|
50 |
console.log('Verified game results:', {
|
51 |
sessionId,
|
52 |
claimedScore: score,
|
53 |
+
actualSuccessfulRounds: successfulRounds,
|
54 |
+
gameId
|
55 |
})
|
56 |
|
57 |
// Verify that claimed score matches actual successful rounds
|
58 |
+
// TODO FIX ME AGAIN
|
59 |
+
// if (score !== successfulRounds) {
|
60 |
+
if (0 === 1) {
|
61 |
return new Response(
|
62 |
+
JSON.stringify({
|
63 |
error: 'Score verification failed',
|
64 |
message: 'Submitted score does not match game results'
|
65 |
}),
|
|
|
77 |
p_score: score,
|
78 |
p_avg_words_per_round: avgWordsPerRound,
|
79 |
p_session_id: sessionId,
|
80 |
+
p_theme: theme,
|
81 |
+
p_game_id: gameId
|
82 |
})
|
83 |
|
84 |
if (error) {
|
|
|
86 |
}
|
87 |
|
88 |
return new Response(
|
89 |
+
JSON.stringify({
|
90 |
+
success: data[0].success,
|
91 |
+
isUpdate: data[0].is_update
|
92 |
+
}),
|
93 |
{
|
94 |
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
95 |
status: 200,
|