Files
breakpilot-lehrer/studio-v2/components/gamification/CoinAnimation.tsx
Benjamin Admin 9dddd80d7a
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 37s
CI / test-go-edu-search (push) Successful in 45s
CI / test-python-agent-core (push) Has been cancelled
CI / test-nodejs-website (push) Has been cancelled
CI / test-python-klausur (push) Has started running
Add Phases 3.2-4.3: STT, stories, syllables, gamification
Phase 3.2 — MicrophoneInput.tsx: Browser Web Speech API for
speech-to-text recognition (EN+DE), integrated for pronunciation practice.

Phase 4.1 — Story Generator: LLM-powered mini-stories using vocabulary
words, with highlighted vocab in HTML output. Backend endpoint
POST /learning-units/{id}/generate-story + frontend /learn/[unitId]/story.

Phase 4.2 — SyllableBow.tsx: SVG arc component for syllable visualization
under words, clickable for per-syllable TTS.

Phase 4.3 — Gamification system:
- CoinAnimation.tsx: Floating coin rewards with accumulator
- CrownBadge.tsx: Crown/medal display for milestones
- ProgressRing.tsx: Circular progress indicator
- progress_api.py: Backend tracking coins, crowns, streaks per unit

Also adds "Geschichte" exercise type button to UnitCard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:22:52 +02:00

92 lines
2.5 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
interface CoinAnimationProps {
amount: number
trigger: number // increment to trigger animation
}
interface FloatingCoin {
id: number
x: number
delay: number
}
export function CoinAnimation({ amount, trigger }: CoinAnimationProps) {
const [coins, setCoins] = useState<FloatingCoin[]>([])
const [total, setTotal] = useState(0)
const [showBounce, setShowBounce] = useState(false)
useEffect(() => {
if (trigger === 0) return
// Create floating coins
const newCoins: FloatingCoin[] = Array.from({ length: Math.min(amount, 5) }, (_, i) => ({
id: Date.now() + i,
x: Math.random() * 60 - 30,
delay: i * 100,
}))
setCoins(newCoins)
setShowBounce(true)
// Update total after animation
setTimeout(() => {
setTotal((prev) => prev + amount)
setShowBounce(false)
}, 800)
// Clean up coins
setTimeout(() => setCoins([]), 1500)
}, [trigger, amount])
return (
<div className="relative inline-flex items-center gap-1.5">
{/* Coin icon + total */}
<div className={`flex items-center gap-1 px-3 py-1 rounded-full bg-yellow-500/20 border border-yellow-500/30 transition-transform ${showBounce ? 'scale-110' : 'scale-100'}`}>
<span className="text-yellow-400 text-sm">&#x1FA99;</span>
<span className="text-yellow-300 text-sm font-bold tabular-nums">{total}</span>
</div>
{/* Floating coins animation */}
{coins.map((coin) => (
<span
key={coin.id}
className="absolute text-lg animate-coin-float pointer-events-none"
style={{
left: `calc(50% + ${coin.x}px)`,
animationDelay: `${coin.delay}ms`,
}}
>
&#x1FA99;
</span>
))}
<style>{`
@keyframes coin-float {
0% { transform: translateY(0) scale(1); opacity: 1; }
100% { transform: translateY(-60px) scale(0.5); opacity: 0; }
}
.animate-coin-float {
animation: coin-float 1s ease-out forwards;
}
`}</style>
</div>
)
}
/** Hook to manage coin rewards */
export function useCoinRewards() {
const [totalCoins, setTotalCoins] = useState(0)
const [triggerCount, setTriggerCount] = useState(0)
const [lastReward, setLastReward] = useState(0)
const awardCoins = useCallback((amount: number) => {
setLastReward(amount)
setTriggerCount((c) => c + 1)
setTotalCoins((t) => t + amount)
}, [])
return { totalCoins, triggerCount, lastReward, awardCoins }
}