import { useState, useRef, useEffect, useMemo } from 'react' import { useParams, useNavigate } from 'react-router' import { useFlashcardSet } from '@/features/flashcards/api/queries' import { Button } from '@/components/ui/button' import { HelpCircle, ArrowRight, Loader2 } from 'lucide-react' import { cn } from '@/lib/utils' import { motion, AnimatePresence } from 'framer-motion' import { Watermark } from '../components/Watermark' import { ProgressBar } from '../components/ProgressBar' import { EndCard } from '../components/EndCard' // Helper to tokenize sentence into [word, punctuation, word, ...] const tokenize = (text: string) => { return text.split(/([^\w\u00C0-\u017F]+)/).filter(t => t.length > 0) } const isWord = (token: string) => { return /^[\w\u00C0-\u017F]+$/.test(token) } interface WordInputProps { expected: string value: string status: 'idle' | 'correct' | 'incorrect' onChange: (val: string) => void onComplete: () => void onBackspaceFromEmpty: () => void inputRef: (el: HTMLInputElement | null) => void isFocused: boolean } const WordInput = ({ expected, value, status, onChange, onComplete, onBackspaceFromEmpty, inputRef, isFocused }: WordInputProps) => { const displayChars = useMemo(() => { const chars = [] for (let i = 0; i < expected.length; i++) { chars.push(value[i] || '_') } return chars }, [expected, value]) const handleChange = (e: React.ChangeEvent) => { // Strip spaces to prevent them from "eating" the underscores const rawVal = e.target.value.replace(/\s/g, '') const newVal = rawVal.slice(0, expected.length) onChange(newVal) if (newVal.length === expected.length) { onComplete() } } return (
{/* Render spaced characters */} {displayChars.join('')}
{ // Navigate on Space if (e.key === ' ') { e.preventDefault() onComplete() } if (e.key === 'Backspace' && value.length === 0) onBackspaceFromEmpty() }} className="absolute inset-0 w-full h-full opacity-0 cursor-text" autoComplete="off" disabled={status !== 'idle'} />
) } export const SpellingModePage = () => { const { setId } = useParams() const navigate = useNavigate() const { data: set, isLoading } = useFlashcardSet(setId || '') const [currentIndex, setCurrentIndex] = useState(0) const [status, setStatus] = useState<'idle' | 'correct' | 'incorrect'>('idle') const [score, setScore] = useState(0) const [streak, setStreak] = useState(0) const [completed, setCompleted] = useState(false) const [showAnswer, setShowAnswer] = useState(false) const [focusedIndex, setFocusedIndex] = useState(0) const [inputValues, setInputValues] = useState([]) const inputRefs = useRef<(HTMLInputElement | null)[]>([]) if (isLoading) return
if (!set) return
Set not found
if (!set.cards || set.cards.length === 0) { return (

No Cards Found

There are no cards in this set. Add some to continue!

) } // Ensure card exists before accessing const card = set.cards && set.cards[currentIndex] ? set.cards[currentIndex] : null if (!card) return
Card not found
const tokens = useMemo(() => tokenize(card.front), [card]) useEffect(() => { setInputValues(tokens.map(t => isWord(t) ? '' : t)) setStatus('idle') setShowAnswer(false) setFocusedIndex(0) // Find first word const firstWordIdx = tokens.findIndex(t => isWord(t)) if (firstWordIdx !== -1) { setFocusedIndex(firstWordIdx) setTimeout(() => inputRefs.current[firstWordIdx]?.focus(), 50) } }, [currentIndex, tokens]) const handleInputChange = (index: number, val: string) => { const newValues = [...inputValues] newValues[index] = val setInputValues(newValues) } const focusNext = (currIdx: number) => { for (let i = currIdx + 1; i < tokens.length; i++) { if (isWord(tokens[i])) { inputRefs.current[i]?.focus() setFocusedIndex(i) return } } } const focusPrev = (currIdx: number) => { for (let i = currIdx - 1; i >= 0; i--) { if (isWord(tokens[i])) { inputRefs.current[i]?.focus() setFocusedIndex(i) return } } } const checkAnswer = (e?: React.FormEvent) => { e?.preventDefault() if (status !== 'idle') return const userSentence = inputValues.join('') const normalize = (s: string) => s.toLowerCase().trim() const isCorrect = normalize(userSentence) === normalize(card.front) setStatus(isCorrect ? 'correct' : 'incorrect') if (isCorrect) { setScore(score + 1) setStreak(streak + 1) } else { setStreak(0) } } const handleGiveUp = () => { setStatus('incorrect') setShowAnswer(true) setStreak(0) } const nextCard = () => { if (currentIndex < set.cards.length - 1) { setCurrentIndex(currentIndex + 1) } else { setCompleted(true) } } const handleRestart = () => { setCurrentIndex(0) setScore(0) setStreak(0) setCompleted(false) setStatus('idle') } if (completed) { return } return (

Translate to Norwegian

{card.back}

{tokens.map((token, idx) => { if (!isWord(token)) { return {token} } return ( handleInputChange(idx, val)} onComplete={() => focusNext(idx)} onBackspaceFromEmpty={() => focusPrev(idx)} inputRef={(el) => (inputRefs.current[idx] = el)} isFocused={focusedIndex === idx} /> ) })}
{status === 'idle' ? (
) : ( {(status === 'incorrect' || showAnswer) && (
{card.front}
)}
)}
) }