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>
This commit is contained in:
146
src/components/PairSentenceCard.jsx
Normal file
146
src/components/PairSentenceCard.jsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useState } from 'react'
|
||||
import './PairCards.css'
|
||||
|
||||
function BboxOverlay({ chip, lang, native }) {
|
||||
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>
|
||||
{/* dim area outside bbox */}
|
||||
<rect width="100" height="100" fill="rgba(0,0,0,0.48)" mask={`url(#${maskId})`} />
|
||||
{/* glowing amber border */}
|
||||
<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"
|
||||
/>
|
||||
{/* word label below bbox */}
|
||||
{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
|
||||
})
|
||||
}
|
||||
|
||||
export default function PairSentenceCard({ card, onComplete }) {
|
||||
const [done, setDone] = useState(false)
|
||||
const [activeChip, setActiveChip] = useState(null)
|
||||
|
||||
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' || !activeChip.bbox)
|
||||
const isObject = activeChip && activeChip.type === 'object' && activeChip.bbox
|
||||
|
||||
function handleChipClick(id, entry) {
|
||||
setActiveChip(prev => prev?.id === id ? null : { id, ...entry })
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
setDone(true)
|
||||
setActiveChip(null)
|
||||
setTimeout(() => onComplete('correct'), 600)
|
||||
}
|
||||
|
||||
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 ?? 2} 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-sentence">
|
||||
{resolveSentence(sentence, card.placeholders, lang, handleChipClick, activeChip?.id)}
|
||||
</p>
|
||||
{hint && (
|
||||
<p className="pair-hint">
|
||||
{resolveSentence(hint, card.placeholders, native)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="pair-btn-row">
|
||||
<button
|
||||
className={`pair-btn ${done ? 'pair-btn-correct' : 'pair-btn-primary'}`}
|
||||
onClick={handleConfirm}
|
||||
disabled={done}
|
||||
>
|
||||
{done ? '✓ Verstanden' : 'Verstanden'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user