Felix Zieger
commited on
Commit
·
018dc4e
1
Parent(s):
01ff877
update
Browse files- Dockerfile +11 -9
- README.md +3 -1
- bun.lockb +0 -0
- package-lock.json +0 -0
- pnpm-lock.yaml +0 -0
- src/App.tsx +24 -11
- src/components/GameContainer.tsx +38 -12
- src/components/auth/ProtectedRoute.tsx +22 -0
- src/components/auth/UserMenu.tsx +79 -0
- src/components/game/GameReview.tsx +62 -16
- src/components/game/GuessDisplay.tsx +17 -7
- src/components/game/ModelSelector.tsx +74 -6
- src/components/game/SentenceBuilder.tsx +19 -2
- src/components/game/WelcomeScreen.tsx +10 -6
- src/components/game/WordDisplay.tsx +0 -19
- src/components/game/guess-display/ActionButtons.tsx +3 -5
- src/components/game/sentence-builder/RoundHeader.tsx +45 -20
- src/components/game/sentence-builder/WordDisplay.tsx +0 -16
- src/components/game/welcome/CreditsDialog.tsx +13 -2
- src/components/game/welcome/HowToPlayDialog.tsx +7 -0
- src/contexts/AuthContext.tsx +134 -0
- src/contexts/LanguageContext.tsx +0 -3
- src/env.d.ts +10 -0
- src/i18n/translations/de.ts +67 -5
- src/i18n/translations/en.ts +69 -4
- src/i18n/translations/es.ts +77 -9
- src/i18n/translations/fr.ts +74 -12
- src/i18n/translations/it.ts +74 -12
- src/i18n/translations/pt.ts +73 -11
- src/lib/words-standard.ts +1 -2
- src/pages/auth/Login.tsx +276 -0
- src/pages/auth/Register.tsx +302 -0
- src/services/gameService.ts +5 -1
- supabase/functions/generate-daily-challenge/index.ts +0 -1
Dockerfile
CHANGED
@@ -1,29 +1,31 @@
|
|
1 |
# Use the official Node.js image as the base image
|
2 |
-
FROM node:
|
3 |
|
4 |
# Set the working directory
|
5 |
WORKDIR /app
|
6 |
|
7 |
-
#
|
8 |
-
|
9 |
|
10 |
# Install dependencies
|
11 |
-
|
|
|
|
|
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
|
29 |
-
CMD ["
|
|
|
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 |
-
|
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 |
-
<
|
14 |
-
<
|
15 |
-
<
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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={
|
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 |
-
<
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
|
182 |
<div className="space-y-4">
|
183 |
-
<div className="
|
184 |
-
<
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
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 |
-
|
52 |
-
onNextRound();
|
53 |
-
} else {
|
54 |
-
onGameReview();
|
55 |
-
}
|
56 |
}
|
57 |
};
|
58 |
|
59 |
window.addEventListener('keydown', handleKeyPress);
|
60 |
return () => window.removeEventListener('keydown', handleKeyPress);
|
61 |
-
}, [
|
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
|
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 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
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 |
-
|
|
|
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 |
-
{
|
25 |
-
|
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 |
-
|
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) {
|
47 |
-
|
48 |
-
onBack?.();
|
49 |
}
|
50 |
};
|
51 |
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
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 |
-
<
|
67 |
</Button>
|
68 |
|
69 |
-
|
70 |
-
|
71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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-
|
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: "
|
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: "
|
137 |
-
startNewButton: "
|
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
|
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
|
136 |
-
startNewButton: "
|
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
|
24 |
-
|
|
|
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
|
49 |
-
friendDescription: "Descripción del
|
50 |
-
aiGuessed: "
|
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: "
|
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: "
|
137 |
-
startNewButton: "
|
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: "
|
|
|
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
|
42 |
word: "Mot",
|
43 |
yourWords: "Vous",
|
44 |
friendWords: "Ami",
|
45 |
result: "Résultat",
|
46 |
details: "Détails",
|
47 |
-
yourDescription: "Votre
|
48 |
-
friendDescription: "Description de l'
|
49 |
-
aiGuessed: "
|
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: "
|
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: "
|
132 |
-
startNewButton: "
|
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: "
|
24 |
-
|
|
|
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}
|
42 |
word: "Parola",
|
43 |
yourWords: "Tu",
|
44 |
friendWords: "Amico",
|
45 |
result: "Risultato",
|
46 |
details: "Dettagli",
|
47 |
-
yourDescription: "La
|
48 |
-
friendDescription: "Descrizione dell'
|
49 |
-
aiGuessed: "
|
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: "
|
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: "
|
138 |
-
startNewButton: "
|
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à
|
195 |
continue: "Continua",
|
196 |
-
generating: "Generazione
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
24 |
-
|
|
|
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
|
49 |
-
friendDescription: "Descrição do
|
50 |
-
aiGuessed: "
|
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: "
|
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: "
|
137 |
-
startNewButton: "
|
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á
|
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 |
-
"
|
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" },
|