import { useState, useRef } from 'react' import confetti from 'canvas-confetti' import './PairCards.css' function triggerConfetti() { confetti({ particleCount: 90, spread: 70, origin: { y: 0.55 }, colors: ['#C4A85A', '#7A5C2E', '#3D7055', '#E8C9A8', '#fff'], scalar: 0.9, gravity: 1.1, }) } function SelectionOverlay({ chip }) { const sels = chip?.selections if (!sels?.length) return null const maskId = `selmask-${chip.id.slice(0, 8)}` const label = chip.label || '' const toPoints = pts => pts.map(p => `${p.x * 100},${p.y * 100}`).join(' ') const firstPts = sels[0].points const xs = firstPts.map(p => p.x * 100) const ys = firstPts.map(p => p.y * 100) const cx = (Math.min(...xs) + Math.max(...xs)) / 2 const labelY = Math.min(Math.max(...ys) + 6, 94) return ( ) } // Sentence format: {{label.w:uuid}} or {{label.o:uuid}} function resolveSentence(sentence, placeholders, onChipClick, activeId) { if (!sentence) return null const parts = sentence.split(/(\{\{[^}]+\.[wo]:[0-9a-f-]{36}\}\})/) return parts.map((part, i) => { const m = part.match(/^\{\{([^.]+)\.(w|o):([0-9a-f-]{36})\}\}$/) if (m) { const label = m[1] const type = m[2] === 'w' ? 'word' : 'object' const id = m[3] const entry = placeholders?.[id] || {} return ( { e.stopPropagation(); onChipClick?.(id, { label, type, ...entry }) }} > {label} ) } return part }) } // Extract unique vocab entries from a sentence string function extractVocab(sentence) { if (!sentence) return [] const matches = [...sentence.matchAll(/\{\{([^.]+)\.[wo]:([0-9a-f-]{36})\}\}/g)] const seen = new Set() return matches .filter(m => { const ok = !seen.has(m[2]); seen.add(m[2]); return ok }) .map(m => ({ label: m[1], id: m[2] })) } // Strip placeholders to plain text for TTS function toPlainText(sentence) { if (!sentence) return '' return sentence.replace(/\{\{([^.]+)\.[wo]:[0-9a-f-]{36}\}\}/g, '$1') } const LANG_LABELS = { sv: 'Svenska', en: 'English', de: 'Deutsch' } const LANG_TTS = { sv: 'sv-SE', en: 'en-US', de: 'de-DE' } // Circumference of r=16 circle ≈ 100.53 const RING_C = 2 * Math.PI * 16 export default function PairSentenceCard({ card, onComplete }) { const [done, setDone] = useState(false) const [activeChip, setActiveChip] = useState(null) const [showTranslation, setShowTranslation] = useState(false) const [holding, setHolding] = useState(false) const [unlocked, setUnlocked] = useState(false) const holdCompleted = useRef(false) const lang = card.lang || 'de' const native = lang === 'de' ? 'en' : 'de' const stmt = card.positive_statement const sentence = stmt?.[`sentence_${lang}`] || stmt?.sentence_de const hint = stmt?.[`sentence_${native}`] || null const pic = card.picture?.url const isWord = activeChip && activeChip.type === 'word' const isObject = activeChip && activeChip.type === 'object' && activeChip.selections?.length const vocab = extractVocab(sentence) function handleChipClick(id, entry) { setActiveChip(prev => prev?.id === id ? null : { id, ...entry }) } function handleConfirm() { if (!unlocked) return setDone(true) setActiveChip(null) triggerConfetti() setTimeout(() => onComplete('correct'), 900) } function handleTTS() { if (!window.speechSynthesis || !sentence) return window.speechSynthesis.cancel() const utt = new SpeechSynthesisUtterance(toPlainText(sentence)) utt.lang = LANG_TTS[lang] || 'de-DE' utt.rate = 0.9 window.speechSynthesis.speak(utt) setUnlocked(true) } function startHold() { holdCompleted.current = false setHolding(true) setShowTranslation(true) } function endHold() { setHolding(false) if (!holdCompleted.current) setShowTranslation(false) } function onHoldComplete() { holdCompleted.current = true setUnlocked(true) } return (
{resolveSentence(sentence, card.placeholders, handleChipClick, activeChip?.id)}
{hint && ({resolveSentence(hint, card.placeholders, null, null)}
)}