chahinebrini 5d6c322129 wip: KeyboardAwareSheet migrations + Snake/Tetris UI + iron.png + useMe live-update
Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow
mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail.
GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift.

- KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding
- react-native-keyboard-controller installiert + KeyboardProvider in Root
- Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO)
- Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn
- DPad-Buttons 60→48, more bg, no scale
- useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates
- dm.tsx: resolveAvatar wrap (iron.png-Warning)
- Mail-error-humanizer + locales

Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:59:25 +02:00

108 lines
3.2 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { Animated, View, Text } from 'react-native';
/**
* Animierter Progress-Bar: aktueller Score vs. persönlicher Rekord.
*
* - Bar-Breite animiert zu `min(score / max(best, 1), 1) * 100%`
* - Bei `isNewBest=true`: Celebration-Animation (Gold-Pulse + Scale-Bounce + 🏆-Label)
* - Position direkt unter `<DigitalScore />` im Game-Layout
*
* Reusable für Snake / Tetris / Memory — pro Spiel den passenden `score`/`best`
* reinreichen. Optional `boardWidth` damit die Bar exakt das Board-Edge matcht.
*/
export interface ScoreProgressBarProps {
score: number;
best: number;
isNewBest: boolean;
boardWidth: number;
}
export function ScoreProgressBar({ score, best, isNewBest, boardWidth }: ScoreProgressBarProps) {
const widthAnim = useRef(new Animated.Value(0)).current;
const celebrationAnim = useRef(new Animated.Value(0)).current;
// Bar-Breite zum aktuellen Score-Verhältnis
useEffect(() => {
const target = best > 0 ? Math.min(score / best, 1) : score > 0 ? 1 : 0;
Animated.timing(widthAnim, {
toValue: target,
duration: 280,
useNativeDriver: false,
}).start();
}, [score, best, widthAnim]);
// Celebration-Pulse bei neuem Rekord
useEffect(() => {
if (!isNewBest) {
celebrationAnim.setValue(0);
return;
}
Animated.sequence([
Animated.timing(celebrationAnim, { toValue: 1, duration: 280, useNativeDriver: false }),
Animated.timing(celebrationAnim, { toValue: 0, duration: 600, useNativeDriver: false }),
]).start();
}, [isNewBest, celebrationAnim]);
const widthInterp = widthAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
});
// Bar-Color: idle blau, beim Celebration-Pulse → gold
const barColor = celebrationAnim.interpolate({
inputRange: [0, 1],
outputRange: ['#007AFF', '#FFD60A'],
});
// Container leicht hochskalieren bei Celebration
const containerScale = celebrationAnim.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.04],
});
return (
<Animated.View
style={{
width: boardWidth,
alignSelf: 'center',
marginBottom: 6,
transform: [{ scale: containerScale }],
}}
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 3 }}>
<Text style={{ fontSize: 9, color: '#6b7280', letterSpacing: 1, fontFamily: 'Nunito_600SemiBold', textTransform: 'uppercase' }}>
{isNewBest ? '🏆 NEW RECORD' : 'PROGRESS'}
</Text>
<Text
style={{
fontSize: 9,
color: isNewBest ? '#b8860b' : '#6b7280',
fontFamily: 'Nunito_700Bold',
letterSpacing: 0.5,
}}
>
{score} / {Math.max(best, score)}
</Text>
</View>
<View
style={{
height: 6,
borderRadius: 3,
backgroundColor: '#1f2937',
overflow: 'hidden',
}}
>
<Animated.View
style={{
height: '100%',
width: widthInterp,
backgroundColor: barColor,
borderRadius: 3,
}}
/>
</View>
</Animated.View>
);
}