Files
app-hejyou/src/components/PairWordCard.jsx
admin fb71af5f1d Redesign pair cards + add chip image highlight
- Complete CSS overhaul: refined typography, warm palette, 4:3 images, elegant chips
- Word chip click → popup badge with word + translation
- Object chip click → SVG bbox overlay dims image outside object bounds, draws amber outline
- All three card types (Sentence, YesNo, Word) updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:30:38 +02:00

185 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useMemo } from 'react'
import './PairCards.css'
function BboxOverlay({ chip, lang }) {
if (!chip?.bbox) return null
const { x, y, w, h } = chip.bbox
const maskId = `bbmask-${chip.id.slice(0, 8)}`
const label = chip[lang] || chip.de || ''
const labelY = Math.min((y + h + 0.07) * 100, 93)
return (
<svg className="pair-bbox-svg" viewBox="0 0 100 100" preserveAspectRatio="none">
<defs>
<mask id={maskId}>
<rect width="100" height="100" fill="white" />
<rect x={x*100} y={y*100} width={w*100} height={h*100} fill="black" />
</mask>
</defs>
<rect width="100" height="100" fill="rgba(0,0,0,0.48)" mask={`url(#${maskId})`} />
<rect
x={x*100} y={y*100} width={w*100} height={h*100}
fill="rgba(255,215,100,0.08)"
stroke="rgba(255,215,100,0.92)"
strokeWidth="1.4"
rx="1.5"
/>
{label && (
<text
x={(x + w / 2) * 100}
y={labelY}
textAnchor="middle"
fill="white"
fontSize="5.5"
fontWeight="700"
fontFamily="Nunito, sans-serif"
style={{ filter: 'drop-shadow(0 1px 4px rgba(0,0,0,1))' }}
>
{label}
</text>
)}
</svg>
)
}
function resolveSentence(sentence, placeholders, lang, onChipClick, activeId) {
if (!sentence) return null
const parts = sentence.split(/(\{\{[0-9a-f-]{36}\}\})/)
return parts.map((part, i) => {
const m = part.match(/^\{\{([0-9a-f-]{36})\}\}$/)
if (m) {
const id = m[1]
const entry = placeholders?.[id]
const label = entry?.[lang] || entry?.de || '…'
return (
<span
key={i}
className={`pair-word-chip${activeId === id ? ' active' : ''}`}
onClick={e => { e.stopPropagation(); onChipClick?.(id, entry) }}
>
{label}
</span>
)
}
return part
})
}
function shuffle(arr) {
const a = [...arr]
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]]
}
return a
}
export default function PairWordCard({ card, onComplete }) {
const [picked, setPicked] = useState(null)
const [activeChip, setActiveChip] = useState(null)
const lang = card.lang || 'de'
const native = lang === 'de' ? 'en' : 'de'
const isWord = activeChip && (activeChip.type === 'word' || !activeChip.bbox)
const isObject = activeChip && activeChip.type === 'object' && activeChip.bbox
const q = card.question
const stmt = card.positive_statement
const neg = card.negative_statement
const sentence = q?.[`sentence_${lang}`] || q?.sentence_de
const hint = q?.[`sentence_${native}`] || null
const pic = card.picture?.url
const options = useMemo(() => {
const pos = (stmt?.positive_words || []).map(w => ({ ...w, correct: true }))
const neg2 = (neg?.negative_words || []).map(w => ({ ...w, correct: false }))
return shuffle([...pos, ...neg2])
}, [stmt, neg])
function handleChipClick(id, entry) {
setActiveChip(prev => prev?.id === id ? null : { id, ...entry })
}
function handlePick(opt) {
if (picked) return
setActiveChip(null)
setPicked(opt)
const r = opt.correct ? 'correct' : 'wrong'
setTimeout(() => onComplete(r), 900)
}
return (
<div className="pair-card" onClick={() => setActiveChip(null)}>
<div className="pair-card-header">
<span className="pair-lang-pill">{lang.toUpperCase()}</span>
<span className="pair-points-pill">+{card.meta?.points ?? 3} Pkt</span>
</div>
<div className={`pair-image-wrap${isWord ? ' chip-active' : ''}`}>
{pic
? <img src={pic} alt="" className="pair-image" loading="lazy" />
: <div className="pair-image-placeholder">🖼</div>
}
{isWord && (
<div className="pair-chip-highlight">
<div className="pair-chip-highlight-badge">
<span className="pair-chip-highlight-target">
{activeChip[lang] || activeChip.de || '…'}
</span>
{(activeChip[native] || activeChip.en) && (
<span className="pair-chip-highlight-native">
{activeChip[native] || activeChip.en}
</span>
)}
</div>
</div>
)}
{isObject && <BboxOverlay chip={activeChip} lang={lang} native={native} />}
</div>
<div className="pair-card-body" onClick={e => e.stopPropagation()}>
<p className="pair-question">
{resolveSentence(sentence, card.placeholders, lang, handleChipClick, activeChip?.id)}
</p>
{hint && !picked && (
<p className="pair-hint">
{resolveSentence(hint, card.placeholders, native)}
</p>
)}
<div className="pair-options">
{options.map((opt) => {
const label = opt[lang] || opt.de || '…'
const hint2 = opt[native] || null
let cls = 'pair-option-btn'
if (picked) {
if (opt.id === picked.id) cls += opt.correct ? ' correct' : ' wrong'
else if (opt.correct) cls += ' correct'
}
return (
<button key={opt.id} className={cls} onClick={() => handlePick(opt)} disabled={!!picked}>
{label}
{picked && hint2 && opt.correct && (
<span style={{ display: 'block', fontSize: '11px', opacity: 0.85, marginTop: 2 }}>
{hint2}
</span>
)}
</button>
)
})}
</div>
{picked && (
<p className={`pair-feedback ${picked.correct ? 'correct' : 'wrong'}`}>
{picked.correct
? '✓ Richtig!'
: `✗ Richtig wäre: ${options.find(o => o.correct)?.[lang] || options.find(o => o.correct)?.de || '?'}`
}
</p>
)}
</div>
</div>
)
}