Felix Zieger commited on
Commit
018dc4e
·
1 Parent(s): 01ff877
Dockerfile CHANGED
@@ -1,29 +1,31 @@
1
  # Use the official Node.js image as the base image
2
- FROM node:18-alpine
3
 
4
  # Set the working directory
5
  WORKDIR /app
6
 
7
- # Copy package.json and package-lock.json
8
- COPY package*.json ./
9
 
10
  # Install dependencies
11
- RUN npm install
 
 
12
 
13
  # Copy the rest of the application code
14
  COPY . .
15
 
 
 
 
16
  # Ensure the correct ownership and permissions
17
  RUN chown -R node:node /app
18
 
19
  # Switch to the non-root 'node' user
20
  USER node
21
 
22
- # Build the Vite application
23
- RUN npm run build
24
-
25
  # Expose the port the app runs on
26
  EXPOSE 8080
27
 
28
- # Start the Vite development server
29
- CMD ["npm", "run", "dev"]
 
1
  # Use the official Node.js image as the base image
2
+ FROM node:22-alpine
3
 
4
  # Set the working directory
5
  WORKDIR /app
6
 
7
+ # Install pnpm and serve
8
+ RUN npm install -g pnpm serve
9
 
10
  # Install dependencies
11
+ COPY package.json ./
12
+ COPY pnpm-lock.yaml ./
13
+ RUN pnpm install
14
 
15
  # Copy the rest of the application code
16
  COPY . .
17
 
18
+ # Build the Vite application
19
+ RUN pnpm run build
20
+
21
  # Ensure the correct ownership and permissions
22
  RUN chown -R node:node /app
23
 
24
  # Switch to the non-root 'node' user
25
  USER node
26
 
 
 
 
27
  # Expose the port the app runs on
28
  EXPOSE 8080
29
 
30
+ # Start the production server
31
+ CMD ["serve", "-s", "dist", "-l", "8080"]
README.md CHANGED
@@ -13,13 +13,15 @@ pinned: false
13
  You will be given a secret word. You aim to describe this secret word so an AI can guess it.
14
  However, you can only say one word at a time, taking turns with another AI.
15
 
 
 
16
  ## Develop locally
17
 
18
  You need Node.js and npm installed on your system.
19
 
20
  ```
21
  npm i
22
- npm run dev
23
  ```
24
 
25
  ## What technologies are used for this project?
 
13
  You will be given a secret word. You aim to describe this secret word so an AI can guess it.
14
  However, you can only say one word at a time, taking turns with another AI.
15
 
16
+ Play this game under [think-in-sync.com](https://www.think-in-sync.com/)
17
+
18
  ## Develop locally
19
 
20
  You need Node.js and npm installed on your system.
21
 
22
  ```
23
  npm i
24
+ pnpm run dev
25
  ```
26
 
27
  ## What technologies are used for this project?
bun.lockb DELETED
Binary file (187 kB)
 
package-lock.json DELETED
The diff for this file is too large to render. See raw diff
 
pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
src/App.tsx CHANGED
@@ -1,27 +1,40 @@
 
1
  import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
2
  import Index from "@/pages/Index";
3
  import { AdminIndex } from "@/pages/admin/Index";
4
  import { AdminLogin } from "@/pages/admin/Login";
 
 
5
  import { Toaster } from "@/components/ui/toaster";
6
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 
 
7
 
8
  const queryClient = new QueryClient();
9
 
10
  function App() {
11
  return (
12
  <QueryClientProvider client={queryClient}>
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>
21
- <Toaster />
22
- </Router>
 
 
 
 
 
 
 
 
23
  </QueryClientProvider>
24
  );
25
  }
26
 
27
- export default App;
 
1
+
2
  import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
3
  import Index from "@/pages/Index";
4
  import { AdminIndex } from "@/pages/admin/Index";
5
  import { AdminLogin } from "@/pages/admin/Login";
6
+ import Login from "@/pages/auth/Login";
7
+ import Register from "@/pages/auth/Register";
8
  import { Toaster } from "@/components/ui/toaster";
9
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
10
+ import { AuthProvider } from "@/contexts/AuthContext";
11
+ import { ProtectedRoute } from "@/components/auth/ProtectedRoute";
12
 
13
  const queryClient = new QueryClient();
14
 
15
  function App() {
16
  return (
17
  <QueryClientProvider client={queryClient}>
18
+ <AuthProvider>
19
+ <Router>
20
+ <Routes>
21
+ <Route path="/" element={<Index />} />
22
+ <Route path="/game" element={<Index />} />
23
+ <Route path="/game/:gameId" element={<Index />} />
24
+ <Route path="/auth/login" element={<Login />} />
25
+ <Route path="/auth/register" element={<Register />} />
26
+ <Route path="/admin" element={
27
+ <ProtectedRoute>
28
+ <AdminIndex />
29
+ </ProtectedRoute>
30
+ } />
31
+ <Route path="/admin/login" element={<AdminLogin />} />
32
+ </Routes>
33
+ <Toaster />
34
+ </Router>
35
+ </AuthProvider>
36
  </QueryClientProvider>
37
  );
38
  }
39
 
40
+ export default App;
src/components/GameContainer.tsx CHANGED
@@ -17,6 +17,7 @@ import { LanguageContext } from "@/contexts/LanguageContext";
17
  import { supabase } from "@/integrations/supabase/client";
18
  import { Language } from "@/i18n/translations";
19
  import { normalizeWord } from "@/lib/wordProcessing";
 
20
 
21
  type GameState = "welcome" | "theme-selection" | "model-selection" | "building-sentence" | "showing-guess" | "game-review" | "invitation";
22
 
@@ -44,6 +45,8 @@ export const GameContainer = () => {
44
  const [aiGuess, setAiGuess] = useState<string>("");
45
  const [aiModel, setAiModel] = useState<string>("");
46
  const [successfulRounds, setSuccessfulRounds] = useState<number>(0);
 
 
47
  const [totalWordsInSuccessfulRounds, setTotalWordsInSuccessfulRounds] = useState<number>(0);
48
  const { toast } = useToast();
49
  const t = useTranslation();
@@ -141,6 +144,8 @@ export const GameContainer = () => {
141
  setAiGuess("");
142
  setCurrentTheme("standard");
143
  setSuccessfulRounds(0);
 
 
144
  setTotalWordsInSuccessfulRounds(0);
145
  setWords([]);
146
  setCurrentWordIndex(0);
@@ -209,6 +214,8 @@ export const GameContainer = () => {
209
  setCurrentWordIndex(0);
210
  setGameState("building-sentence");
211
  setSuccessfulRounds(0);
 
 
212
  setTotalWordsInSuccessfulRounds(0);
213
  console.log("Game started with theme:", currentTheme, "language:", language, "model:", model);
214
  } catch (error) {
@@ -308,19 +315,21 @@ export const GameContainer = () => {
308
  };
309
 
310
  const handleNextRound = () => {
311
- if (isGuessCorrect()) {
 
312
  setSuccessfulRounds(prev => prev + 1);
313
- if (currentWordIndex < words.length - 1) {
314
- setCurrentWordIndex(prev => prev + 1);
315
- setGameState("building-sentence");
316
- setSentence([]);
317
- setAiGuess("");
318
- console.log("Next round started with word:", words[currentWordIndex + 1]);
319
- } else {
320
- handleGameReview();
321
- }
322
  } else {
323
- setGameState("game-review");
 
 
 
 
 
 
 
 
 
 
324
  }
325
  };
326
 
@@ -328,6 +337,8 @@ export const GameContainer = () => {
328
  setSentence([]);
329
  setAiGuess("");
330
  setSuccessfulRounds(0);
 
 
331
  setTotalWordsInSuccessfulRounds(0);
332
  setWords([]);
333
  setCurrentWordIndex(0);
@@ -364,6 +375,11 @@ export const GameContainer = () => {
364
 
365
  return (
366
  <div className="flex min-h-screen items-center justify-center p-1 md:p-4">
 
 
 
 
 
367
  <motion.div
368
  initial={{ opacity: 0, y: 20 }}
369
  animate={{ opacity: 1, y: 0 }}
@@ -381,6 +397,9 @@ export const GameContainer = () => {
381
  <SentenceBuilder
382
  currentWord={currentWord}
383
  successfulRounds={successfulRounds}
 
 
 
384
  sentence={sentence}
385
  playerInput={playerInput}
386
  isAiThinking={isAiThinking}
@@ -389,7 +408,7 @@ export const GameContainer = () => {
389
  onMakeGuess={handleMakeGuess}
390
  normalizeWord={(word: string) => normalizeWord(word, language)}
391
  onBack={handleBack}
392
- onClose={handleBack}
393
  />
394
  ) : gameState === "showing-guess" ? (
395
  <GuessDisplay
@@ -400,6 +419,9 @@ export const GameContainer = () => {
400
  onGameReview={handleGameReview}
401
  onBack={handleBack}
402
  currentScore={successfulRounds}
 
 
 
403
  avgWordsPerRound={getAverageWordsPerSuccessfulRound()}
404
  sessionId={sessionId}
405
  currentTheme={currentTheme}
@@ -409,6 +431,8 @@ export const GameContainer = () => {
409
  ) : (
410
  <GameReview
411
  currentScore={successfulRounds}
 
 
412
  avgWordsPerRound={getAverageWordsPerSuccessfulRound()}
413
  onPlayAgain={handlePlayAgain}
414
  onBack={handleBack}
@@ -416,6 +440,8 @@ export const GameContainer = () => {
416
  sessionId={sessionId}
417
  currentTheme={currentTheme}
418
  fromSession={fromSession}
 
 
419
  />
420
  )}
421
  </motion.div>
 
17
  import { supabase } from "@/integrations/supabase/client";
18
  import { Language } from "@/i18n/translations";
19
  import { normalizeWord } from "@/lib/wordProcessing";
20
+ import { UserMenu } from "./auth/UserMenu";
21
 
22
  type GameState = "welcome" | "theme-selection" | "model-selection" | "building-sentence" | "showing-guess" | "game-review" | "invitation";
23
 
 
45
  const [aiGuess, setAiGuess] = useState<string>("");
46
  const [aiModel, setAiModel] = useState<string>("");
47
  const [successfulRounds, setSuccessfulRounds] = useState<number>(0);
48
+ const [wrongGuesses, setWrongGuesses] = useState<number>(0);
49
+ const [guessSequence, setGuessSequence] = useState<Array<'success' | 'wrong'>>([]);
50
  const [totalWordsInSuccessfulRounds, setTotalWordsInSuccessfulRounds] = useState<number>(0);
51
  const { toast } = useToast();
52
  const t = useTranslation();
 
144
  setAiGuess("");
145
  setCurrentTheme("standard");
146
  setSuccessfulRounds(0);
147
+ setWrongGuesses(0);
148
+ setGuessSequence([]);
149
  setTotalWordsInSuccessfulRounds(0);
150
  setWords([]);
151
  setCurrentWordIndex(0);
 
214
  setCurrentWordIndex(0);
215
  setGameState("building-sentence");
216
  setSuccessfulRounds(0);
217
+ setWrongGuesses(0);
218
+ setGuessSequence([]);
219
  setTotalWordsInSuccessfulRounds(0);
220
  console.log("Game started with theme:", currentTheme, "language:", language, "model:", model);
221
  } catch (error) {
 
315
  };
316
 
317
  const handleNextRound = () => {
318
+ const wasCorrect = isGuessCorrect();
319
+ if (wasCorrect) {
320
  setSuccessfulRounds(prev => prev + 1);
 
 
 
 
 
 
 
 
 
321
  } else {
322
+ setWrongGuesses(prev => prev + 1);
323
+ }
324
+ setGuessSequence(prev => [...prev, wasCorrect ? 'success' : 'wrong']);
325
+
326
+ if (currentWordIndex < words.length - 1) {
327
+ setCurrentWordIndex(prev => prev + 1);
328
+ setGameState("building-sentence");
329
+ setSentence([]);
330
+ setAiGuess("");
331
+ } else {
332
+ handleGameReview();
333
  }
334
  };
335
 
 
337
  setSentence([]);
338
  setAiGuess("");
339
  setSuccessfulRounds(0);
340
+ setWrongGuesses(0);
341
+ setGuessSequence([]);
342
  setTotalWordsInSuccessfulRounds(0);
343
  setWords([]);
344
  setCurrentWordIndex(0);
 
375
 
376
  return (
377
  <div className="flex min-h-screen items-center justify-center p-1 md:p-4">
378
+ {gameState === "welcome" && (
379
+ <div className="absolute top-2 right-2 flex items-center gap-4">
380
+ <UserMenu />
381
+ </div>
382
+ )}
383
  <motion.div
384
  initial={{ opacity: 0, y: 20 }}
385
  animate={{ opacity: 1, y: 0 }}
 
397
  <SentenceBuilder
398
  currentWord={currentWord}
399
  successfulRounds={successfulRounds}
400
+ totalRounds={words.length}
401
+ wrongGuesses={wrongGuesses}
402
+ guessSequence={guessSequence}
403
  sentence={sentence}
404
  playerInput={playerInput}
405
  isAiThinking={isAiThinking}
 
408
  onMakeGuess={handleMakeGuess}
409
  normalizeWord={(word: string) => normalizeWord(word, language)}
410
  onBack={handleBack}
411
+ onClose={handleGameReview}
412
  />
413
  ) : gameState === "showing-guess" ? (
414
  <GuessDisplay
 
419
  onGameReview={handleGameReview}
420
  onBack={handleBack}
421
  currentScore={successfulRounds}
422
+ totalRounds={words.length}
423
+ wrongGuesses={wrongGuesses}
424
+ guessSequence={guessSequence}
425
  avgWordsPerRound={getAverageWordsPerSuccessfulRound()}
426
  sessionId={sessionId}
427
  currentTheme={currentTheme}
 
431
  ) : (
432
  <GameReview
433
  currentScore={successfulRounds}
434
+ wrongGuesses={wrongGuesses}
435
+ totalRounds={words.length}
436
  avgWordsPerRound={getAverageWordsPerSuccessfulRound()}
437
  onPlayAgain={handlePlayAgain}
438
  onBack={handleBack}
 
440
  sessionId={sessionId}
441
  currentTheme={currentTheme}
442
  fromSession={fromSession}
443
+ words={words}
444
+ guessSequence={guessSequence}
445
  />
446
  )}
447
  </motion.div>
src/components/auth/ProtectedRoute.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { ReactNode } from "react";
3
+ import { Navigate } from "react-router-dom";
4
+ import { useAuth } from "@/contexts/AuthContext";
5
+
6
+ interface ProtectedRouteProps {
7
+ children: ReactNode;
8
+ }
9
+
10
+ export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
11
+ const { user, loading } = useAuth();
12
+
13
+ if (loading) {
14
+ return <div className="flex h-screen items-center justify-center">Loading...</div>;
15
+ }
16
+
17
+ if (!user) {
18
+ return <Navigate to="/auth/login" />;
19
+ }
20
+
21
+ return <>{children}</>;
22
+ };
src/components/auth/UserMenu.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { useEffect, useState } from "react";
3
+ import { Link, useNavigate } from "react-router-dom";
4
+ import { useAuth } from "@/contexts/AuthContext";
5
+ import { Button } from "@/components/ui/button";
6
+ import { useToast } from "@/hooks/use-toast";
7
+ import { LogIn, LogOut, User } from "lucide-react";
8
+ import { useTranslation } from "@/hooks/useTranslation";
9
+
10
+ export const UserMenu = () => {
11
+ const { user, signOut } = useAuth();
12
+ const navigate = useNavigate();
13
+ const { toast } = useToast();
14
+ const t = useTranslation();
15
+ const [mounted, setMounted] = useState(false);
16
+
17
+ useEffect(() => {
18
+ setMounted(true);
19
+ }, []);
20
+
21
+ if (!mounted) return null;
22
+
23
+ const handleSignOut = async () => {
24
+ await signOut();
25
+ toast({
26
+ title: t.auth.logoutSuccess.title,
27
+ description: t.auth.logoutSuccess.description,
28
+ });
29
+ navigate("/");
30
+ };
31
+
32
+ return (
33
+ <div className="flex items-center gap-4">
34
+ {user ? (
35
+ <div className="flex items-center gap-2">
36
+ <span className="text-sm hidden md:inline-block">
37
+ {user.email}
38
+ </span>
39
+ <Button
40
+ variant="outline"
41
+ size="sm"
42
+ onClick={handleSignOut}
43
+ className="flex items-center gap-2"
44
+ >
45
+ <LogOut className="h-4 w-4" />
46
+ <span className="hidden md:inline-block">
47
+ {t.auth.form.logout || "Logout"}
48
+ </span>
49
+ </Button>
50
+ </div>
51
+ ) : (
52
+ <div className="flex items-center gap-2">
53
+ <Button
54
+ variant="outline"
55
+ size="sm"
56
+ asChild
57
+ className="flex items-center gap-2"
58
+ >
59
+ <Link to="/auth/login">
60
+ <LogIn className="h-4 w-4" />
61
+ <span className="hidden md:inline-block">{t.auth.login.linkText}</span>
62
+ </Link>
63
+ </Button>
64
+ <Button
65
+ variant="default"
66
+ size="sm"
67
+ asChild
68
+ className="flex items-center gap-2"
69
+ >
70
+ <Link to="/auth/register">
71
+ <User className="h-4 w-4" />
72
+ <span className="hidden md:inline-block">{t.auth.register.linkText}</span>
73
+ </Link>
74
+ </Button>
75
+ </div>
76
+ )}
77
+ </div>
78
+ );
79
+ };
src/components/game/GameReview.tsx CHANGED
@@ -3,7 +3,7 @@ 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,
@@ -18,6 +18,8 @@ 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;
@@ -25,10 +27,13 @@ interface GameReviewProps {
25
  sessionId: string;
26
  currentTheme: string;
27
  fromSession?: string | null;
 
28
  }
29
 
30
  export const GameReview = ({
31
  currentScore,
 
 
32
  avgWordsPerRound,
33
  onPlayAgain,
34
  onBack,
@@ -36,6 +41,7 @@ export const GameReview = ({
36
  sessionId,
37
  currentTheme,
38
  fromSession,
 
39
  }: GameReviewProps) => {
40
  const t = useTranslation();
41
  const { toast } = useToast();
@@ -147,6 +153,12 @@ export const GameReview = ({
147
  onPlayAgain();
148
  };
149
 
 
 
 
 
 
 
150
  const renderComparisonResult = () => {
151
  if (!friendData) return null;
152
 
@@ -171,24 +183,58 @@ export const GameReview = ({
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
 
 
3
  import { useTranslation } from "@/hooks/useTranslation";
4
  import { Button } from "@/components/ui/button";
5
  import { Input } from "@/components/ui/input";
6
+ import { Copy, Home } from "lucide-react";
7
  import {
8
  Dialog,
9
  DialogContent,
 
18
 
19
  interface GameReviewProps {
20
  currentScore: number;
21
+ wrongGuesses: number;
22
+ totalRounds: number;
23
  avgWordsPerRound: number;
24
  onPlayAgain: (game_id?: string, fromSession?: string) => void;
25
  onBack?: () => void;
 
27
  sessionId: string;
28
  currentTheme: string;
29
  fromSession?: string | null;
30
+ words: string[];
31
  }
32
 
33
  export const GameReview = ({
34
  currentScore,
35
+ wrongGuesses,
36
+ totalRounds,
37
  avgWordsPerRound,
38
  onPlayAgain,
39
  onBack,
 
41
  sessionId,
42
  currentTheme,
43
  fromSession,
44
+ words,
45
  }: GameReviewProps) => {
46
  const t = useTranslation();
47
  const { toast } = useToast();
 
153
  onPlayAgain();
154
  };
155
 
156
+ const handleHomeClick = () => {
157
+ if (onBack) {
158
+ onBack();
159
+ }
160
+ };
161
+
162
  const renderComparisonResult = () => {
163
  if (!friendData) return null;
164
 
 
183
  animate={{ opacity: 1 }}
184
  className="text-center space-y-6"
185
  >
186
+ <div className="relative flex items-center justify-center">
187
+ <Button
188
+ variant="ghost"
189
+ size="icon"
190
+ className="absolute left-0 text-gray-600 hover:text-primary"
191
+ onClick={handleHomeClick}
192
+ >
193
+ <Home className="h-5 w-5" />
194
+ </Button>
195
+ <h2 className="text-2xl font-bold text-gray-900">
196
+ {t.game.review.title}
197
+ </h2>
198
+ </div>
199
 
200
  <div className="space-y-4">
201
+ <div className="space-y-2">
202
+ <div className="rounded-lg bg-gray-50 p-6">
203
+ <div className="flex justify-center items-center gap-8">
204
+ <div className="text-center">
205
+ <div className="text-3xl font-bold text-green-600">{currentScore}</div>
206
+ <div className="text-sm text-gray-600">{t.game.review.correct}</div>
207
+ </div>
208
+ <div className="text-center">
209
+ <div className="text-3xl font-bold text-red-600">{wrongGuesses}</div>
210
+ <div className="text-sm text-gray-600">{t.game.review.wrong}</div>
211
+ </div>
212
+ <div className="text-center">
213
+ <div className="text-3xl font-bold text-gray-600">{totalRounds}</div>
214
+ <div className="text-sm text-gray-600">{t.game.review.total}</div>
215
+ </div>
216
+ </div>
217
+ <div className="mt-4 space-y-1">
218
+ <div className="w-full bg-gray-200 rounded-full h-2">
219
+ <div
220
+ className="bg-gradient-to-r from-green-500 to-green-600 h-2 rounded-full"
221
+ style={{ width: `${(currentScore / totalRounds) * 100}%` }}
222
+ />
223
+ <div
224
+ className="bg-gradient-to-r from-red-500 to-red-600 h-2 rounded-full -mt-2"
225
+ style={{
226
+ width: `${(wrongGuesses / totalRounds) * 100}%`,
227
+ marginLeft: `${(currentScore / totalRounds) * 100}%`
228
+ }}
229
+ />
230
+ </div>
231
+ <p className="text-sm text-gray-600 text-center">
232
+ {t.game.review.avgWords}: {avgWordsPerRound.toFixed(1)}
233
+ </p>
234
+ </div>
235
+ </div>
236
  </div>
237
+ {renderComparisonResult()}
238
 
239
  <GameDetailsView gameResults={gameResults} fromSession={fromSession} />
240
 
src/components/game/GuessDisplay.tsx CHANGED
@@ -7,14 +7,22 @@ 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;
@@ -30,6 +38,9 @@ export const GuessDisplay = ({
30
  avgWordsPerRound,
31
  sessionId,
32
  currentTheme,
 
 
 
33
  onNextRound,
34
  onBack,
35
  onGameReview,
@@ -48,17 +59,13 @@ export const GuessDisplay = ({
48
  useEffect(() => {
49
  const handleKeyPress = (e: KeyboardEvent) => {
50
  if (e.key === 'Enter') {
51
- if (isGuessCorrect()) {
52
- onNextRound();
53
- } else {
54
- onGameReview();
55
- }
56
  }
57
  };
58
 
59
  window.addEventListener('keydown', handleKeyPress);
60
  return () => window.removeEventListener('keydown', handleKeyPress);
61
- }, [isGuessCorrect, onNextRound, onGameReview]);
62
 
63
  return (
64
  <motion.div
@@ -68,6 +75,9 @@ export const GuessDisplay = ({
68
  >
69
  <RoundHeader
70
  successfulRounds={currentScore}
 
 
 
71
  onBack={onBack}
72
  showConfirmDialog={showConfirmDialog}
73
  setShowConfirmDialog={handleSetShowConfirmDialog}
 
7
  import { GuessResult } from "./guess-display/GuessResult";
8
  import { ActionButtons } from "./guess-display/ActionButtons";
9
 
10
+ interface SentenceWord {
11
+ word: string;
12
+ provider: 'player' | 'ai';
13
+ }
14
+
15
  interface GuessDisplayProps {
16
  currentScore: number;
17
  currentWord: string;
18
+ sentence: SentenceWord[];
19
  aiGuess: string;
20
  avgWordsPerRound: number;
21
  sessionId: string;
22
  currentTheme: string;
23
+ totalRounds: number;
24
+ wrongGuesses: number;
25
+ guessSequence: Array<'success' | 'wrong'>;
26
  onNextRound: () => void;
27
  onGameReview: () => void;
28
  onBack?: () => void;
 
38
  avgWordsPerRound,
39
  sessionId,
40
  currentTheme,
41
+ totalRounds,
42
+ wrongGuesses,
43
+ guessSequence,
44
  onNextRound,
45
  onBack,
46
  onGameReview,
 
59
  useEffect(() => {
60
  const handleKeyPress = (e: KeyboardEvent) => {
61
  if (e.key === 'Enter') {
62
+ onNextRound();
 
 
 
 
63
  }
64
  };
65
 
66
  window.addEventListener('keydown', handleKeyPress);
67
  return () => window.removeEventListener('keydown', handleKeyPress);
68
+ }, [onNextRound]);
69
 
70
  return (
71
  <motion.div
 
75
  >
76
  <RoundHeader
77
  successfulRounds={currentScore}
78
+ totalRounds={totalRounds}
79
+ wrongGuesses={wrongGuesses}
80
+ guessSequence={guessSequence}
81
  onBack={onBack}
82
  showConfirmDialog={showConfirmDialog}
83
  setShowConfirmDialog={handleSetShowConfirmDialog}
src/components/game/ModelSelector.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useRef } from "react";
2
  import { Button } from "@/components/ui/button";
3
  import { motion } from "framer-motion";
4
  import { useTranslation } from "@/hooks/useTranslation";
@@ -6,6 +6,15 @@ import { useContext } from "react";
6
  import { LanguageContext } from "@/contexts/LanguageContext";
7
  import { ArrowLeft } from "lucide-react";
8
  import { modelNames } from "@/lib/modelNames";
 
 
 
 
 
 
 
 
 
9
 
10
  interface ModelSelectorProps {
11
  onModelSelect: (model: string) => void;
@@ -16,23 +25,34 @@ interface ModelSelectorProps {
16
  // based on the user's subscription level
17
  const AVAILABLE_MODELS = [
18
  "google/gemini-2.0-flash-lite-001",
19
- // "x-ai/grok-2-1212",
20
  "deepseek/deepseek-chat:free",
21
  "meta-llama/llama-3.3-70b-instruct:free",
 
 
 
 
 
 
 
 
 
 
22
  ];
23
 
24
  export const ModelSelector = ({ onModelSelect, onBack }: ModelSelectorProps) => {
25
  const [selectedModel, setSelectedModel] = useState<string>(AVAILABLE_MODELS[0]);
 
26
  const [isGenerating, setIsGenerating] = useState(false);
27
  const t = useTranslation();
28
  const { language } = useContext(LanguageContext);
 
29
 
30
  const handleSubmit = async () => {
31
  if (!selectedModel) return;
32
 
33
  setIsGenerating(true);
34
  try {
35
- await onModelSelect(selectedModel);
36
  } finally {
37
  setIsGenerating(false);
38
  }
@@ -64,6 +84,10 @@ export const ModelSelector = ({ onModelSelect, onBack }: ModelSelectorProps) =>
64
  case 'c':
65
  setSelectedModel(AVAILABLE_MODELS[2]);
66
  break;
 
 
 
 
67
  case 'enter':
68
  if (selectedModel) {
69
  handleSubmit();
@@ -74,7 +98,7 @@ export const ModelSelector = ({ onModelSelect, onBack }: ModelSelectorProps) =>
74
 
75
  window.addEventListener('keydown', handleKeyPress);
76
  return () => window.removeEventListener('keydown', handleKeyPress);
77
- }, [selectedModel, onBack, handleSubmit]);
78
 
79
  return (
80
  <motion.div
@@ -105,15 +129,59 @@ export const ModelSelector = ({ onModelSelect, onBack }: ModelSelectorProps) =>
105
  className="w-full justify-between"
106
  onClick={() => setSelectedModel(modelId)}
107
  >
108
- {modelNames[modelId]} <span className="text-sm opacity-50">{t.themes.pressKey} {String.fromCharCode(65 + index)}</span>
 
 
 
 
109
  </Button>
110
  ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  </div>
112
 
113
  <Button
114
  onClick={handleSubmit}
115
  className="w-full"
116
- disabled={!selectedModel || isGenerating}
117
  >
118
  {isGenerating ? t.models.generating : `${t.models.continue} ⏎`}
119
  </Button>
 
1
+ import { useState, useEffect } from "react";
2
  import { Button } from "@/components/ui/button";
3
  import { motion } from "framer-motion";
4
  import { useTranslation } from "@/hooks/useTranslation";
 
6
  import { LanguageContext } from "@/contexts/LanguageContext";
7
  import { ArrowLeft } from "lucide-react";
8
  import { modelNames } from "@/lib/modelNames";
9
+ import { useAuth } from "@/contexts/AuthContext";
10
+ import { Link } from "react-router-dom";
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from "@/components/ui/select";
18
 
19
  interface ModelSelectorProps {
20
  onModelSelect: (model: string) => void;
 
25
  // based on the user's subscription level
26
  const AVAILABLE_MODELS = [
27
  "google/gemini-2.0-flash-lite-001",
 
28
  "deepseek/deepseek-chat:free",
29
  "meta-llama/llama-3.3-70b-instruct:free",
30
+ "custom",
31
+ ];
32
+
33
+ // A larger set of models that can be selected
34
+ const SEARCHABLE_MODELS = [
35
+ "google/gemini-2.0-flash-001",
36
+ "anthropic/claude-3.7-sonnet",
37
+ "openai/gpt-4o",
38
+ "mistralai/mistral-large-2411",
39
+ "x-ai/grok-beta",
40
  ];
41
 
42
  export const ModelSelector = ({ onModelSelect, onBack }: ModelSelectorProps) => {
43
  const [selectedModel, setSelectedModel] = useState<string>(AVAILABLE_MODELS[0]);
44
+ const [customModel, setCustomModel] = useState<string>("");
45
  const [isGenerating, setIsGenerating] = useState(false);
46
  const t = useTranslation();
47
  const { language } = useContext(LanguageContext);
48
+ const { user } = useAuth();
49
 
50
  const handleSubmit = async () => {
51
  if (!selectedModel) return;
52
 
53
  setIsGenerating(true);
54
  try {
55
+ await onModelSelect(selectedModel === "custom" ? customModel : selectedModel);
56
  } finally {
57
  setIsGenerating(false);
58
  }
 
84
  case 'c':
85
  setSelectedModel(AVAILABLE_MODELS[2]);
86
  break;
87
+ case 'd':
88
+ e.preventDefault();
89
+ setSelectedModel("custom");
90
+ break;
91
  case 'enter':
92
  if (selectedModel) {
93
  handleSubmit();
 
98
 
99
  window.addEventListener('keydown', handleKeyPress);
100
  return () => window.removeEventListener('keydown', handleKeyPress);
101
+ }, [selectedModel, customModel, onBack, handleSubmit]);
102
 
103
  return (
104
  <motion.div
 
129
  className="w-full justify-between"
130
  onClick={() => setSelectedModel(modelId)}
131
  >
132
+ <div className="flex items-center gap-2">
133
+ {modelId === "custom" ? t.models.custom : modelNames[modelId]}
134
+ {modelId === "custom" && !user}
135
+ </div>
136
+ <span className="text-sm opacity-50">{t.themes.pressKey} {String.fromCharCode(65 + index)}</span>
137
  </Button>
138
  ))}
139
+
140
+ {selectedModel === "custom" && (
141
+ <motion.div
142
+ initial={{ opacity: 0, height: 0 }}
143
+ animate={{ opacity: 1, height: "auto" }}
144
+ exit={{ opacity: 0, height: 0 }}
145
+ transition={{ duration: 0.2 }}
146
+ >
147
+ {user ? (
148
+ <Select
149
+ value={customModel}
150
+ onValueChange={setCustomModel}
151
+ >
152
+ <SelectTrigger className="w-full">
153
+ <SelectValue placeholder={t.models.searchPlaceholder} />
154
+ </SelectTrigger>
155
+ <SelectContent>
156
+ {SEARCHABLE_MODELS.map((model) => (
157
+ <SelectItem key={model} value={model}>
158
+ {modelNames[model] || model}
159
+ </SelectItem>
160
+ ))}
161
+ </SelectContent>
162
+ </Select>
163
+ ) : (
164
+ <div className="text-center text-sm text-gray-600">
165
+ <p>{t.models.loginRequired}</p>
166
+ <div className="mt-2 flex justify-center gap-2">
167
+ <Link to="/auth/login" className="text-primary hover:underline">
168
+ {t.auth.login.linkText}
169
+ </Link>
170
+ <span>or</span>
171
+ <Link to="/auth/register" className="text-primary hover:underline">
172
+ {t.auth.register.linkText}
173
+ </Link>
174
+ </div>
175
+ </div>
176
+ )}
177
+ </motion.div>
178
+ )}
179
  </div>
180
 
181
  <Button
182
  onClick={handleSubmit}
183
  className="w-full"
184
+ disabled={!selectedModel || (selectedModel === "custom" && !customModel && user !== null) || isGenerating}
185
  >
186
  {isGenerating ? t.models.generating : `${t.models.continue} ⏎`}
187
  </Button>
src/components/game/SentenceBuilder.tsx CHANGED
@@ -25,6 +25,9 @@ interface SentenceWord {
25
  interface SentenceBuilderProps {
26
  currentWord: string;
27
  successfulRounds: number;
 
 
 
28
  sentence: SentenceWord[];
29
  playerInput: string;
30
  isAiThinking: boolean;
@@ -39,6 +42,9 @@ interface SentenceBuilderProps {
39
  export const SentenceBuilder = ({
40
  currentWord,
41
  successfulRounds,
 
 
 
42
  sentence,
43
  playerInput,
44
  isAiThinking,
@@ -55,8 +61,6 @@ export const SentenceBuilder = ({
55
  const [isTooLong, setIsTooLong] = useState(false);
56
  const t = useTranslation();
57
 
58
- console.log("SentenceBuilder - Rendering with showConfirmDialog:", showConfirmDialog);
59
-
60
  const validateInput = (input: string) => {
61
  setHasMultipleWords(input.trim().split(/\s+/).length > 1);
62
  setContainsTargetWord(
@@ -85,6 +89,9 @@ export const SentenceBuilder = ({
85
  >
86
  <RoundHeader
87
  successfulRounds={successfulRounds}
 
 
 
88
  onBack={onBack}
89
  showConfirmDialog={showConfirmDialog}
90
  setShowConfirmDialog={handleSetShowConfirmDialog}
@@ -107,6 +114,16 @@ export const SentenceBuilder = ({
107
  sentence={sentence}
108
  />
109
 
 
 
 
 
 
 
 
 
 
 
110
  <AlertDialog open={showConfirmDialog} onOpenChange={handleSetShowConfirmDialog}>
111
  <AlertDialogContent>
112
  <AlertDialogHeader>
 
25
  interface SentenceBuilderProps {
26
  currentWord: string;
27
  successfulRounds: number;
28
+ totalRounds: number;
29
+ wrongGuesses: number;
30
+ guessSequence: Array<'success' | 'wrong'>;
31
  sentence: SentenceWord[];
32
  playerInput: string;
33
  isAiThinking: boolean;
 
42
  export const SentenceBuilder = ({
43
  currentWord,
44
  successfulRounds,
45
+ totalRounds,
46
+ wrongGuesses,
47
+ guessSequence,
48
  sentence,
49
  playerInput,
50
  isAiThinking,
 
61
  const [isTooLong, setIsTooLong] = useState(false);
62
  const t = useTranslation();
63
 
 
 
64
  const validateInput = (input: string) => {
65
  setHasMultipleWords(input.trim().split(/\s+/).length > 1);
66
  setContainsTargetWord(
 
89
  >
90
  <RoundHeader
91
  successfulRounds={successfulRounds}
92
+ totalRounds={totalRounds}
93
+ wrongGuesses={wrongGuesses}
94
+ guessSequence={guessSequence}
95
  onBack={onBack}
96
  showConfirmDialog={showConfirmDialog}
97
  setShowConfirmDialog={handleSetShowConfirmDialog}
 
114
  sentence={sentence}
115
  />
116
 
117
+ <div className="mt-4 flex justify-center gap-4 text-sm">
118
+ <Button
119
+ variant="link"
120
+ className="text-muted-foreground hover:text-primary"
121
+ onClick={onClose}
122
+ >
123
+ {t.game.finishGame}
124
+ </Button>
125
+ </div>
126
+
127
  <AlertDialog open={showConfirmDialog} onOpenChange={handleSetShowConfirmDialog}>
128
  <AlertDialogContent>
129
  <AlertDialogHeader>
src/components/game/WelcomeScreen.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import { motion } from "framer-motion";
2
  import { useState, useEffect } from "react";
3
  import { HighScoreBoard } from "../HighScoreBoard";
@@ -9,8 +10,9 @@ import { HuggingFaceLink } from "./welcome/HuggingFaceLink";
9
  import { MainActions } from "./welcome/MainActions";
10
  import { HowToPlayDialog } from "./welcome/HowToPlayDialog";
11
  import { CreditsDialog } from "./welcome/CreditsDialog";
12
- import { Mail } from "lucide-react";
13
  import { StatsDialog } from "./welcome/StatsDialog";
 
14
 
15
  interface WelcomeScreenProps {
16
  onStartDaily: () => void;
@@ -44,7 +46,7 @@ export const WelcomeScreen = ({ onStartDaily, onStartNew }: WelcomeScreenProps)
44
  >
45
  <div className="relative">
46
  <h1 className="mb-4 text-4xl font-bold text-gray-900">{t.welcome.title}</h1>
47
- <div className="absolute top-0 right-0">
48
  <LanguageSelector />
49
  </div>
50
  <p className="text-lg text-gray-600">
@@ -70,9 +72,10 @@ export const WelcomeScreen = ({ onStartDaily, onStartNew }: WelcomeScreenProps)
70
  <div className="flex justify-center items-center gap-4">
71
  <button
72
  onClick={() => setShowCredits(true)}
73
- className="text-primary hover:text-primary/80 transition-colors"
74
  >
75
- Made by M1X
 
76
  </button>
77
  <span>•</span>
78
  <a
@@ -86,9 +89,10 @@ export const WelcomeScreen = ({ onStartDaily, onStartNew }: WelcomeScreenProps)
86
  <span>•</span>
87
  <button
88
  onClick={() => setShowStats(true)}
89
- className="text-primary hover:text-primary/80 transition-colors"
90
  >
91
- {t.welcome.stats.title}
 
92
  </button>
93
  </div>
94
  </div>
 
1
+
2
  import { motion } from "framer-motion";
3
  import { useState, useEffect } from "react";
4
  import { HighScoreBoard } from "../HighScoreBoard";
 
10
  import { MainActions } from "./welcome/MainActions";
11
  import { HowToPlayDialog } from "./welcome/HowToPlayDialog";
12
  import { CreditsDialog } from "./welcome/CreditsDialog";
13
+ import { Mail, Award, BarChart } from "lucide-react";
14
  import { StatsDialog } from "./welcome/StatsDialog";
15
+ import { UserMenu } from "../auth/UserMenu";
16
 
17
  interface WelcomeScreenProps {
18
  onStartDaily: () => void;
 
46
  >
47
  <div className="relative">
48
  <h1 className="mb-4 text-4xl font-bold text-gray-900">{t.welcome.title}</h1>
49
+ <div className="absolute top-0 right-0 flex items-center gap-4">
50
  <LanguageSelector />
51
  </div>
52
  <p className="text-lg text-gray-600">
 
72
  <div className="flex justify-center items-center gap-4">
73
  <button
74
  onClick={() => setShowCredits(true)}
75
+ className="inline-flex items-center gap-2 text-primary hover:text-primary/80 transition-colors"
76
  >
77
+ <Award className="w-4 h-4" />
78
+ Credits
79
  </button>
80
  <span>•</span>
81
  <a
 
89
  <span>•</span>
90
  <button
91
  onClick={() => setShowStats(true)}
92
+ className="inline-flex items-center gap-2 text-primary hover:text-primary/80 transition-colors"
93
  >
94
+ <BarChart className="w-4 h-4" />
95
+ <span>{t.welcome.stats.title}</span>
96
  </button>
97
  </div>
98
  </div>
src/components/game/WordDisplay.tsx CHANGED
@@ -10,18 +10,6 @@ interface WordDisplayProps {
10
  }
11
 
12
  export const WordDisplay = ({ currentWord, successfulRounds, onContinue }: WordDisplayProps) => {
13
- const [imageLoaded, setImageLoaded] = useState(false);
14
- const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
15
- const isMobile = useIsMobile();
16
-
17
- useEffect(() => {
18
- if (!isMobile) {
19
- const img = new Image();
20
- img.onload = () => setImageLoaded(true);
21
- img.src = imagePath;
22
- console.log("Attempting to load image:", imagePath);
23
- }
24
- }, [imagePath, isMobile]);
25
 
26
  return (
27
  <motion.div
@@ -31,13 +19,6 @@ export const WordDisplay = ({ currentWord, successfulRounds, onContinue }: WordD
31
  >
32
  <h2 className="mb-4 text-2xl font-semibold text-gray-900">Your Word</h2>
33
  <div className="mb-4 overflow-hidden rounded-lg bg-secondary/10">
34
- {!isMobile && imageLoaded && (
35
- <img
36
- src={imagePath}
37
- alt={currentWord}
38
- className="mx-auto h-48 w-full object-cover"
39
- />
40
- )}
41
  <p className="p-6 text-4xl font-bold tracking-wider text-secondary">
42
  {currentWord}
43
  </p>
 
10
  }
11
 
12
  export const WordDisplay = ({ currentWord, successfulRounds, onContinue }: WordDisplayProps) => {
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  return (
15
  <motion.div
 
19
  >
20
  <h2 className="mb-4 text-2xl font-semibold text-gray-900">Your Word</h2>
21
  <div className="mb-4 overflow-hidden rounded-lg bg-secondary/10">
 
 
 
 
 
 
 
22
  <p className="p-6 text-4xl font-bold tracking-wider text-secondary">
23
  {currentWord}
24
  </p>
src/components/game/guess-display/ActionButtons.tsx CHANGED
@@ -21,11 +21,9 @@ export const ActionButtons = ({
21
 
22
  return (
23
  <div className="flex justify-center gap-4">
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
  );
31
  };
 
21
 
22
  return (
23
  <div className="flex justify-center gap-4">
24
+ <Button onClick={onNextRound} className="text-white">
25
+ {isCorrect ? t.game.nextRound : t.game.nextWord}
26
+ </Button>
 
 
27
  </div>
28
  );
29
  };
src/components/game/sentence-builder/RoundHeader.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import { House } from "lucide-react";
2
  import { Button } from "@/components/ui/button";
 
3
  import { useTranslation } from "@/hooks/useTranslation";
4
  import {
5
  AlertDialog,
@@ -14,6 +14,9 @@ import {
14
 
15
  interface RoundHeaderProps {
16
  successfulRounds: number;
 
 
 
17
  onBack?: () => void;
18
  showConfirmDialog: boolean;
19
  setShowConfirmDialog: (show: boolean) => void;
@@ -22,53 +25,75 @@ interface RoundHeaderProps {
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) {
35
- console.log("RoundHeader - Setting showConfirmDialog to true");
36
  setShowConfirmDialog(true);
37
  } else {
38
- console.log("RoundHeader - No successful rounds, navigating directly");
39
  onBack?.();
40
  }
41
  };
42
 
43
  const handleDialogChange = (open: boolean) => {
44
- console.log("RoundHeader - Dialog state changing to:", open);
45
  setShowConfirmDialog(open);
46
- if (!open) { // Dialog is closing
47
- console.log("RoundHeader - Dialog closing, triggering navigation");
48
- onBack?.();
49
  }
50
  };
51
 
52
- return (
53
- <div className="relative">
54
- <div className="absolute right-0 top-0 bg-primary/10 px-3 py-1 rounded-lg">
55
- <span className="text-sm font-medium text-primary">
56
- {t.game.round} {successfulRounds + 1}
57
- </span>
58
- </div>
59
 
 
 
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" />
67
  </Button>
68
 
69
- <h2 className="mb-4 text-2xl font-semibold text-gray-900">
70
- {t.game.title}
71
- </h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  <AlertDialog open={showConfirmDialog} onOpenChange={handleDialogChange}>
74
  <AlertDialogContent>
 
 
1
  import { Button } from "@/components/ui/button";
2
+ import { Home } from "lucide-react";
3
  import { useTranslation } from "@/hooks/useTranslation";
4
  import {
5
  AlertDialog,
 
14
 
15
  interface RoundHeaderProps {
16
  successfulRounds: number;
17
+ totalRounds: number;
18
+ wrongGuesses: number;
19
+ guessSequence: Array<'success' | 'wrong'>;
20
  onBack?: () => void;
21
  showConfirmDialog: boolean;
22
  setShowConfirmDialog: (show: boolean) => void;
 
25
 
26
  export const RoundHeader = ({
27
  successfulRounds,
28
+ totalRounds,
29
+ wrongGuesses,
30
+ guessSequence,
31
  onBack,
32
  showConfirmDialog,
33
  setShowConfirmDialog,
34
  onCancel
35
  }: RoundHeaderProps) => {
36
  const t = useTranslation();
37
+ const currentRound = successfulRounds + wrongGuesses;
38
+ const remainingRounds = totalRounds - currentRound;
39
+ const isShortGame = totalRounds <= 10;
40
 
41
  const handleHomeClick = () => {
42
+ if (successfulRounds > 0 || wrongGuesses > 0) {
 
 
43
  setShowConfirmDialog(true);
44
  } else {
 
45
  onBack?.();
46
  }
47
  };
48
 
49
  const handleDialogChange = (open: boolean) => {
 
50
  setShowConfirmDialog(open);
51
+ if (!open && onBack) {
52
+ onBack();
 
53
  }
54
  };
55
 
56
+ // Create an array of results in sequence for games with 10 or fewer words
57
+ const results = Array(totalRounds).fill('pending').map((_, index) => {
58
+ return guessSequence[index] || 'pending';
59
+ });
 
 
 
60
 
61
+ return (
62
+ <div className="relative mb-2 min-h-[3rem]">
63
  <Button
64
  variant="ghost"
65
  size="icon"
66
  className="absolute left-0 top-0 text-gray-600 hover:text-white"
67
  onClick={handleHomeClick}
68
  >
69
+ <Home className="h-5 w-5" />
70
  </Button>
71
 
72
+ {isShortGame ? (
73
+ <div className="flex justify-center items-center h-12">
74
+ {results.map((result, index) => (
75
+ <div
76
+ key={index}
77
+ className={`h-2.5 w-2.5 rounded-full transition-colors duration-300 mx-1 ${
78
+ result === 'success' ? 'bg-green-500' :
79
+ result === 'wrong' ? 'bg-red-500' :
80
+ 'bg-gray-300'
81
+ }`}
82
+ />
83
+ ))}
84
+ </div>
85
+ ) : (
86
+ <div className="absolute right-0 top-0 flex items-center gap-3 bg-primary/5 px-3 py-1.5 rounded-lg">
87
+ <div className="flex items-center gap-1.5">
88
+ <div className="h-2.5 w-2.5 rounded-full bg-green-500" />
89
+ <span className="text-sm font-medium text-green-700">{successfulRounds}</span>
90
+ </div>
91
+ <div className="flex items-center gap-1.5">
92
+ <div className="h-2.5 w-2.5 rounded-full bg-red-500" />
93
+ <span className="text-sm font-medium text-red-700">{wrongGuesses}</span>
94
+ </div>
95
+ </div>
96
+ )}
97
 
98
  <AlertDialog open={showConfirmDialog} onOpenChange={handleDialogChange}>
99
  <AlertDialogContent>
src/components/game/sentence-builder/WordDisplay.tsx CHANGED
@@ -6,30 +6,14 @@ interface WordDisplayProps {
6
  }
7
 
8
  export const WordDisplay = ({ currentWord }: WordDisplayProps) => {
9
- const [imageLoaded, setImageLoaded] = useState(false);
10
- const imagePath = `/think_in_sync_assets/${currentWord.toLowerCase()}.jpg`;
11
  const t = useTranslation();
12
 
13
- useEffect(() => {
14
- const img = new Image();
15
- img.onload = () => setImageLoaded(true);
16
- img.src = imagePath;
17
- console.log("Attempting to load image:", imagePath);
18
- }, [imagePath]);
19
-
20
  return (
21
  <div>
22
  <p className="mb-1 text-sm text-gray-600">
23
  {t.game.describeWord}
24
  </p>
25
  <div className="mb-6 overflow-hidden rounded-lg bg-secondary/10">
26
- {imageLoaded && (
27
- <img
28
- src={imagePath}
29
- alt={currentWord}
30
- className="mx-auto h-48 w-full object-cover"
31
- />
32
- )}
33
  <p className="p-4 text-2xl font-bold tracking-wider text-secondary">
34
  {currentWord}
35
  </p>
 
6
  }
7
 
8
  export const WordDisplay = ({ currentWord }: WordDisplayProps) => {
 
 
9
  const t = useTranslation();
10
 
 
 
 
 
 
 
 
11
  return (
12
  <div>
13
  <p className="mb-1 text-sm text-gray-600">
14
  {t.game.describeWord}
15
  </p>
16
  <div className="mb-6 overflow-hidden rounded-lg bg-secondary/10">
 
 
 
 
 
 
 
17
  <p className="p-4 text-2xl font-bold tracking-wider text-secondary">
18
  {currentWord}
19
  </p>
src/components/game/welcome/CreditsDialog.tsx CHANGED
@@ -1,5 +1,5 @@
1
-
2
  import { Dialog, DialogContent } from "@/components/ui/dialog";
 
3
 
4
  interface CreditsDialogProps {
5
  open: boolean;
@@ -11,7 +11,7 @@ export const CreditsDialog = ({ open, onOpenChange }: CreditsDialogProps) => {
11
  <Dialog open={open} onOpenChange={onOpenChange}>
12
  <DialogContent className="max-w-md">
13
  <h2 className="text-2xl font-bold mb-4">Credits</h2>
14
- <div className="space-y-2 text-gray-600">
15
  <p>
16
  Made by M1X. We are{" "}
17
  <a href="https://www.linkedin.com/in/sandro-mikautadze/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Sandro</a>,{" "}
@@ -21,6 +21,17 @@ export const CreditsDialog = ({ open, onOpenChange }: CreditsDialogProps) => {
21
  <a href="https://www.linkedin.com/in/michael-sheroubi/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Emiliano</a>,{" "}
22
  and <a href="https://felixzieger.de/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Felix</a>.
23
  </p>
 
 
 
 
 
 
 
 
 
 
 
24
  </div>
25
  </DialogContent>
26
  </Dialog>
 
 
1
  import { Dialog, DialogContent } from "@/components/ui/dialog";
2
+ import { Github } from "lucide-react";
3
 
4
  interface CreditsDialogProps {
5
  open: boolean;
 
11
  <Dialog open={open} onOpenChange={onOpenChange}>
12
  <DialogContent className="max-w-md">
13
  <h2 className="text-2xl font-bold mb-4">Credits</h2>
14
+ <div className="space-y-4 text-gray-600">
15
  <p>
16
  Made by M1X. We are{" "}
17
  <a href="https://www.linkedin.com/in/sandro-mikautadze/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Sandro</a>,{" "}
 
21
  <a href="https://www.linkedin.com/in/michael-sheroubi/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Emiliano</a>,{" "}
22
  and <a href="https://felixzieger.de/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Felix</a>.
23
  </p>
24
+ <div className="flex items-center justify-center pt-2">
25
+ <a
26
+ href="https://github.com/felixzieger/think-in-sync/"
27
+ target="_blank"
28
+ rel="noopener noreferrer"
29
+ className="inline-flex items-center gap-2 text-primary hover:text-primary/80 transition-colors"
30
+ >
31
+ <Github className="w-5 h-5" />
32
+ <span>View source on GitHub</span>
33
+ </a>
34
+ </div>
35
  </div>
36
  </DialogContent>
37
  </Dialog>
src/components/game/welcome/HowToPlayDialog.tsx CHANGED
@@ -33,6 +33,13 @@ export const HowToPlayDialog = ({ open, onOpenChange }: HowToPlayDialogProps) =>
33
  ))}
34
  </ul>
35
  </div>
 
 
 
 
 
 
 
36
  </div>
37
  </div>
38
  </DialogContent>
 
33
  ))}
34
  </ul>
35
  </div>
36
+ <div>
37
+ <h3 className="font-medium text-gray-800">{t.howToPlay.gameModes.title}</h3>
38
+ <ul className="list-disc list-inside space-y-1">
39
+ <li>{t.howToPlay.gameModes.daily}</li>
40
+ <li>{t.howToPlay.gameModes.custom}</li>
41
+ </ul>
42
+ </div>
43
  </div>
44
  </div>
45
  </DialogContent>
src/contexts/AuthContext.tsx ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createContext, useState, useEffect, useContext, ReactNode } from "react";
2
+ import { supabase } from "@/integrations/supabase/client";
3
+ import { Session, User } from "@supabase/supabase-js";
4
+
5
+ type AuthContextType = {
6
+ session: Session | null;
7
+ user: User | null;
8
+ loading: boolean;
9
+ signIn: (email: string, password: string) => Promise<{
10
+ error: Error | null;
11
+ success: boolean;
12
+ }>;
13
+ signUp: (email: string, password: string) => Promise<{
14
+ error: Error | null;
15
+ success: boolean;
16
+ }>;
17
+ signInWithGoogle: () => Promise<{
18
+ error: Error | null;
19
+ success: boolean;
20
+ }>;
21
+ signInWithGitHub: () => Promise<{
22
+ error: Error | null;
23
+ success: boolean;
24
+ }>;
25
+ signOut: () => Promise<void>;
26
+ };
27
+
28
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
29
+
30
+ export const AuthProvider = ({ children }: { children: ReactNode }) => {
31
+ const [session, setSession] = useState<Session | null>(null);
32
+ const [user, setUser] = useState<User | null>(null);
33
+ const [loading, setLoading] = useState(true);
34
+
35
+ useEffect(() => {
36
+ // Set up auth state listener FIRST
37
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(
38
+ (event, session) => {
39
+ setSession(session);
40
+ setUser(session?.user ?? null);
41
+ }
42
+ );
43
+
44
+ // THEN check for existing session
45
+ supabase.auth.getSession().then(({ data: { session } }) => {
46
+ setSession(session);
47
+ setUser(session?.user ?? null);
48
+ setLoading(false);
49
+ });
50
+
51
+ return () => subscription.unsubscribe();
52
+ }, []);
53
+
54
+ const signIn = async (email: string, password: string) => {
55
+ try {
56
+ const { error } = await supabase.auth.signInWithPassword({
57
+ email,
58
+ password,
59
+ });
60
+ return { error, success: !error };
61
+ } catch (error) {
62
+ return { error: error as Error, success: false };
63
+ }
64
+ };
65
+
66
+ const signUp = async (email: string, password: string) => {
67
+ try {
68
+ const { error } = await supabase.auth.signUp({
69
+ email,
70
+ password,
71
+ });
72
+ return { error, success: !error };
73
+ } catch (error) {
74
+ return { error: error as Error, success: false };
75
+ }
76
+ };
77
+
78
+ const signInWithGoogle = async () => {
79
+ try {
80
+ const { data, error } = await supabase.auth.signInWithOAuth({
81
+ provider: 'google',
82
+ options: {
83
+ queryParams: {
84
+ access_type: 'offline',
85
+ prompt: 'consent',
86
+ },
87
+ },
88
+ });
89
+ return { error, success: !error };
90
+ } catch (error) {
91
+ return { error: error as Error, success: false };
92
+ }
93
+ };
94
+
95
+ const signInWithGitHub = async () => {
96
+ try {
97
+ const { data, error } = await supabase.auth.signInWithOAuth({
98
+ provider: 'github',
99
+ });
100
+ return { error, success: !error };
101
+ } catch (error) {
102
+ return { error: error as Error, success: false };
103
+ }
104
+ };
105
+
106
+ const signOut = async () => {
107
+ await supabase.auth.signOut();
108
+ };
109
+
110
+ return (
111
+ <AuthContext.Provider
112
+ value={{
113
+ session,
114
+ user,
115
+ loading,
116
+ signIn,
117
+ signUp,
118
+ signInWithGoogle,
119
+ signInWithGitHub,
120
+ signOut,
121
+ }}
122
+ >
123
+ {children}
124
+ </AuthContext.Provider>
125
+ );
126
+ };
127
+
128
+ export const useAuth = () => {
129
+ const context = useContext(AuthContext);
130
+ if (context === undefined) {
131
+ throw new Error("useAuth must be used within an AuthProvider");
132
+ }
133
+ return context;
134
+ };
src/contexts/LanguageContext.tsx CHANGED
@@ -20,17 +20,14 @@ export const LanguageProvider = ({ children }: LanguageProviderProps) => {
20
 
21
  useEffect(() => {
22
  const savedLang = localStorage.getItem('language') as Language;
23
- console.log('[LanguageContext] Initial load - Saved language:', savedLang);
24
  if (savedLang && ['en', 'fr', 'de', 'it', 'es', 'pt'].includes(savedLang)) {
25
  setLanguage(savedLang);
26
  }
27
  }, []);
28
 
29
  const handleSetLanguage = (lang: Language) => {
30
- console.log('[LanguageContext] Setting new language:', lang);
31
  setLanguage(lang);
32
  localStorage.setItem('language', lang);
33
- console.log('[LanguageContext] Updated localStorage:', localStorage.getItem('language'));
34
  };
35
 
36
  return (
 
20
 
21
  useEffect(() => {
22
  const savedLang = localStorage.getItem('language') as Language;
 
23
  if (savedLang && ['en', 'fr', 'de', 'it', 'es', 'pt'].includes(savedLang)) {
24
  setLanguage(savedLang);
25
  }
26
  }, []);
27
 
28
  const handleSetLanguage = (lang: Language) => {
 
29
  setLanguage(lang);
30
  localStorage.setItem('language', lang);
 
31
  };
32
 
33
  return (
src/env.d.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_GOOGLE_CLIENT_ID: string
5
+ // Add other env variables here if needed
6
+ }
7
+
8
+ interface ImportMeta {
9
+ readonly env: ImportMetaEnv
10
+ }
src/i18n/translations/de.ts CHANGED
@@ -21,9 +21,12 @@ export const de = {
21
  confirm: "Bestätigen",
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",
@@ -48,7 +51,8 @@ export const de = {
48
  yourDescription: "Deine Beschreibung",
49
  friendDescription: "Beschreibung des Freundes",
50
  aiGuessed: "KI hat geraten",
51
- words: "Wörter"
 
52
  },
53
  invitation: {
54
  title: "Spieleinladung",
@@ -83,7 +87,7 @@ export const de = {
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: {
@@ -133,8 +137,8 @@ export const de = {
133
  title: "Think in Sync",
134
  subtitle: "Arbeite mit KI zusammen, um einen Hinweis zu erstellen und lass eine andere KI dein geheimes Wort erraten!",
135
  startButton: "Spiel starten",
136
- startDailyButton: "Tägliche Herausforderung",
137
- startNewButton: "Neues Spiel",
138
  dailyLeaderboard: "Tagesranking",
139
  howToPlay: "Spielanleitung",
140
  leaderboard: "Bestenliste",
@@ -186,12 +190,70 @@ export const de = {
186
  "Sei kreativ und beschreibend",
187
  "Die KI wird nach jedem Satz versuchen, dein Wort zu erraten"
188
  ]
 
 
 
 
 
189
  }
190
  },
191
  models: {
192
  title: "KI-Modell wählen",
193
  subtitle: "Wähle das KI-Modell, das mit dir zusammen spielen wird",
194
  continue: "Weiter",
195
- generating: "Wird generiert..."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  }
197
  };
 
21
  confirm: "Bestätigen",
22
  describeWord: "Dein Ziel ist es folgendes Wort zu beschreiben",
23
  nextRound: "Nächste Runde",
24
+ nextWord: "Nächstes Wort",
25
  playAgain: "Erneut spielen",
26
  saveScore: "Punktzahl speichern",
27
  playNewWords: "Neue Wörter spielen",
28
+ skipWord: "Wort überspringen",
29
+ finishGame: "Spiel beenden",
30
  review: {
31
  title: "Spielübersicht",
32
  successfulRounds: "Erfolgreiche Runden",
 
51
  yourDescription: "Deine Beschreibung",
52
  friendDescription: "Beschreibung des Freundes",
53
  aiGuessed: "KI hat geraten",
54
+ words: "Wörter",
55
+ avgWords: "Durchschnittliche Wörter pro Runde"
56
  },
57
  invitation: {
58
  title: "Spieleinladung",
 
87
  scoreSubmitted: "Punktzahl eingereicht!",
88
  scoreSubmittedDesc: "Deine Punktzahl wurde zur Bestenliste hinzugefügt",
89
  modes: {
90
+ daily: "Die heutigen Täglichen 10",
91
  "all-time": "Bestenliste"
92
  },
93
  error: {
 
137
  title: "Think in Sync",
138
  subtitle: "Arbeite mit KI zusammen, um einen Hinweis zu erstellen und lass eine andere KI dein geheimes Wort erraten!",
139
  startButton: "Spiel starten",
140
+ startDailyButton: "Spiele die Täglichen 10",
141
+ startNewButton: "Freestyle spielen",
142
  dailyLeaderboard: "Tagesranking",
143
  howToPlay: "Spielanleitung",
144
  leaderboard: "Bestenliste",
 
190
  "Sei kreativ und beschreibend",
191
  "Die KI wird nach jedem Satz versuchen, dein Wort zu erraten"
192
  ]
193
+ },
194
+ gameModes: {
195
+ title: "Spielmodi",
196
+ daily: "Tägliche 10: Alle Spieler erhalten die gleiche Wortliste, die sich alle 24 Stunden erneuert",
197
+ custom: "Freestyle: Wähle dein eigenes Thema und spiele dein persönliches Spiel"
198
  }
199
  },
200
  models: {
201
  title: "KI-Modell wählen",
202
  subtitle: "Wähle das KI-Modell, das mit dir zusammen spielen wird",
203
  continue: "Weiter",
204
+ generating: "Wird generiert...",
205
+ custom: "Benutzerdefiniertes Modell",
206
+ searchPlaceholder: "Nach einem Modell suchen...",
207
+ loginRequired: "Bitte melde dich an oder registriere dich, um benutzerdefinierte Modelle zu verwenden"
208
+ },
209
+ auth: {
210
+ login: {
211
+ linkText: "Anmelden",
212
+ title: "Anmelden",
213
+ subtitle: "Melde dich bei deinem Konto an",
214
+ email: "E-Mail",
215
+ password: "Passwort",
216
+ submit: "Anmelden",
217
+ loggingIn: "Wird angemeldet...",
218
+ noAccount: "Noch kein Konto?",
219
+ register: "Registrieren"
220
+ },
221
+ loginSuccess: {
222
+ title: "Anmeldung erfolgreich",
223
+ description: "Du wurdest erfolgreich angemeldet"
224
+ },
225
+ loginError: {
226
+ title: "Anmeldung fehlgeschlagen",
227
+ description: "Bei der Anmeldung ist ein Fehler aufgetreten"
228
+ },
229
+ register: {
230
+ linkText: "Registrieren",
231
+ title: "Registrieren",
232
+ description: "Erstelle ein neues Konto",
233
+ email: "E-Mail",
234
+ password: "Passwort",
235
+ confirmPassword: "Passwort bestätigen",
236
+ submit: "Registrieren",
237
+ registering: "Registrieren...",
238
+ haveAccount: "Bereits ein Konto?",
239
+ login: "Anmelden"
240
+ },
241
+ registerSuccess: {
242
+ title: "Registrierung erfolgreich",
243
+ description: "Dein Konto wurde erfolgreich erstellt"
244
+ },
245
+ registerError: {
246
+ title: "Registrierung fehlgeschlagen",
247
+ description: "Bei der Registrierung ist ein Fehler aufgetreten"
248
+ },
249
+ logoutSuccess: {
250
+ title: "Abgemeldet",
251
+ description: "Du wurdest erfolgreich abgemeldet"
252
+ },
253
+ form: {
254
+ email: "E-Mail",
255
+ password: "Passwort",
256
+ logout: "Abmelden"
257
+ }
258
  }
259
  };
src/i18n/translations/en.ts CHANGED
@@ -21,8 +21,11 @@ export const en = {
21
  confirm: "Confirm",
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",
@@ -37,6 +40,10 @@ export const en = {
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",
@@ -82,7 +89,7 @@ export const en = {
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: {
@@ -132,8 +139,8 @@ export const en = {
132
  title: "Think in Sync",
133
  subtitle: "Team up with AI to craft a clue and have a different AI guess your secret word!",
134
  startButton: "Start Game",
135
- startDailyButton: "Daily Challenge",
136
- startNewButton: "New Game",
137
  dailyLeaderboard: "Today's Ranking",
138
  howToPlay: "How to Play",
139
  leaderboard: "Leaderboard",
@@ -185,12 +192,70 @@ export const en = {
185
  "Try to be creative and descriptive",
186
  "The AI will try to guess your word after each sentence"
187
  ]
 
 
 
 
 
188
  }
189
  },
190
  models: {
191
  title: "Choose an AI Model",
192
  subtitle: "Select the AI model that will play together with you",
193
  continue: "Continue",
194
- generating: "Generating..."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  }
196
  };
 
21
  confirm: "Confirm",
22
  describeWord: "Your goal is to describe the word",
23
  nextRound: "Next Round",
24
+ nextWord: "Next Word",
25
  playAgain: "Play Again",
26
  saveScore: "Save Score",
27
+ skipWord: "Skip word",
28
+ finishGame: "Finish game",
29
  review: {
30
  title: "Game Review",
31
  successfulRounds: "Successful Rounds",
 
40
  urlCopyErrorDesc: "Please try copying the URL manually",
41
  youWin: "You Won!",
42
  youLost: "You Lost!",
43
+ correct: "Correct",
44
+ wrong: "Wrong",
45
+ total: "Total",
46
+ avgWords: "Average Words per Round",
47
  friendScore: (score: number, avgWords: string) =>
48
  `The person that challenged you completed ${score} rounds successfully with an average of ${avgWords} words.`,
49
  word: "Word",
 
89
  scoreSubmitted: "Score Submitted!",
90
  scoreSubmittedDesc: "Your score has been added to the leaderboard",
91
  modes: {
92
+ daily: "Daily 10",
93
  "all-time": "All Time"
94
  },
95
  error: {
 
139
  title: "Think in Sync",
140
  subtitle: "Team up with AI to craft a clue and have a different AI guess your secret word!",
141
  startButton: "Start Game",
142
+ startDailyButton: "Play Daily 10",
143
+ startNewButton: "Play Freestyle",
144
  dailyLeaderboard: "Today's Ranking",
145
  howToPlay: "How to Play",
146
  leaderboard: "Leaderboard",
 
192
  "Try to be creative and descriptive",
193
  "The AI will try to guess your word after each sentence"
194
  ]
195
+ },
196
+ gameModes: {
197
+ title: "Game Modes",
198
+ daily: "Daily 10: Everyone gets the same wordlist, refreshed every 24 hours",
199
+ custom: "Freestyle: Choose your own theme and play a personal game"
200
  }
201
  },
202
  models: {
203
  title: "Choose an AI Model",
204
  subtitle: "Select the AI model that will play together with you",
205
  continue: "Continue",
206
+ generating: "Generating...",
207
+ custom: "Custom Model",
208
+ searchPlaceholder: "Search for a model...",
209
+ loginRequired: "Please log in or register to use custom models"
210
+ },
211
+ auth: {
212
+ login: {
213
+ linkText: "Login",
214
+ title: "Login",
215
+ subtitle: "Sign in to your account",
216
+ email: "Email",
217
+ password: "Password",
218
+ submit: "Login",
219
+ loggingIn: "Logging in...",
220
+ noAccount: "Don't have an account?",
221
+ register: "Register"
222
+ },
223
+ loginSuccess: {
224
+ title: "Login successful",
225
+ description: "You have been successfully logged in"
226
+ },
227
+ loginError: {
228
+ title: "Login failed",
229
+ description: "An error occurred while trying to log in"
230
+ },
231
+ register: {
232
+ linkText: "Register",
233
+ title: "Register",
234
+ description: "Create a new account",
235
+ email: "Email",
236
+ password: "Password",
237
+ confirmPassword: "Confirm Password",
238
+ submit: "Register",
239
+ registering: "Registering...",
240
+ haveAccount: "Already have an account?",
241
+ login: "Login"
242
+ },
243
+ registerSuccess: {
244
+ title: "Registration successful",
245
+ description: "Your account has been created successfully"
246
+ },
247
+ registerError: {
248
+ title: "Registration failed",
249
+ description: "An error occurred while trying to register"
250
+ },
251
+ logoutSuccess: {
252
+ title: "Logged out",
253
+ description: "You have been successfully logged out"
254
+ },
255
+ form: {
256
+ email: "Email",
257
+ password: "Password",
258
+ logout: "Logout"
259
+ }
260
  }
261
  };
src/i18n/translations/es.ts CHANGED
@@ -20,10 +20,13 @@ export const es = {
20
  cancel: "Cancelar",
21
  confirm: "Confirmar",
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",
@@ -45,10 +48,11 @@ export const es = {
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",
@@ -83,7 +87,7 @@ export const es = {
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: {
@@ -133,8 +137,8 @@ export const es = {
133
  title: "Think in Sync",
134
  subtitle: "¡Forma equipo con la IA para crear una pista y deja que otra IA adivine tu palabra secreta!",
135
  startButton: "Comenzar juego",
136
- startDailyButton: "Desafío Diario",
137
- startNewButton: "Nuevo Juego",
138
  dailyLeaderboard: "Ranking diario",
139
  howToPlay: "Cómo jugar",
140
  leaderboard: "Clasificación",
@@ -186,6 +190,70 @@ export const es = {
186
  "Sé creativo y descriptivo",
187
  "La IA intentará adivinar tu palabra después de cada frase"
188
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  }
190
  }
191
  };
 
20
  cancel: "Cancelar",
21
  confirm: "Confirmar",
22
  describeWord: "Tu objetivo es describir la palabra",
23
+ nextRound: "Siguiente ronda",
24
+ nextWord: "Siguiente palabra",
25
+ playAgain: "Jugar de nuevo",
26
  saveScore: "Guardar Puntuación",
27
  playNewWords: "Jugar nuevas palabras",
28
+ skipWord: "Saltar palabra",
29
+ finishGame: "Terminar juego",
30
  review: {
31
  title: "Resumen del Juego",
32
  successfulRounds: "Rondas Exitosas",
 
48
  friendWords: "Amigo",
49
  result: "Resultado",
50
  details: "Detalles",
51
+ yourDescription: "Tu descripción",
52
+ friendDescription: "Descripción del amigo",
53
+ aiGuessed: "IA adivinó",
54
+ words: "Palabras",
55
+ avgWords: "Promedio de palabras por ronda"
56
  },
57
  invitation: {
58
  title: "Invitación al Juego",
 
87
  scoreSubmitted: "¡Puntuación enviada!",
88
  scoreSubmittedDesc: "Tu puntuación ha sido añadida a la tabla de clasificación",
89
  modes: {
90
+ daily: "Diario 10 de hoy",
91
  "all-time": "Histórico"
92
  },
93
  error: {
 
137
  title: "Think in Sync",
138
  subtitle: "¡Forma equipo con la IA para crear una pista y deja que otra IA adivine tu palabra secreta!",
139
  startButton: "Comenzar juego",
140
+ startDailyButton: "Jugar Diario 10",
141
+ startNewButton: "Jugar Freestyle",
142
  dailyLeaderboard: "Ranking diario",
143
  howToPlay: "Cómo jugar",
144
  leaderboard: "Clasificación",
 
190
  "Sé creativo y descriptivo",
191
  "La IA intentará adivinar tu palabra después de cada frase"
192
  ]
193
+ },
194
+ gameModes: {
195
+ title: "Modos de Juego",
196
+ daily: "Diario 10: La misma lista de palabras para todos, renovada cada 24 horas",
197
+ custom: "Freestyle: Elige un tema y juega tu partida personal"
198
+ }
199
+ },
200
+ models: {
201
+ title: "Elige un Modelo de IA",
202
+ subtitle: "Selecciona el modelo de IA que jugará contigo",
203
+ continue: "Continuar",
204
+ generating: "Generando...",
205
+ custom: "Modelo Personalizado",
206
+ searchPlaceholder: "Buscar un modelo...",
207
+ loginRequired: "Por favor inicia sesión o regístrate para usar modelos personalizados"
208
+ },
209
+ auth: {
210
+ login: {
211
+ linkText: "Iniciar sesión",
212
+ title: "Iniciar sesión",
213
+ subtitle: "Inicia sesión en tu cuenta",
214
+ email: "Correo electrónico",
215
+ password: "Contraseña",
216
+ submit: "Iniciar sesión",
217
+ loggingIn: "Iniciando sesión...",
218
+ noAccount: "¿No tienes una cuenta?",
219
+ register: "Registrarse"
220
+ },
221
+ loginSuccess: {
222
+ title: "Inicio de sesión exitoso",
223
+ description: "Has iniciado sesión correctamente"
224
+ },
225
+ loginError: {
226
+ title: "Error de inicio de sesión",
227
+ description: "Ocurrió un error al intentar iniciar sesión"
228
+ },
229
+ register: {
230
+ linkText: "Registrarse",
231
+ title: "Registrarse",
232
+ description: "Crea una nueva cuenta",
233
+ email: "Correo electrónico",
234
+ password: "Contraseña",
235
+ confirmPassword: "Confirmar contraseña",
236
+ submit: "Registrarse",
237
+ registering: "Registrando...",
238
+ haveAccount: "¿Ya tienes una cuenta?",
239
+ login: "Iniciar sesión"
240
+ },
241
+ registerSuccess: {
242
+ title: "Registro exitoso",
243
+ description: "Tu cuenta ha sido creada exitosamente"
244
+ },
245
+ registerError: {
246
+ title: "Error de registro",
247
+ description: "Ocurrió un error al intentar registrarse"
248
+ },
249
+ logoutSuccess: {
250
+ title: "Sesión cerrada",
251
+ description: "Has cerrado la sesión correctamente"
252
+ },
253
+ form: {
254
+ email: "Correo electrónico",
255
+ password: "Contraseña",
256
+ logout: "Cerrar sesión"
257
  }
258
  }
259
  };
src/i18n/translations/fr.ts CHANGED
@@ -20,9 +20,12 @@ export const fr = {
20
  cancel: "Annuler",
21
  confirm: "Confirmer",
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",
@@ -38,16 +41,17 @@ export const fr = {
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",
@@ -82,7 +86,7 @@ export const fr = {
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: {
@@ -128,8 +132,8 @@ export const fr = {
128
  title: "Think in Sync",
129
  subtitle: "Collaborez avec une IA pour créer un indice, puis laissez-en une autre deviner votre mot secret !",
130
  startButton: "Commencer",
131
- startDailyButton: "Défi du Jour",
132
- startNewButton: "Nouvelle Partie",
133
  dailyLeaderboard: "Classement du jour",
134
  howToPlay: "Comment Jouer",
135
  leaderboard: "Classement",
@@ -181,12 +185,70 @@ export const fr = {
181
  "Soyez créatif et descriptif",
182
  "L'IA essaiera de deviner votre mot après chaque phrase"
183
  ]
 
 
 
 
 
184
  }
185
  },
186
  models: {
187
- title: "Choisissez un Modèle IA",
188
- subtitle: "Sélectionnez le modèle IA qui jouera avec vous",
189
  continue: "Continuer",
190
- generating: "Génération en cours..."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  }
192
  };
 
20
  cancel: "Annuler",
21
  confirm: "Confirmer",
22
  describeWord: "Votre objectif est de décrire le mot",
23
+ nextRound: "Ronde suivante",
24
+ nextWord: "Mot suivant",
25
  playAgain: "Rejouer",
26
  saveScore: "Sauvegarder le Score",
27
+ skipWord: "Passer le mot",
28
+ finishGame: "Terminer la partie",
29
  review: {
30
  title: "Résumé de la Partie",
31
  successfulRounds: "Manches Réussies",
 
41
  youWin: "Vous avez gagné !",
42
  youLost: "Vous avez perdu !",
43
  friendScore: (score: number, avgWords: string) =>
44
+ `La personne qui vous a mis au défi a complété ${score} rondes avec succès avec une moyenne de ${avgWords} mots.`,
45
  word: "Mot",
46
  yourWords: "Vous",
47
  friendWords: "Ami",
48
  result: "Résultat",
49
  details: "Détails",
50
+ yourDescription: "Votre description",
51
+ friendDescription: "Description de l'ami",
52
+ aiGuessed: "IA a deviné",
53
+ words: "Mots",
54
+ avgWords: "Moyenne de mots par ronde"
55
  },
56
  invitation: {
57
  title: "Invitation au Jeu",
 
86
  scoreSubmitted: "Score soumis !",
87
  scoreSubmittedDesc: "Votre score a été ajouté au classement",
88
  modes: {
89
+ daily: "Quotidien 10 d'aujourd'hui",
90
  "all-time": "Historique"
91
  },
92
  error: {
 
132
  title: "Think in Sync",
133
  subtitle: "Collaborez avec une IA pour créer un indice, puis laissez-en une autre deviner votre mot secret !",
134
  startButton: "Commencer",
135
+ startDailyButton: "Jouer Quotidien 10",
136
+ startNewButton: "Jouer Freestyle",
137
  dailyLeaderboard: "Classement du jour",
138
  howToPlay: "Comment Jouer",
139
  leaderboard: "Classement",
 
185
  "Soyez créatif et descriptif",
186
  "L'IA essaiera de deviner votre mot après chaque phrase"
187
  ]
188
+ },
189
+ gameModes: {
190
+ title: "Modes de Jeu",
191
+ daily: "Quotidien 10 : La même liste de mots pour tous, renouvelée toutes les 24 heures",
192
+ custom: "Freestyle : Choisissez un thème et jouez votre partie personnelle"
193
  }
194
  },
195
  models: {
196
+ title: "Choisissez un Modèle d'IA",
197
+ subtitle: "Sélectionnez le modèle d'IA qui jouera avec vous",
198
  continue: "Continuer",
199
+ generating: "Génération en cours...",
200
+ custom: "Modèle Personnalisé",
201
+ searchPlaceholder: "Rechercher un modèle...",
202
+ loginRequired: "Veuillez vous connecter ou vous inscrire pour utiliser des modèles personnalisés"
203
+ },
204
+ auth: {
205
+ login: {
206
+ linkText: "Se connecter",
207
+ title: "Se connecter",
208
+ subtitle: "Connectez-vous à votre compte",
209
+ email: "Email",
210
+ password: "Mot de passe",
211
+ submit: "Se connecter",
212
+ loggingIn: "Connexion en cours...",
213
+ noAccount: "Vous n'avez pas de compte ?",
214
+ register: "S'inscrire"
215
+ },
216
+ loginSuccess: {
217
+ title: "Connexion réussie",
218
+ description: "Vous êtes connecté avec succès"
219
+ },
220
+ loginError: {
221
+ title: "Échec de la connexion",
222
+ description: "Une erreur s'est produite lors de la tentative de connexion"
223
+ },
224
+ register: {
225
+ linkText: "S'inscrire",
226
+ title: "S'inscrire",
227
+ description: "Créez un nouveau compte",
228
+ email: "Email",
229
+ password: "Mot de passe",
230
+ confirmPassword: "Confirmer le mot de passe",
231
+ submit: "S'inscrire",
232
+ registering: "Inscription en cours...",
233
+ haveAccount: "Vous avez déjà un compte ?",
234
+ login: "Se connecter"
235
+ },
236
+ registerSuccess: {
237
+ title: "Inscription réussie",
238
+ description: "Votre compte a été créé avec succès"
239
+ },
240
+ registerError: {
241
+ title: "Échec de l'inscription",
242
+ description: "Une erreur s'est produite lors de la tentative d'inscription"
243
+ },
244
+ logoutSuccess: {
245
+ title: "Déconnexion",
246
+ description: "Vous avez été déconnecté avec succès"
247
+ },
248
+ form: {
249
+ email: "Email",
250
+ password: "Mot de passe",
251
+ logout: "Se déconnecter"
252
+ }
253
  }
254
  };
src/i18n/translations/it.ts CHANGED
@@ -20,9 +20,12 @@ export const it = {
20
  cancel: "Annulla",
21
  confirm: "Conferma",
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",
@@ -38,16 +41,17 @@ export const it = {
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",
@@ -82,7 +86,7 @@ export const it = {
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: {
@@ -134,8 +138,8 @@ export const it = {
134
  title: "Think in Sync",
135
  subtitle: "Fai squadra con l'IA per creare un indizio e lascia che un'altra IA indovini la tua parola segreta!",
136
  startButton: "Inizia gioco",
137
- startDailyButton: "Sfida Giornaliera",
138
- startNewButton: "Nuova Partita",
139
  dailyLeaderboard: "Classifica di oggi",
140
  howToPlay: "Come giocare",
141
  leaderboard: "Classifica",
@@ -187,12 +191,70 @@ export const it = {
187
  "Sii creativo e descrittivo",
188
  "L'IA cercherà di indovinare la tua parola dopo ogni frase"
189
  ]
 
 
 
 
 
190
  }
191
  },
192
  models: {
193
  title: "Scegli un Modello IA",
194
- subtitle: "Seleziona il modello IA che giocherà insieme a te",
195
  continue: "Continua",
196
- generating: "Generazione in corso..."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  }
198
  };
 
20
  cancel: "Annulla",
21
  confirm: "Conferma",
22
  describeWord: "Il tuo obiettivo è descrivere la parola",
23
+ nextRound: "Round successivo",
24
+ nextWord: "Prossima parola",
25
+ playAgain: "Gioca di nuovo",
26
  saveScore: "Salva Punteggio",
27
+ skipWord: "Salta parola",
28
+ finishGame: "Termina gioco",
29
  review: {
30
  title: "Riepilogo Partita",
31
  successfulRounds: "Turni Riusciti",
 
41
  youWin: "Hai vinto!",
42
  youLost: "Hai perso!",
43
  friendScore: (score: number, avgWords: string) =>
44
+ `La persona che ti ha sfidato ha completato ${score} round con successo con una media di ${avgWords} parole.`,
45
  word: "Parola",
46
  yourWords: "Tu",
47
  friendWords: "Amico",
48
  result: "Risultato",
49
  details: "Dettagli",
50
+ yourDescription: "La tua descrizione",
51
+ friendDescription: "Descrizione dell'amico",
52
+ aiGuessed: "IA ha indovinato",
53
+ words: "Parole",
54
+ avgWords: "Media di parole per round"
55
  },
56
  invitation: {
57
  title: "Invito al Gioco",
 
86
  scoreSubmitted: "Punteggio inviato!",
87
  scoreSubmittedDesc: "Il tuo punteggio è stato aggiunto alla classifica",
88
  modes: {
89
+ daily: "Giornaliero 10 di oggi",
90
  "all-time": "Classifica Generale"
91
  },
92
  error: {
 
138
  title: "Think in Sync",
139
  subtitle: "Fai squadra con l'IA per creare un indizio e lascia che un'altra IA indovini la tua parola segreta!",
140
  startButton: "Inizia gioco",
141
+ startDailyButton: "Gioca Giornaliero 10",
142
+ startNewButton: "Gioca Freestyle",
143
  dailyLeaderboard: "Classifica di oggi",
144
  howToPlay: "Come giocare",
145
  leaderboard: "Classifica",
 
191
  "Sii creativo e descrittivo",
192
  "L'IA cercherà di indovinare la tua parola dopo ogni frase"
193
  ]
194
+ },
195
+ gameModes: {
196
+ title: "Modalità di Gioco",
197
+ daily: "Giornaliero 10: La stessa lista di parole per tutti, rinnovata ogni 24 ore",
198
+ custom: "Freestyle: Scegli un tema e gioca la tua partita personale"
199
  }
200
  },
201
  models: {
202
  title: "Scegli un Modello IA",
203
+ subtitle: "Seleziona il modello IA che giocherà con te",
204
  continue: "Continua",
205
+ generating: "Generazione...",
206
+ custom: "Modello Personalizzato",
207
+ searchPlaceholder: "Cerca un modello...",
208
+ loginRequired: "Accedi o registrati per utilizzare modelli personalizzati"
209
+ },
210
+ auth: {
211
+ login: {
212
+ linkText: "Accedi",
213
+ title: "Accedi",
214
+ subtitle: "Accedi al tuo account",
215
+ email: "Email",
216
+ password: "Password",
217
+ submit: "Accedi",
218
+ loggingIn: "Accesso in corso...",
219
+ noAccount: "Non hai un account?",
220
+ register: "Registrati"
221
+ },
222
+ loginSuccess: {
223
+ title: "Accesso riuscito",
224
+ description: "Hai effettuato l'accesso con successo"
225
+ },
226
+ loginError: {
227
+ title: "Accesso fallito",
228
+ description: "Si è verificato un errore durante l'accesso"
229
+ },
230
+ register: {
231
+ linkText: "Registrati",
232
+ title: "Registrati",
233
+ description: "Crea un nuovo account",
234
+ email: "Email",
235
+ password: "Password",
236
+ confirmPassword: "Conferma password",
237
+ submit: "Registrati",
238
+ registering: "Registrazione in corso...",
239
+ haveAccount: "Hai già un account?",
240
+ login: "Accedi"
241
+ },
242
+ registerSuccess: {
243
+ title: "Registrazione completata",
244
+ description: "Il tuo account è stato creato con successo"
245
+ },
246
+ registerError: {
247
+ title: "Registrazione fallita",
248
+ description: "Si è verificato un errore durante la registrazione"
249
+ },
250
+ logoutSuccess: {
251
+ title: "Disconnesso",
252
+ description: "Hai effettuato la disconnessione con successo"
253
+ },
254
+ form: {
255
+ email: "Email",
256
+ password: "Password",
257
+ logout: "Disconnetti"
258
+ }
259
  }
260
  };
src/i18n/translations/pt.ts CHANGED
@@ -20,10 +20,13 @@ export const pt = {
20
  cancel: "Cancelar",
21
  confirm: "Confirmar",
22
  describeWord: "Seu objetivo é descrever a palavra",
23
- nextRound: "Próxima Rodada",
24
- playAgain: "Jogar Novamente",
 
25
  saveScore: "Salvar Pontuação",
26
  playNewWords: "Jogar novas palavras",
 
 
27
  review: {
28
  title: "Resumo do Jogo",
29
  successfulRounds: "Rodadas Bem-sucedidas",
@@ -45,10 +48,11 @@ export const pt = {
45
  friendWords: "Amigo",
46
  result: "Resultado",
47
  details: "Detalhes",
48
- yourDescription: "Sua Descrição",
49
- friendDescription: "Descrição do Amigo",
50
- aiGuessed: "A IA adivinhou",
51
- words: "Palavras"
 
52
  },
53
  invitation: {
54
  title: "Convite para o Jogo",
@@ -83,7 +87,7 @@ export const pt = {
83
  scoreSubmitted: "Pontuação enviada!",
84
  scoreSubmittedDesc: "Sua pontuação foi adicionada ao placar",
85
  modes: {
86
- daily: "Desafio Diário",
87
  "all-time": "Histórico"
88
  },
89
  error: {
@@ -133,8 +137,8 @@ export const pt = {
133
  title: "Think in Sync",
134
  subtitle: "Forme uma equipe com a IA para criar uma pista e deixe outra IA adivinhar sua palavra secreta!",
135
  startButton: "Iniciar jogo",
136
- startDailyButton: "Desafio Diário",
137
- startNewButton: "Novo Jogo",
138
  dailyLeaderboard: "Placar diário",
139
  howToPlay: "Como jogar",
140
  leaderboard: "Placar",
@@ -186,12 +190,70 @@ export const pt = {
186
  "Seja criativo e descritivo",
187
  "A IA tentará adivinhar sua palavra após cada frase"
188
  ]
 
 
 
 
 
189
  }
190
  },
191
  models: {
192
  title: "Escolha um Modelo de IA",
193
- subtitle: "Selecione o modelo de IA que jogará junto com você",
194
  continue: "Continuar",
195
- generating: "Gerando..."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  }
197
  };
 
20
  cancel: "Cancelar",
21
  confirm: "Confirmar",
22
  describeWord: "Seu objetivo é descrever a palavra",
23
+ nextRound: "Próxima rodada",
24
+ nextWord: "Próxima palavra",
25
+ playAgain: "Jogar novamente",
26
  saveScore: "Salvar Pontuação",
27
  playNewWords: "Jogar novas palavras",
28
+ skipWord: "Pular palavra",
29
+ finishGame: "Terminar jogo",
30
  review: {
31
  title: "Resumo do Jogo",
32
  successfulRounds: "Rodadas Bem-sucedidas",
 
48
  friendWords: "Amigo",
49
  result: "Resultado",
50
  details: "Detalhes",
51
+ yourDescription: "Sua descrição",
52
+ friendDescription: "Descrição do amigo",
53
+ aiGuessed: "IA adivinhou",
54
+ words: "Palavras",
55
+ avgWords: "Média de palavras por rodada"
56
  },
57
  invitation: {
58
  title: "Convite para o Jogo",
 
87
  scoreSubmitted: "Pontuação enviada!",
88
  scoreSubmittedDesc: "Sua pontuação foi adicionada ao placar",
89
  modes: {
90
+ daily: "Diário 10 de hoje",
91
  "all-time": "Histórico"
92
  },
93
  error: {
 
137
  title: "Think in Sync",
138
  subtitle: "Forme uma equipe com a IA para criar uma pista e deixe outra IA adivinhar sua palavra secreta!",
139
  startButton: "Iniciar jogo",
140
+ startDailyButton: "Jogar Diário 10",
141
+ startNewButton: "Jogar Freestyle",
142
  dailyLeaderboard: "Placar diário",
143
  howToPlay: "Como jogar",
144
  leaderboard: "Placar",
 
190
  "Seja criativo e descritivo",
191
  "A IA tentará adivinhar sua palavra após cada frase"
192
  ]
193
+ },
194
+ gameModes: {
195
+ title: "Modos de Jogo",
196
+ daily: "Diário 10: A mesma lista de palavras para todos, atualizada a cada 24 horas",
197
+ custom: "Freestyle: Escolha um tema e jogue seu jogo pessoal"
198
  }
199
  },
200
  models: {
201
  title: "Escolha um Modelo de IA",
202
+ subtitle: "Selecione o modelo de IA que jogará com você",
203
  continue: "Continuar",
204
+ generating: "Gerando...",
205
+ custom: "Modelo Personalizado",
206
+ searchPlaceholder: "Pesquisar um modelo...",
207
+ loginRequired: "Por favor, faça login ou registre-se para usar modelos personalizados"
208
+ },
209
+ auth: {
210
+ login: {
211
+ linkText: "Entrar",
212
+ title: "Entrar",
213
+ subtitle: "Entre na sua conta",
214
+ email: "Email",
215
+ password: "Senha",
216
+ submit: "Entrar",
217
+ loggingIn: "Entrando...",
218
+ noAccount: "Não tem uma conta?",
219
+ register: "Registrar"
220
+ },
221
+ loginSuccess: {
222
+ title: "Login bem-sucedido",
223
+ description: "Você entrou com sucesso"
224
+ },
225
+ loginError: {
226
+ title: "Falha no login",
227
+ description: "Ocorreu um erro ao tentar fazer login"
228
+ },
229
+ register: {
230
+ linkText: "Registrar",
231
+ title: "Registrar",
232
+ description: "Crie uma nova conta",
233
+ email: "Email",
234
+ password: "Senha",
235
+ confirmPassword: "Confirmar senha",
236
+ submit: "Registrar",
237
+ registering: "Registrando...",
238
+ haveAccount: "Já tem uma conta?",
239
+ login: "Entrar"
240
+ },
241
+ registerSuccess: {
242
+ title: "Registro bem-sucedido",
243
+ description: "Sua conta foi criada com sucesso"
244
+ },
245
+ registerError: {
246
+ title: "Falha no registro",
247
+ description: "Ocorreu um erro ao tentar se registrar"
248
+ },
249
+ logoutSuccess: {
250
+ title: "Desconectado",
251
+ description: "Você foi desconectado com sucesso"
252
+ },
253
+ form: {
254
+ email: "Email",
255
+ password: "Senha",
256
+ logout: "Sair"
257
+ }
258
  }
259
  };
src/lib/words-standard.ts CHANGED
@@ -76,7 +76,6 @@ export const englishWords = [
76
  "NEST",
77
  "ROCK",
78
  "LEAF",
79
- "BRUSH",
80
  "TOOTH",
81
  "HAND",
82
  "FOOT",
@@ -129,7 +128,7 @@ export const englishWords = [
129
  "DRYER",
130
  "FURNACE",
131
  "FAN",
132
- "PAINTBRUSH",
133
  "BUCKET",
134
  "SPONGE",
135
  "SOAP",
 
76
  "NEST",
77
  "ROCK",
78
  "LEAF",
 
79
  "TOOTH",
80
  "HAND",
81
  "FOOT",
 
128
  "DRYER",
129
  "FURNACE",
130
  "FAN",
131
+ "BRUSH",
132
  "BUCKET",
133
  "SPONGE",
134
  "SOAP",
src/pages/auth/Login.tsx ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react";
2
+ import { useNavigate, Link } from "react-router-dom";
3
+ import { useAuth } from "@/contexts/AuthContext";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
7
+ import { z } from "zod";
8
+ import { zodResolver } from "@hookform/resolvers/zod";
9
+ import { useForm } from "react-hook-form";
10
+ import { useToast } from "@/hooks/use-toast";
11
+ import { useTranslation } from "@/hooks/useTranslation";
12
+ import { Loader2 } from "lucide-react";
13
+
14
+ declare global {
15
+ interface Window {
16
+ handleSignInWithGoogle: (response: { credential: string }) => Promise<void>;
17
+ google?: {
18
+ accounts: {
19
+ id: {
20
+ initialize: (config: {
21
+ client_id: string;
22
+ callback: (response: { credential: string }) => Promise<void>;
23
+ }) => void;
24
+ prompt: () => void;
25
+ };
26
+ };
27
+ };
28
+ }
29
+ }
30
+
31
+ const formSchema = z.object({
32
+ email: z.string().email(),
33
+ password: z.string().min(6),
34
+ });
35
+
36
+ export const Login = () => {
37
+ const { signIn, signInWithGoogle, signInWithGitHub, user } = useAuth();
38
+ const navigate = useNavigate();
39
+ const [isLoading, setIsLoading] = useState(false);
40
+ const [isOAuthLoading, setIsOAuthLoading] = useState(false);
41
+ const { toast } = useToast();
42
+ const t = useTranslation();
43
+
44
+ const form = useForm<z.infer<typeof formSchema>>({
45
+ resolver: zodResolver(formSchema),
46
+ defaultValues: {
47
+ email: "",
48
+ password: "",
49
+ },
50
+ });
51
+
52
+ useEffect(() => {
53
+ // Add Google Sign-In script
54
+ const script = document.createElement('script');
55
+ script.src = 'https://accounts.google.com/gsi/client';
56
+ script.async = true;
57
+ script.onload = () => {
58
+ // Initialize Google Identity Services
59
+ window.google?.accounts.id.initialize({
60
+ client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
61
+ callback: window.handleSignInWithGoogle,
62
+ });
63
+ };
64
+ document.body.appendChild(script);
65
+
66
+ return () => {
67
+ document.body.removeChild(script);
68
+ };
69
+ }, []);
70
+
71
+ useEffect(() => {
72
+ if (user && isOAuthLoading) {
73
+ setIsOAuthLoading(false);
74
+ toast({
75
+ title: t.auth.loginSuccess.title,
76
+ description: t.auth.loginSuccess.description,
77
+ });
78
+ navigate("/");
79
+ }
80
+ }, [user, isOAuthLoading, navigate, toast, t.auth.loginSuccess]);
81
+
82
+ useEffect(() => {
83
+ // Initialize Google One Tap
84
+ window.handleSignInWithGoogle = async (response) => {
85
+ try {
86
+ if (!response.credential) {
87
+ throw new Error('No credential received from Google');
88
+ }
89
+
90
+ setIsOAuthLoading(true);
91
+ const { error } = await signInWithGoogle();
92
+
93
+ if (error) {
94
+ setIsOAuthLoading(false);
95
+ toast({
96
+ variant: "destructive",
97
+ title: t.auth.loginError.title,
98
+ description: error.message,
99
+ });
100
+ }
101
+ } catch (error) {
102
+ setIsOAuthLoading(false);
103
+ toast({
104
+ variant: "destructive",
105
+ title: t.auth.loginError.title,
106
+ description: t.auth.loginError.description,
107
+ });
108
+ }
109
+ };
110
+ }, [signInWithGoogle, toast, t.auth.loginError]);
111
+
112
+ const handleGitHubLogin = async () => {
113
+ try {
114
+ setIsOAuthLoading(true);
115
+ const { error } = await signInWithGitHub();
116
+
117
+ if (error) {
118
+ setIsOAuthLoading(false);
119
+ toast({
120
+ variant: "destructive",
121
+ title: t.auth.loginError.title,
122
+ description: error.message,
123
+ });
124
+ }
125
+ } catch (error) {
126
+ setIsOAuthLoading(false);
127
+ toast({
128
+ variant: "destructive",
129
+ title: t.auth.loginError.title,
130
+ description: t.auth.loginError.description,
131
+ });
132
+ }
133
+ };
134
+
135
+ const onSubmit = async (values: z.infer<typeof formSchema>) => {
136
+ try {
137
+ setIsLoading(true);
138
+ const { error, success } = await signIn(values.email, values.password);
139
+
140
+ if (success) {
141
+ toast({
142
+ title: t.auth.loginSuccess.title,
143
+ description: t.auth.loginSuccess.description,
144
+ });
145
+ navigate("/");
146
+ } else if (error) {
147
+ toast({
148
+ variant: "destructive",
149
+ title: t.auth.loginError.title,
150
+ description: error.message,
151
+ });
152
+ }
153
+ } catch (error) {
154
+ toast({
155
+ variant: "destructive",
156
+ title: t.auth.loginError.title,
157
+ description: t.auth.loginError.description,
158
+ });
159
+ } finally {
160
+ setIsLoading(false);
161
+ }
162
+ };
163
+
164
+ return (
165
+ <div className="flex min-h-screen items-center justify-center px-4 py-12">
166
+ {isOAuthLoading ? (
167
+ <div className="flex flex-col items-center gap-4">
168
+ <Loader2 className="h-8 w-8 animate-spin" />
169
+ <p className="text-sm text-muted-foreground">Authenticating...</p>
170
+ </div>
171
+ ) : (
172
+ <div className="w-full max-w-md space-y-8">
173
+ <div className="text-center">
174
+ <h1 className="text-2xl font-bold">{t.auth.login.title}</h1>
175
+ <p className="mt-2 text-sm text-gray-600">{t.auth.login.subtitle}</p>
176
+ </div>
177
+
178
+ <div className="flex flex-col gap-4">
179
+ <Button
180
+ type="button"
181
+ variant="outline"
182
+ className="w-full flex items-center justify-center gap-2"
183
+ onClick={() => {
184
+ window.google?.accounts.id.prompt();
185
+ }}
186
+ disabled={isLoading}
187
+ >
188
+ <img src="https://www.google.com/favicon.ico" alt="Google" className="w-4 h-4" />
189
+ Continue with Google
190
+ </Button>
191
+
192
+ <Button
193
+ type="button"
194
+ variant="outline"
195
+ className="w-full flex items-center justify-center gap-2"
196
+ onClick={handleGitHubLogin}
197
+ disabled={isLoading}
198
+ >
199
+ <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
200
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
201
+ </svg>
202
+ Continue with GitHub
203
+ </Button>
204
+ </div>
205
+
206
+ <div className="relative">
207
+ <div className="absolute inset-0 flex items-center">
208
+ <span className="w-full border-t" />
209
+ </div>
210
+ <div className="relative flex justify-center text-xs uppercase">
211
+ <span className="bg-background px-2 text-muted-foreground">
212
+ Or continue with email
213
+ </span>
214
+ </div>
215
+ </div>
216
+
217
+ <Form {...form}>
218
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
219
+ <FormField
220
+ control={form.control}
221
+ name="email"
222
+ render={({ field }) => (
223
+ <FormItem>
224
+ <FormLabel>{t.auth.form.email}</FormLabel>
225
+ <FormControl>
226
+ <Input
227
+ placeholder="[email protected]"
228
+ {...field}
229
+ disabled={isLoading}
230
+ />
231
+ </FormControl>
232
+ <FormMessage />
233
+ </FormItem>
234
+ )}
235
+ />
236
+
237
+ <FormField
238
+ control={form.control}
239
+ name="password"
240
+ render={({ field }) => (
241
+ <FormItem>
242
+ <FormLabel>{t.auth.form.password}</FormLabel>
243
+ <FormControl>
244
+ <Input
245
+ type="password"
246
+ placeholder="••••••••"
247
+ {...field}
248
+ disabled={isLoading}
249
+ />
250
+ </FormControl>
251
+ <FormMessage />
252
+ </FormItem>
253
+ )}
254
+ />
255
+
256
+ <Button type="submit" className="w-full" disabled={isLoading}>
257
+ {isLoading ? t.auth.login.loggingIn : t.auth.login.submit}
258
+ </Button>
259
+ </form>
260
+ </Form>
261
+
262
+ <div className="mt-4 text-center text-sm">
263
+ <p>
264
+ {t.auth.login.noAccount}{" "}
265
+ <Link to="/auth/register" className="font-medium text-primary hover:underline">
266
+ {t.auth.register.linkText}
267
+ </Link>
268
+ </p>
269
+ </div>
270
+ </div>
271
+ )}
272
+ </div>
273
+ );
274
+ };
275
+
276
+ export default Login;
src/pages/auth/Register.tsx ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react";
2
+ import { useNavigate, Link } from "react-router-dom";
3
+ import { useAuth } from "@/contexts/AuthContext";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
7
+ import { z } from "zod";
8
+ import { zodResolver } from "@hookform/resolvers/zod";
9
+ import { useForm } from "react-hook-form";
10
+ import { useToast } from "@/hooks/use-toast";
11
+ import { useTranslation } from "@/hooks/useTranslation";
12
+ import { Loader2 } from "lucide-react";
13
+
14
+ declare global {
15
+ interface Window {
16
+ handleSignInWithGoogle: (response: { credential: string }) => Promise<void>;
17
+ google?: {
18
+ accounts: {
19
+ id: {
20
+ initialize: (config: {
21
+ client_id: string;
22
+ callback: (response: { credential: string }) => Promise<void>;
23
+ }) => void;
24
+ prompt: () => void;
25
+ };
26
+ };
27
+ };
28
+ }
29
+ }
30
+
31
+ const formSchema = z.object({
32
+ email: z.string().email(),
33
+ password: z.string().min(6),
34
+ confirmPassword: z.string().min(6),
35
+ }).refine((data) => data.password === data.confirmPassword, {
36
+ message: "Passwords don't match",
37
+ path: ["confirmPassword"],
38
+ });
39
+
40
+ export const Register = () => {
41
+ const { signUp, signInWithGoogle, signInWithGitHub, user } = useAuth();
42
+ const navigate = useNavigate();
43
+ const [isLoading, setIsLoading] = useState(false);
44
+ const [isOAuthLoading, setIsOAuthLoading] = useState(false);
45
+ const { toast } = useToast();
46
+ const t = useTranslation();
47
+
48
+ const form = useForm<z.infer<typeof formSchema>>({
49
+ resolver: zodResolver(formSchema),
50
+ defaultValues: {
51
+ email: "",
52
+ password: "",
53
+ confirmPassword: "",
54
+ },
55
+ });
56
+
57
+ useEffect(() => {
58
+ // Add Google Sign-In script
59
+ const script = document.createElement('script');
60
+ script.src = 'https://accounts.google.com/gsi/client';
61
+ script.async = true;
62
+ document.body.appendChild(script);
63
+
64
+ return () => {
65
+ document.body.removeChild(script);
66
+ };
67
+ }, []);
68
+
69
+ useEffect(() => {
70
+ if (user && isOAuthLoading) {
71
+ setIsOAuthLoading(false);
72
+ toast({
73
+ title: t.auth.loginSuccess.title,
74
+ description: t.auth.loginSuccess.description,
75
+ });
76
+ navigate("/");
77
+ }
78
+ }, [user, isOAuthLoading, navigate, toast, t.auth.loginSuccess]);
79
+
80
+ useEffect(() => {
81
+ // Initialize Google One Tap
82
+ window.handleSignInWithGoogle = async (response) => {
83
+ try {
84
+ if (!response.credential) {
85
+ throw new Error('No credential received from Google');
86
+ }
87
+
88
+ setIsOAuthLoading(true);
89
+ const { error } = await signInWithGoogle();
90
+
91
+ if (error) {
92
+ setIsOAuthLoading(false);
93
+ toast({
94
+ variant: "destructive",
95
+ title: t.auth.loginError.title,
96
+ description: error.message,
97
+ });
98
+ }
99
+ } catch (error) {
100
+ setIsOAuthLoading(false);
101
+ toast({
102
+ variant: "destructive",
103
+ title: t.auth.loginError.title,
104
+ description: t.auth.loginError.description,
105
+ });
106
+ }
107
+ };
108
+ }, [signInWithGoogle, toast, t.auth.loginError]);
109
+
110
+ const handleGitHubLogin = async () => {
111
+ try {
112
+ setIsOAuthLoading(true);
113
+ const { error } = await signInWithGitHub();
114
+
115
+ if (error) {
116
+ setIsOAuthLoading(false);
117
+ toast({
118
+ variant: "destructive",
119
+ title: t.auth.loginError.title,
120
+ description: error.message,
121
+ });
122
+ }
123
+ } catch (error) {
124
+ setIsOAuthLoading(false);
125
+ toast({
126
+ variant: "destructive",
127
+ title: t.auth.loginError.title,
128
+ description: t.auth.loginError.description,
129
+ });
130
+ }
131
+ };
132
+
133
+ const onSubmit = async (values: z.infer<typeof formSchema>) => {
134
+ try {
135
+ setIsLoading(true);
136
+ const { error, success } = await signUp(values.email, values.password);
137
+
138
+ if (success) {
139
+ toast({
140
+ title: t.auth.registerSuccess.title,
141
+ description: t.auth.registerSuccess.description,
142
+ });
143
+ navigate("/auth/login");
144
+ } else if (error) {
145
+ toast({
146
+ variant: "destructive",
147
+ title: t.auth.registerError.title,
148
+ description: error.message,
149
+ });
150
+ }
151
+ } catch (error) {
152
+ toast({
153
+ variant: "destructive",
154
+ title: t.auth.registerError.title,
155
+ description: t.auth.registerError.description,
156
+ });
157
+ } finally {
158
+ setIsLoading(false);
159
+ }
160
+ };
161
+
162
+ return (
163
+ <div className="flex min-h-screen items-center justify-center px-4 py-12">
164
+ {isOAuthLoading ? (
165
+ <div className="flex flex-col items-center gap-4">
166
+ <Loader2 className="h-8 w-8 animate-spin" />
167
+ <p className="text-sm text-muted-foreground">Authenticating...</p>
168
+ </div>
169
+ ) : (
170
+ <div className="w-full max-w-md space-y-8">
171
+ <div className="text-center">
172
+ <h1 className="text-2xl font-bold">{t.auth.register.title}</h1>
173
+ <p className="mt-2 text-sm text-gray-600">{t.auth.register.description}</p>
174
+ </div>
175
+
176
+ <div className="flex flex-col gap-4">
177
+ <div
178
+ id="g_id_onload"
179
+ data-client_id={import.meta.env.VITE_GOOGLE_CLIENT_ID}
180
+ data-context="signin"
181
+ data-ux_mode="popup"
182
+ data-callback="handleSignInWithGoogle"
183
+ data-itp_support="true"
184
+ />
185
+
186
+ <Button
187
+ type="button"
188
+ variant="outline"
189
+ className="w-full flex items-center justify-center gap-2"
190
+ onClick={() => {
191
+ window.google?.accounts.id.prompt();
192
+ }}
193
+ disabled={isLoading}
194
+ >
195
+ <img src="https://www.google.com/favicon.ico" alt="Google" className="w-4 h-4" />
196
+ Continue with Google
197
+ </Button>
198
+
199
+ <Button
200
+ type="button"
201
+ variant="outline"
202
+ className="w-full flex items-center justify-center gap-2"
203
+ onClick={handleGitHubLogin}
204
+ disabled={isLoading}
205
+ >
206
+ <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
207
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
208
+ </svg>
209
+ Continue with GitHub
210
+ </Button>
211
+ </div>
212
+
213
+ <div className="relative">
214
+ <div className="absolute inset-0 flex items-center">
215
+ <span className="w-full border-t" />
216
+ </div>
217
+ <div className="relative flex justify-center text-xs uppercase">
218
+ <span className="bg-background px-2 text-muted-foreground">
219
+ Or continue with email
220
+ </span>
221
+ </div>
222
+ </div>
223
+
224
+ <Form {...form}>
225
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
226
+ <FormField
227
+ control={form.control}
228
+ name="email"
229
+ render={({ field }) => (
230
+ <FormItem>
231
+ <FormLabel>{t.auth.register.email}</FormLabel>
232
+ <FormControl>
233
+ <Input
234
+ placeholder="[email protected]"
235
+ {...field}
236
+ disabled={isLoading}
237
+ />
238
+ </FormControl>
239
+ <FormMessage />
240
+ </FormItem>
241
+ )}
242
+ />
243
+
244
+ <FormField
245
+ control={form.control}
246
+ name="password"
247
+ render={({ field }) => (
248
+ <FormItem>
249
+ <FormLabel>{t.auth.register.password}</FormLabel>
250
+ <FormControl>
251
+ <Input
252
+ type="password"
253
+ placeholder="••••••••"
254
+ {...field}
255
+ disabled={isLoading}
256
+ />
257
+ </FormControl>
258
+ <FormMessage />
259
+ </FormItem>
260
+ )}
261
+ />
262
+
263
+ <FormField
264
+ control={form.control}
265
+ name="confirmPassword"
266
+ render={({ field }) => (
267
+ <FormItem>
268
+ <FormLabel>{t.auth.register.confirmPassword}</FormLabel>
269
+ <FormControl>
270
+ <Input
271
+ type="password"
272
+ placeholder="••••••••"
273
+ {...field}
274
+ disabled={isLoading}
275
+ />
276
+ </FormControl>
277
+ <FormMessage />
278
+ </FormItem>
279
+ )}
280
+ />
281
+
282
+ <Button type="submit" className="w-full" disabled={isLoading}>
283
+ {isLoading ? t.auth.register.registering : t.auth.register.submit}
284
+ </Button>
285
+ </form>
286
+ </Form>
287
+
288
+ <div className="mt-4 text-center text-sm">
289
+ <p>
290
+ {t.auth.register.haveAccount}{" "}
291
+ <Link to="/auth/login" className="font-medium text-primary hover:underline">
292
+ {t.auth.register.login}
293
+ </Link>
294
+ </p>
295
+ </div>
296
+ </div>
297
+ )}
298
+ </div>
299
+ );
300
+ };
301
+
302
+ export default Register;
src/services/gameService.ts CHANGED
@@ -60,10 +60,14 @@ export const createGame = async (theme: string, language: Language = 'en'): Prom
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();
 
60
  export const createSession = async (gameId: string): Promise<string> => {
61
  console.log('Creating new session for game:', gameId);
62
 
63
+ // Get the current user
64
+ const { data: { user } } = await supabase.auth.getUser();
65
+
66
  const { data: session, error } = await supabase
67
  .from('sessions')
68
  .insert({
69
+ game_id: gameId,
70
+ user_id: user?.id
71
  })
72
  .select()
73
  .single();
supabase/functions/generate-daily-challenge/index.ts CHANGED
@@ -152,7 +152,6 @@ const wordTranslations: Record<string, Record<string, string>> = {
152
  "DRYER": { de: "TROCKNER", fr: "SÈCHE-LINGE", it: "ASCUGATRICE", es: "SECADORA", pt: "SECADORA" },
153
  "FURNACE": { de: "OFEN", fr: "FOURNAISE", it: "FORNACE", es: "HORNO", pt: "FORNALHA" },
154
  "FAN": { de: "VENTILATOR", fr: "VENTILATEUR", it: "VENTILATORE", es: "VENTILADOR", pt: "VENTILADOR" },
155
- "PAINTBRUSH": { de: "PINSEL", fr: "PINCEAU", it: "PENNELLO", es: "Pincel", pt: "Pincel" },
156
  "BUCKET": { de: "EIMER", fr: "SEAU", it: "SECCHIO", es: "CUBO", pt: "BALDE" },
157
  "SPONGE": { de: "SCHWAMM", fr: "ÉPONGE", it: "SPUGNA", es: "ESPONJA", pt: "ESPONJA" },
158
  "SOAP": { de: "SEIFE", fr: "SAVON", it: "SAPONE", es: "JABÓN", pt: "SABÃO" },
 
152
  "DRYER": { de: "TROCKNER", fr: "SÈCHE-LINGE", it: "ASCUGATRICE", es: "SECADORA", pt: "SECADORA" },
153
  "FURNACE": { de: "OFEN", fr: "FOURNAISE", it: "FORNACE", es: "HORNO", pt: "FORNALHA" },
154
  "FAN": { de: "VENTILATOR", fr: "VENTILATEUR", it: "VENTILATORE", es: "VENTILADOR", pt: "VENTILADOR" },
 
155
  "BUCKET": { de: "EIMER", fr: "SEAU", it: "SECCHIO", es: "CUBO", pt: "BALDE" },
156
  "SPONGE": { de: "SCHWAMM", fr: "ÉPONGE", it: "SPUGNA", es: "ESPONJA", pt: "ESPONJA" },
157
  "SOAP": { de: "SEIFE", fr: "SAVON", it: "SAPONE", es: "JABÓN", pt: "SABÃO" },