Felix Zieger
commited on
Commit
·
e9a2815
1
Parent(s):
47fea40
input validation
Browse files- README.md +1 -1
- src/components/game/SentenceBuilder.tsx +39 -167
- src/components/game/sentence-builder/InputForm.tsx +92 -0
- src/components/game/sentence-builder/RoundHeader.tsx +50 -0
- src/components/game/sentence-builder/SentenceDisplay.tsx +21 -0
- src/components/game/sentence-builder/WordDisplay.tsx +39 -0
- src/i18n/translations/de.ts +1 -1
- src/i18n/translations/en.ts +1 -1
- src/i18n/translations/es.ts +1 -1
- src/i18n/translations/fr.ts +1 -1
- src/i18n/translations/it.ts +1 -1
README.md
CHANGED
|
@@ -5,7 +5,7 @@ colorFrom: blue
|
|
| 5 |
colorTo: pink
|
| 6 |
sdk: docker
|
| 7 |
app_port: 8080
|
| 8 |
-
pinned:
|
| 9 |
---
|
| 10 |
# Think in Sync
|
| 11 |
|
|
|
|
| 5 |
colorTo: pink
|
| 6 |
sdk: docker
|
| 7 |
app_port: 8080
|
| 8 |
+
pinned: true
|
| 9 |
---
|
| 10 |
# Think in Sync
|
| 11 |
|
src/components/game/SentenceBuilder.tsx
CHANGED
|
@@ -1,10 +1,5 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import { Input } from "@/components/ui/input";
|
| 3 |
import { motion } from "framer-motion";
|
| 4 |
-
import { KeyboardEvent, useRef, useEffect, useState } from "react";
|
| 5 |
-
import { useToast } from "@/hooks/use-toast";
|
| 6 |
-
import { useTranslation } from "@/hooks/useTranslation";
|
| 7 |
-
import { House } from "lucide-react";
|
| 8 |
import {
|
| 9 |
AlertDialog,
|
| 10 |
AlertDialogAction,
|
|
@@ -15,6 +10,11 @@ import {
|
|
| 15 |
AlertDialogHeader,
|
| 16 |
AlertDialogTitle,
|
| 17 |
} from "@/components/ui/alert-dialog";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
interface SentenceBuilderProps {
|
| 20 |
currentWord: string;
|
|
@@ -39,92 +39,25 @@ export const SentenceBuilder = ({
|
|
| 39 |
onMakeGuess,
|
| 40 |
onBack,
|
| 41 |
}: SentenceBuilderProps) => {
|
| 42 |
-
const inputRef = useRef<HTMLInputElement>(null);
|
| 43 |
-
const [imageLoaded, setImageLoaded] = useState(false);
|
| 44 |
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
| 45 |
const [hasMultipleWords, setHasMultipleWords] = useState(false);
|
| 46 |
-
const
|
| 47 |
-
const { toast } = useToast();
|
| 48 |
const t = useTranslation();
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
useEffect(() => {
|
| 58 |
-
setTimeout(() => {
|
| 59 |
-
inputRef.current?.focus();
|
| 60 |
-
}, 100);
|
| 61 |
-
}, []);
|
| 62 |
-
|
| 63 |
-
useEffect(() => {
|
| 64 |
-
if (!isAiThinking && sentence.length > 0 && sentence.length % 2 === 0) {
|
| 65 |
-
setTimeout(() => {
|
| 66 |
-
inputRef.current?.focus();
|
| 67 |
-
}, 100);
|
| 68 |
-
}
|
| 69 |
-
}, [isAiThinking, sentence.length]);
|
| 70 |
-
|
| 71 |
-
useEffect(() => {
|
| 72 |
-
// Check if input contains multiple words
|
| 73 |
-
setHasMultipleWords(playerInput.trim().split(/\s+/).length > 1);
|
| 74 |
-
}, [playerInput]);
|
| 75 |
-
|
| 76 |
-
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
| 77 |
-
if (e.shiftKey && e.key === 'Enter') {
|
| 78 |
-
e.preventDefault();
|
| 79 |
-
// Only trigger if buttons are not disabled and either we have a sentence or valid input
|
| 80 |
-
if (!hasMultipleWords && !isAiThinking && (sentence.length > 0 || playerInput.trim())) {
|
| 81 |
-
onMakeGuess();
|
| 82 |
-
}
|
| 83 |
-
}
|
| 84 |
};
|
| 85 |
|
| 86 |
-
const
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
const target = currentWord.toLowerCase();
|
| 90 |
-
|
| 91 |
-
if (hasMultipleWords) {
|
| 92 |
-
toast({
|
| 93 |
-
title: t.game.invalidWord,
|
| 94 |
-
description: t.game.singleWordOnly,
|
| 95 |
-
variant: "destructive",
|
| 96 |
-
});
|
| 97 |
-
return;
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
if (!/^[\p{L}]+$/u.test(input)) {
|
| 101 |
-
toast({
|
| 102 |
-
title: t.game.invalidWord,
|
| 103 |
-
description: t.game.lettersOnly,
|
| 104 |
-
variant: "destructive",
|
| 105 |
-
});
|
| 106 |
-
return;
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
if (input.includes(target)) {
|
| 110 |
-
toast({
|
| 111 |
-
title: t.game.invalidWord,
|
| 112 |
-
description: `${t.game.cantUseTargetWord} "${currentWord}"`,
|
| 113 |
-
variant: "destructive",
|
| 114 |
-
});
|
| 115 |
-
return;
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
onSubmitWord(e);
|
| 119 |
};
|
| 120 |
|
| 121 |
-
const
|
| 122 |
-
if (successfulRounds > 0) {
|
| 123 |
-
setShowConfirmDialog(true);
|
| 124 |
-
} else {
|
| 125 |
-
onBack?.();
|
| 126 |
-
}
|
| 127 |
-
};
|
| 128 |
|
| 129 |
return (
|
| 130 |
<motion.div
|
|
@@ -132,88 +65,27 @@ export const SentenceBuilder = ({
|
|
| 132 |
animate={{ opacity: 1 }}
|
| 133 |
className="text-center relative"
|
| 134 |
>
|
| 135 |
-
<
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
</p>
|
| 157 |
-
<div className="mb-6 overflow-hidden rounded-lg bg-secondary/10">
|
| 158 |
-
{imageLoaded && (
|
| 159 |
-
<img
|
| 160 |
-
src={imagePath}
|
| 161 |
-
alt={currentWord}
|
| 162 |
-
className="mx-auto h-48 w-full object-cover"
|
| 163 |
-
/>
|
| 164 |
-
)}
|
| 165 |
-
<p className="p-4 text-2xl font-bold tracking-wider text-secondary">
|
| 166 |
-
{currentWord}
|
| 167 |
-
</p>
|
| 168 |
-
</div>
|
| 169 |
-
</div>
|
| 170 |
-
<form onSubmit={handleSubmit} className="mb-4">
|
| 171 |
-
{sentence.length > 0 && (
|
| 172 |
-
<motion.div
|
| 173 |
-
initial={{ opacity: 0, y: -10 }}
|
| 174 |
-
animate={{ opacity: 1, y: 0 }}
|
| 175 |
-
className="mb-4 text-left p-3 rounded-lg bg-gray-50"
|
| 176 |
-
>
|
| 177 |
-
<p className="text-gray-700">
|
| 178 |
-
{sentence.join(" ")}
|
| 179 |
-
</p>
|
| 180 |
-
</motion.div>
|
| 181 |
-
)}
|
| 182 |
-
<div className="relative mb-4">
|
| 183 |
-
<Input
|
| 184 |
-
ref={inputRef}
|
| 185 |
-
type="text"
|
| 186 |
-
value={playerInput}
|
| 187 |
-
onChange={(e) => onInputChange(e.target.value)}
|
| 188 |
-
onKeyDown={handleKeyDown}
|
| 189 |
-
placeholder={t.game.inputPlaceholder}
|
| 190 |
-
className={`w-full ${hasMultipleWords ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
|
| 191 |
-
disabled={isAiThinking}
|
| 192 |
-
/>
|
| 193 |
-
{hasMultipleWords && (
|
| 194 |
-
<p className="text-sm text-red-500 mt-1">
|
| 195 |
-
{t.game.singleWordOnly}
|
| 196 |
-
</p>
|
| 197 |
-
)}
|
| 198 |
-
</div>
|
| 199 |
-
<div className="flex gap-4">
|
| 200 |
-
<Button
|
| 201 |
-
type="submit"
|
| 202 |
-
className="flex-1 bg-primary text-lg hover:bg-primary/90"
|
| 203 |
-
disabled={!playerInput.trim() || isAiThinking || hasMultipleWords}
|
| 204 |
-
>
|
| 205 |
-
{isAiThinking ? t.game.aiThinking : `${t.game.addWord} ⏎`}
|
| 206 |
-
</Button>
|
| 207 |
-
<Button
|
| 208 |
-
type="button"
|
| 209 |
-
onClick={onMakeGuess}
|
| 210 |
-
className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
|
| 211 |
-
disabled={(!sentence.length && !playerInput.trim()) || isAiThinking || hasMultipleWords}
|
| 212 |
-
>
|
| 213 |
-
{isAiThinking ? t.game.aiThinking : `${t.game.makeGuess} ⇧⏎`}
|
| 214 |
-
</Button>
|
| 215 |
-
</div>
|
| 216 |
-
</form>
|
| 217 |
|
| 218 |
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
| 219 |
<AlertDialogContent>
|
|
@@ -233,4 +105,4 @@ export const SentenceBuilder = ({
|
|
| 233 |
</AlertDialog>
|
| 234 |
</motion.div>
|
| 235 |
);
|
| 236 |
-
};
|
|
|
|
| 1 |
+
import { useState } from "react";
|
|
|
|
| 2 |
import { motion } from "framer-motion";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import {
|
| 4 |
AlertDialog,
|
| 5 |
AlertDialogAction,
|
|
|
|
| 10 |
AlertDialogHeader,
|
| 11 |
AlertDialogTitle,
|
| 12 |
} from "@/components/ui/alert-dialog";
|
| 13 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
| 14 |
+
import { RoundHeader } from "./sentence-builder/RoundHeader";
|
| 15 |
+
import { WordDisplay } from "./sentence-builder/WordDisplay";
|
| 16 |
+
import { SentenceDisplay } from "./sentence-builder/SentenceDisplay";
|
| 17 |
+
import { InputForm } from "./sentence-builder/InputForm";
|
| 18 |
|
| 19 |
interface SentenceBuilderProps {
|
| 20 |
currentWord: string;
|
|
|
|
| 39 |
onMakeGuess,
|
| 40 |
onBack,
|
| 41 |
}: SentenceBuilderProps) => {
|
|
|
|
|
|
|
| 42 |
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
| 43 |
const [hasMultipleWords, setHasMultipleWords] = useState(false);
|
| 44 |
+
const [containsTargetWord, setContainsTargetWord] = useState(false);
|
|
|
|
| 45 |
const t = useTranslation();
|
| 46 |
|
| 47 |
+
// Input validation
|
| 48 |
+
const validateInput = (input: string) => {
|
| 49 |
+
setHasMultipleWords(input.trim().split(/\s+/).length > 1);
|
| 50 |
+
setContainsTargetWord(
|
| 51 |
+
input.toLowerCase().includes(currentWord.toLowerCase())
|
| 52 |
+
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
};
|
| 54 |
|
| 55 |
+
const handleInputChange = (value: string) => {
|
| 56 |
+
validateInput(value);
|
| 57 |
+
onInputChange(value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
};
|
| 59 |
|
| 60 |
+
const isValidInput = !playerInput || /^[\p{L} ]+$/u.test(playerInput);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
return (
|
| 63 |
<motion.div
|
|
|
|
| 65 |
animate={{ opacity: 1 }}
|
| 66 |
className="text-center relative"
|
| 67 |
>
|
| 68 |
+
<RoundHeader
|
| 69 |
+
successfulRounds={successfulRounds}
|
| 70 |
+
onBack={onBack}
|
| 71 |
+
showConfirmDialog={showConfirmDialog}
|
| 72 |
+
setShowConfirmDialog={setShowConfirmDialog}
|
| 73 |
+
/>
|
| 74 |
+
|
| 75 |
+
<WordDisplay currentWord={currentWord} />
|
| 76 |
+
|
| 77 |
+
<SentenceDisplay sentence={sentence} />
|
| 78 |
+
|
| 79 |
+
<InputForm
|
| 80 |
+
playerInput={playerInput}
|
| 81 |
+
onInputChange={handleInputChange}
|
| 82 |
+
onSubmitWord={onSubmitWord}
|
| 83 |
+
onMakeGuess={onMakeGuess}
|
| 84 |
+
isAiThinking={isAiThinking}
|
| 85 |
+
hasMultipleWords={hasMultipleWords}
|
| 86 |
+
containsTargetWord={containsTargetWord}
|
| 87 |
+
isValidInput={isValidInput}
|
| 88 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
| 91 |
<AlertDialogContent>
|
|
|
|
| 105 |
</AlertDialog>
|
| 106 |
</motion.div>
|
| 107 |
);
|
| 108 |
+
};
|
src/components/game/sentence-builder/InputForm.tsx
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { KeyboardEvent, useRef, useEffect } from "react";
|
| 2 |
+
import { Input } from "@/components/ui/input";
|
| 3 |
+
import { Button } from "@/components/ui/button";
|
| 4 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
| 5 |
+
|
| 6 |
+
interface InputFormProps {
|
| 7 |
+
playerInput: string;
|
| 8 |
+
onInputChange: (value: string) => void;
|
| 9 |
+
onSubmitWord: (e: React.FormEvent) => void;
|
| 10 |
+
onMakeGuess: () => void;
|
| 11 |
+
isAiThinking: boolean;
|
| 12 |
+
hasMultipleWords: boolean;
|
| 13 |
+
containsTargetWord: boolean;
|
| 14 |
+
isValidInput: boolean;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export const InputForm = ({
|
| 18 |
+
playerInput,
|
| 19 |
+
onInputChange,
|
| 20 |
+
onSubmitWord,
|
| 21 |
+
onMakeGuess,
|
| 22 |
+
isAiThinking,
|
| 23 |
+
hasMultipleWords,
|
| 24 |
+
containsTargetWord,
|
| 25 |
+
isValidInput
|
| 26 |
+
}: InputFormProps) => {
|
| 27 |
+
const inputRef = useRef<HTMLInputElement>(null);
|
| 28 |
+
const t = useTranslation();
|
| 29 |
+
|
| 30 |
+
useEffect(() => {
|
| 31 |
+
setTimeout(() => {
|
| 32 |
+
inputRef.current?.focus();
|
| 33 |
+
}, 100);
|
| 34 |
+
}, []);
|
| 35 |
+
|
| 36 |
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
| 37 |
+
if (e.shiftKey && e.key === 'Enter') {
|
| 38 |
+
e.preventDefault();
|
| 39 |
+
if (!hasMultipleWords && !containsTargetWord && !isAiThinking && isValidInput) {
|
| 40 |
+
onMakeGuess();
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const getInputError = () => {
|
| 46 |
+
if (hasMultipleWords) return t.game.singleWordOnly;
|
| 47 |
+
if (containsTargetWord) return t.game.cantUseTargetWord;
|
| 48 |
+
if (!isValidInput && playerInput) return t.game.lettersOnly;
|
| 49 |
+
return null;
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
const error = getInputError();
|
| 53 |
+
|
| 54 |
+
return (
|
| 55 |
+
<form onSubmit={onSubmitWord} className="mb-4">
|
| 56 |
+
<div className="relative mb-4">
|
| 57 |
+
<Input
|
| 58 |
+
ref={inputRef}
|
| 59 |
+
type="text"
|
| 60 |
+
value={playerInput}
|
| 61 |
+
onChange={(e) => onInputChange(e.target.value)}
|
| 62 |
+
onKeyDown={handleKeyDown}
|
| 63 |
+
placeholder={t.game.inputPlaceholder}
|
| 64 |
+
className={`w-full ${error ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
|
| 65 |
+
disabled={isAiThinking}
|
| 66 |
+
/>
|
| 67 |
+
{error && (
|
| 68 |
+
<p className="text-sm text-red-500 mt-1">
|
| 69 |
+
{error}
|
| 70 |
+
</p>
|
| 71 |
+
)}
|
| 72 |
+
</div>
|
| 73 |
+
<div className="flex gap-4">
|
| 74 |
+
<Button
|
| 75 |
+
type="submit"
|
| 76 |
+
className="flex-1 bg-primary text-lg hover:bg-primary/90"
|
| 77 |
+
disabled={!playerInput.trim() || isAiThinking || hasMultipleWords || containsTargetWord || !isValidInput}
|
| 78 |
+
>
|
| 79 |
+
{isAiThinking ? t.game.aiThinking : `${t.game.addWord} ⏎`}
|
| 80 |
+
</Button>
|
| 81 |
+
<Button
|
| 82 |
+
type="button"
|
| 83 |
+
onClick={onMakeGuess}
|
| 84 |
+
className="flex-1 bg-secondary text-lg hover:bg-secondary/90"
|
| 85 |
+
disabled={(!playerInput.trim() && !playerInput.trim()) || isAiThinking || hasMultipleWords || containsTargetWord || !isValidInput}
|
| 86 |
+
>
|
| 87 |
+
{isAiThinking ? t.game.aiThinking : `${t.game.makeGuess} ⇧⏎`}
|
| 88 |
+
</Button>
|
| 89 |
+
</div>
|
| 90 |
+
</form>
|
| 91 |
+
);
|
| 92 |
+
};
|
src/components/game/sentence-builder/RoundHeader.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { House } from "lucide-react";
|
| 2 |
+
import { Button } from "@/components/ui/button";
|
| 3 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
| 4 |
+
|
| 5 |
+
interface RoundHeaderProps {
|
| 6 |
+
successfulRounds: number;
|
| 7 |
+
onBack?: () => void;
|
| 8 |
+
showConfirmDialog: boolean;
|
| 9 |
+
setShowConfirmDialog: (show: boolean) => void;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export const RoundHeader = ({
|
| 13 |
+
successfulRounds,
|
| 14 |
+
onBack,
|
| 15 |
+
showConfirmDialog,
|
| 16 |
+
setShowConfirmDialog
|
| 17 |
+
}: RoundHeaderProps) => {
|
| 18 |
+
const t = useTranslation();
|
| 19 |
+
|
| 20 |
+
const handleHomeClick = () => {
|
| 21 |
+
if (successfulRounds > 0) {
|
| 22 |
+
setShowConfirmDialog(true);
|
| 23 |
+
} else {
|
| 24 |
+
onBack?.();
|
| 25 |
+
}
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
return (
|
| 29 |
+
<div className="relative">
|
| 30 |
+
<div className="absolute right-0 top-0 bg-primary/10 px-3 py-1 rounded-lg">
|
| 31 |
+
<span className="text-sm font-medium text-primary">
|
| 32 |
+
{t.game.round} {successfulRounds + 1}
|
| 33 |
+
</span>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<Button
|
| 37 |
+
variant="ghost"
|
| 38 |
+
size="icon"
|
| 39 |
+
className="absolute left-0 top-0 text-gray-600 hover:text-primary"
|
| 40 |
+
onClick={handleHomeClick}
|
| 41 |
+
>
|
| 42 |
+
<House className="h-5 w-5" />
|
| 43 |
+
</Button>
|
| 44 |
+
|
| 45 |
+
<h2 className="mb-4 text-2xl font-semibold text-gray-900">
|
| 46 |
+
Think in Sync
|
| 47 |
+
</h2>
|
| 48 |
+
</div>
|
| 49 |
+
);
|
| 50 |
+
};
|
src/components/game/sentence-builder/SentenceDisplay.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from "framer-motion";
|
| 2 |
+
|
| 3 |
+
interface SentenceDisplayProps {
|
| 4 |
+
sentence: string[];
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export const SentenceDisplay = ({ sentence }: SentenceDisplayProps) => {
|
| 8 |
+
if (!sentence.length) return null;
|
| 9 |
+
|
| 10 |
+
return (
|
| 11 |
+
<motion.div
|
| 12 |
+
initial={{ opacity: 0, y: -10 }}
|
| 13 |
+
animate={{ opacity: 1, y: 0 }}
|
| 14 |
+
className="mb-4 text-left p-3 rounded-lg bg-gray-50"
|
| 15 |
+
>
|
| 16 |
+
<p className="text-gray-700">
|
| 17 |
+
{sentence.join(" ")}
|
| 18 |
+
</p>
|
| 19 |
+
</motion.div>
|
| 20 |
+
);
|
| 21 |
+
};
|
src/components/game/sentence-builder/WordDisplay.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { useTranslation } from "@/hooks/useTranslation";
|
| 3 |
+
|
| 4 |
+
interface WordDisplayProps {
|
| 5 |
+
currentWord: string;
|
| 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>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
);
|
| 39 |
+
};
|
src/i18n/translations/de.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const de = {
|
|
| 10 |
aiThinking: "KI denkt nach...",
|
| 11 |
aiDelayed: "Die KI ist derzeit beschäftigt. Bitte versuche es gleich noch einmal.",
|
| 12 |
invalidWord: "Ungültiges Wort",
|
| 13 |
-
cantUseTargetWord: "
|
| 14 |
lettersOnly: "Bitte nur Buchstaben verwenden",
|
| 15 |
singleWordOnly: "Bitte nur ein Wort eingeben",
|
| 16 |
leaveGameTitle: "Spiel verlassen?",
|
|
|
|
| 10 |
aiThinking: "KI denkt nach...",
|
| 11 |
aiDelayed: "Die KI ist derzeit beschäftigt. Bitte versuche es gleich noch einmal.",
|
| 12 |
invalidWord: "Ungültiges Wort",
|
| 13 |
+
cantUseTargetWord: "Verwende nicht das geheime Wort",
|
| 14 |
lettersOnly: "Bitte nur Buchstaben verwenden",
|
| 15 |
singleWordOnly: "Bitte nur ein Wort eingeben",
|
| 16 |
leaveGameTitle: "Spiel verlassen?",
|
src/i18n/translations/en.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const en = {
|
|
| 10 |
aiThinking: "AI is thinking...",
|
| 11 |
aiDelayed: "The AI is currently busy. Please try again in a moment.",
|
| 12 |
invalidWord: "Invalid Word",
|
| 13 |
-
cantUseTargetWord: "
|
| 14 |
lettersOnly: "Please use letters only",
|
| 15 |
singleWordOnly: "Please enter only one word",
|
| 16 |
leaveGameTitle: "Leave Game?",
|
|
|
|
| 10 |
aiThinking: "AI is thinking...",
|
| 11 |
aiDelayed: "The AI is currently busy. Please try again in a moment.",
|
| 12 |
invalidWord: "Invalid Word",
|
| 13 |
+
cantUseTargetWord: "Do not use the secret word",
|
| 14 |
lettersOnly: "Please use letters only",
|
| 15 |
singleWordOnly: "Please enter only one word",
|
| 16 |
leaveGameTitle: "Leave Game?",
|
src/i18n/translations/es.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const es = {
|
|
| 10 |
aiThinking: "La IA está pensando...",
|
| 11 |
aiDelayed: "La IA está ocupada en este momento. Por favor, inténtalo de nuevo en un momento.",
|
| 12 |
invalidWord: "Palabra inválida",
|
| 13 |
-
cantUseTargetWord: "No
|
| 14 |
lettersOnly: "Por favor, usa solo letras",
|
| 15 |
singleWordOnly: "Por favor, ingresa solo una palabra",
|
| 16 |
leaveGameTitle: "¿Salir del juego?",
|
|
|
|
| 10 |
aiThinking: "La IA está pensando...",
|
| 11 |
aiDelayed: "La IA está ocupada en este momento. Por favor, inténtalo de nuevo en un momento.",
|
| 12 |
invalidWord: "Palabra inválida",
|
| 13 |
+
cantUseTargetWord: "No uses la palabra secreta",
|
| 14 |
lettersOnly: "Por favor, usa solo letras",
|
| 15 |
singleWordOnly: "Por favor, ingresa solo una palabra",
|
| 16 |
leaveGameTitle: "¿Salir del juego?",
|
src/i18n/translations/fr.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const fr = {
|
|
| 9 |
aiThinking: "L'IA réfléchit...",
|
| 10 |
aiDelayed: "L'IA est actuellement occupée. Veuillez réessayer dans un moment.",
|
| 11 |
invalidWord: "Mot invalide",
|
| 12 |
-
cantUseTargetWord: "
|
| 13 |
lettersOnly: "Veuillez utiliser uniquement des lettres",
|
| 14 |
singleWordOnly: "Veuillez entrer un seul mot",
|
| 15 |
leaveGameTitle: "Quitter le jeu ?",
|
|
|
|
| 9 |
aiThinking: "L'IA réfléchit...",
|
| 10 |
aiDelayed: "L'IA est actuellement occupée. Veuillez réessayer dans un moment.",
|
| 11 |
invalidWord: "Mot invalide",
|
| 12 |
+
cantUseTargetWord: "N'utilisez pas le mot secret",
|
| 13 |
lettersOnly: "Veuillez utiliser uniquement des lettres",
|
| 14 |
singleWordOnly: "Veuillez entrer un seul mot",
|
| 15 |
leaveGameTitle: "Quitter le jeu ?",
|
src/i18n/translations/it.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const it = {
|
|
| 10 |
aiThinking: "L'IA sta pensando...",
|
| 11 |
aiDelayed: "L'IA è attualmente occupata. Riprova tra un momento.",
|
| 12 |
invalidWord: "Parola non valida",
|
| 13 |
-
cantUseTargetWord: "Non
|
| 14 |
lettersOnly: "Usa solo lettere",
|
| 15 |
singleWordOnly: "Inserisci una sola parola",
|
| 16 |
leaveGameTitle: "Lasciare il gioco?",
|
|
|
|
| 10 |
aiThinking: "L'IA sta pensando...",
|
| 11 |
aiDelayed: "L'IA è attualmente occupata. Riprova tra un momento.",
|
| 12 |
invalidWord: "Parola non valida",
|
| 13 |
+
cantUseTargetWord: "Non usare la parola segreta",
|
| 14 |
lettersOnly: "Usa solo lettere",
|
| 15 |
singleWordOnly: "Inserisci una sola parola",
|
| 16 |
leaveGameTitle: "Lasciare il gioco?",
|