feat: remove header row, add unlock gate + ring animation to Satz card
- Remove lang/points header from all 3 card types - PairSentenceCard: Verstanden button locked until audio played OR translate button held for 2 seconds - Hold button shows circular gold ring filling up over 2s while held - TTS button highlights gold when unlocked - .pair-btn-locked style for disabled state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -71,12 +71,31 @@
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
color: #7A6E55;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.pair-icon-btn:hover { background: #E0DAC8; }
|
||||
.pair-icon-btn.active { background: #E0DAC8; }
|
||||
.pair-icon-btn.active { background: #C4A85A22; color: #B07840; }
|
||||
|
||||
/* Hold-to-translate button */
|
||||
.pair-hold-wrap {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
border-radius: 10px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Ring fill animation — 2 seconds */
|
||||
@keyframes holdRing {
|
||||
from { stroke-dashoffset: 100.53; }
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
/* ── Image area ── */
|
||||
.pair-image-wrap {
|
||||
@@ -278,6 +297,11 @@
|
||||
background: #5C3D22;
|
||||
color: #F5EDE0;
|
||||
}
|
||||
.pair-btn-locked {
|
||||
background: #E0DDD5;
|
||||
color: #B0A898;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.pair-btn-yes {
|
||||
background: #3D7055;
|
||||
color: #fff;
|
||||
|
||||
@@ -93,11 +93,16 @@ function toPlainText(sentence) {
|
||||
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 holdTimer = useRef(null)
|
||||
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'
|
||||
@@ -117,6 +122,7 @@ export default function PairSentenceCard({ card, onComplete }) {
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (!unlocked) return
|
||||
setDone(true)
|
||||
setActiveChip(null)
|
||||
triggerConfetti()
|
||||
@@ -130,14 +136,21 @@ export default function PairSentenceCard({ card, onComplete }) {
|
||||
utt.lang = LANG_TTS[lang] || 'de-DE'
|
||||
utt.rate = 0.9
|
||||
window.speechSynthesis.speak(utt)
|
||||
setUnlocked(true)
|
||||
}
|
||||
|
||||
function startTranslation() {
|
||||
holdTimer.current = setTimeout(() => setShowTranslation(true), 150)
|
||||
function startHold() {
|
||||
holdCompleted.current = false
|
||||
setHolding(true)
|
||||
setShowTranslation(true)
|
||||
}
|
||||
function endTranslation() {
|
||||
clearTimeout(holdTimer.current)
|
||||
setShowTranslation(false)
|
||||
function endHold() {
|
||||
setHolding(false)
|
||||
if (!holdCompleted.current) setShowTranslation(false)
|
||||
}
|
||||
function onHoldComplete() {
|
||||
holdCompleted.current = true
|
||||
setUnlocked(true)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -159,17 +172,7 @@ export default function PairSentenceCard({ card, onComplete }) {
|
||||
{isObject && <SelectionOverlay chip={activeChip} />}
|
||||
</div>
|
||||
|
||||
{/* Header below image */}
|
||||
<div className="pair-card-header">
|
||||
<span className="pair-lang-pill">{LANG_LABELS[lang] || lang}</span>
|
||||
<span className="pair-points-pill">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="#C4A85A"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||
+{card.meta?.points ?? 2} Punkte
|
||||
</span>
|
||||
</div>
|
||||
<div className="pair-header-divider" />
|
||||
|
||||
<div className="pair-card-body" onClick={e => e.stopPropagation()}>
|
||||
<div className="pair-card-body" onClick={e => e.stopPropagation()} style={{ paddingTop: 18 }}>
|
||||
|
||||
{/* Sentence + action buttons */}
|
||||
<p className="pair-section-label">Satz</p>
|
||||
@@ -184,7 +187,7 @@ export default function PairSentenceCard({ card, onComplete }) {
|
||||
color: '#7A7060',
|
||||
transition: 'opacity 0.18s',
|
||||
margin: 0,
|
||||
marginTop: showTranslation ? 0 : '-1.7em', /* overlay effect */
|
||||
marginTop: showTranslation ? 0 : '-1.7em',
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
{resolveSentence(hint, card.placeholders, null, null)}
|
||||
@@ -193,30 +196,52 @@ export default function PairSentenceCard({ card, onComplete }) {
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingTop: 2 }}>
|
||||
{/* TTS */}
|
||||
<button className="pair-icon-btn" onClick={handleTTS} title="Vorlesen">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#7A6E55" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
{/* TTS — playing unlocks "Verstanden" */}
|
||||
<button className={`pair-icon-btn${unlocked ? ' active' : ''}`} onClick={handleTTS} title="Vorlesen">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
|
||||
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
|
||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/* Hold-to-translate */}
|
||||
|
||||
{/* Hold-to-translate: 2 s hold unlocks "Verstanden" */}
|
||||
{hint && (
|
||||
<button
|
||||
className={`pair-icon-btn${showTranslation ? ' active' : ''}`}
|
||||
onMouseDown={startTranslation}
|
||||
onMouseUp={endTranslation}
|
||||
onMouseLeave={endTranslation}
|
||||
onTouchStart={e => { e.preventDefault(); startTranslation() }}
|
||||
onTouchEnd={endTranslation}
|
||||
title="Übersetzung halten"
|
||||
<div className="pair-hold-wrap"
|
||||
onMouseDown={startHold}
|
||||
onMouseUp={endHold}
|
||||
onMouseLeave={endHold}
|
||||
onTouchStart={e => { e.preventDefault(); startHold() }}
|
||||
onTouchEnd={endHold}
|
||||
title="2 s halten zum Übersetzen"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#7A6E55" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg width="38" height="38" viewBox="0 0 38 38" style={{ display: 'block' }}>
|
||||
{/* Button fill */}
|
||||
<rect x="1" y="1" width="36" height="36" rx="9" ry="9"
|
||||
fill="#F0EDE3" stroke="#D8D3C5" strokeWidth="0.5"/>
|
||||
{/* Progress ring track */}
|
||||
<circle cx="19" cy="19" r="16" fill="none" stroke="#E0DDD5" strokeWidth="2.5"/>
|
||||
{/* Progress ring — animates when holding */}
|
||||
{holding && (
|
||||
<circle
|
||||
cx="19" cy="19" r="16"
|
||||
fill="none"
|
||||
stroke="#C4A85A"
|
||||
strokeWidth="2.5"
|
||||
strokeDasharray={RING_C}
|
||||
strokeDashoffset={RING_C}
|
||||
strokeLinecap="round"
|
||||
style={{ transformOrigin: '19px 19px', transform: 'rotate(-90deg)', animation: 'holdRing 2s linear forwards' }}
|
||||
onAnimationEnd={onHoldComplete}
|
||||
/>
|
||||
)}
|
||||
{/* Translate icon */}
|
||||
<g transform="translate(10, 10)" fill="none" stroke="#7A6E55" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M5 8l6 6"/><path d="M4 14l6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/>
|
||||
<path d="M22 22l-5-10-5 10"/><path d="M14 18h6"/>
|
||||
<path d="M14 14l-3-6-3 6"/><path d="M6 12h4" transform="translate(8,0)"/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,9 +260,9 @@ export default function PairSentenceCard({ card, onComplete }) {
|
||||
|
||||
<div className="pair-btn-row" style={{ marginTop: 20 }}>
|
||||
<button
|
||||
className={`pair-btn ${done ? 'pair-btn-correct' : 'pair-btn-primary'}`}
|
||||
className={`pair-btn ${done ? 'pair-btn-correct' : unlocked ? 'pair-btn-primary' : 'pair-btn-locked'}`}
|
||||
onClick={handleConfirm}
|
||||
disabled={done}
|
||||
disabled={done || !unlocked}
|
||||
>
|
||||
{done ? '✓ Verstanden' : 'Verstanden'}
|
||||
</button>
|
||||
|
||||
@@ -139,17 +139,7 @@ export default function PairWordCard({ card, onComplete }) {
|
||||
{isObject && <SelectionOverlay chip={activeChip} />}
|
||||
</div>
|
||||
|
||||
{/* Header below image */}
|
||||
<div className="pair-card-header">
|
||||
<span className="pair-lang-pill">{lang === 'sv' ? 'Svenska' : lang === 'en' ? 'English' : 'Deutsch'}</span>
|
||||
<span className="pair-points-pill">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="#C4A85A"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||
+{card.meta?.points ?? 3} Punkte
|
||||
</span>
|
||||
</div>
|
||||
<div className="pair-header-divider" />
|
||||
|
||||
<div className="pair-card-body" onClick={e => e.stopPropagation()}>
|
||||
<div className="pair-card-body" onClick={e => e.stopPropagation()} style={{ paddingTop: 18 }}>
|
||||
<p className="pair-section-label">Frage</p>
|
||||
<p className="pair-question">
|
||||
{resolveSentence(sentence, card.placeholders, handleChipClick, activeChip?.id)}
|
||||
|
||||
@@ -124,17 +124,7 @@ export default function PairYesNoCard({ card, onComplete }) {
|
||||
{isObject && <SelectionOverlay chip={activeChip} />}
|
||||
</div>
|
||||
|
||||
{/* Header below image */}
|
||||
<div className="pair-card-header">
|
||||
<span className="pair-lang-pill">{lang === 'sv' ? 'Svenska' : lang === 'en' ? 'English' : 'Deutsch'}</span>
|
||||
<span className="pair-points-pill">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="#C4A85A"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||
+{card.meta?.points ?? 2} Punkte
|
||||
</span>
|
||||
</div>
|
||||
<div className="pair-header-divider" />
|
||||
|
||||
<div className="pair-card-body" onClick={e => e.stopPropagation()}>
|
||||
<div className="pair-card-body" onClick={e => e.stopPropagation()} style={{ paddingTop: 18 }}>
|
||||
<p className="pair-section-label">Frage</p>
|
||||
<p className="pair-question">
|
||||
{resolveSentence(sentence, card.placeholders, handleChipClick, activeChip?.id)}
|
||||
|
||||
Reference in New Issue
Block a user