From 964dc2b6e0c942dc77edb643f2bfefa4bf7df2c7 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Sat, 16 May 2026 00:44:44 +0200 Subject: [PATCH] =?UTF-8?q?fix(native/games):=20game-over=20modal=20?= =?UTF-8?q?=E2=80=94=20maxHeight=2085%,=20KeyboardAvoidingView,=20Button?= =?UTF-8?q?=20comp,=20regenerate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four issues from the screenshot review plus one new affordance: 1. Modal overflowing on small devices — capped at maxHeight: '85%'. Header (handle bar + Lyra avatar + title + subtitle) stays fixed above a ScrollView body; action buttons stay fixed below with a border separator. Stat cards, star rating, and TextInput now live inside the scrollable body. 2. Keyboard pushed the TextInput out of sight — replaced the bespoke Keyboard.addListener + Animated.multiply lift hack (Easing, keyboardLiftY, the whole apparatus) with a plain KeyboardAvoidingView wrapper (behavior="padding" iOS / "height" Android). ScrollView already had keyboardShouldPersistTaps="handled" so taps on Posten/Abbrechen still work while the keyboard is up. 3. All four action buttons (Nochmal, Beenden, Abbrechen, Posten) plus the inner Save-Rating CTA now route through components/Button.tsx — picks up the slimmer paddingVertical:12 default from the central component. Posten gets the paper-plane icon. Nochmal + Posten = primary, Beenden + Abbrechen = secondary. 4. New "Neuer Vorschlag" regenerate button (ghost variant, sm size, refresh-outline icon) sits between the TextInput and the Abbrechen/ Posten row. Reuses POST /api/games/share-text — no new endpoint. Tracks the last Lyra-generated text in a ref so we can detect user edits; if the user has modified the suggestion, taps go through an Alert.alert confirm before overwrite. Spinner during the regen call, Posten / Abbrechen stay active. i18n keys gameOver.regen_* across DE/EN/FR. --- .../components/games/GameOverScreen.tsx | 304 ++++++++---------- apps/rebreak-native/locales/de.json | 6 +- apps/rebreak-native/locales/en.json | 6 +- apps/rebreak-native/locales/fr.json | 6 +- 4 files changed, 152 insertions(+), 170 deletions(-) diff --git a/apps/rebreak-native/components/games/GameOverScreen.tsx b/apps/rebreak-native/components/games/GameOverScreen.tsx index cc7c9d8..42b297f 100644 --- a/apps/rebreak-native/components/games/GameOverScreen.tsx +++ b/apps/rebreak-native/components/games/GameOverScreen.tsx @@ -1,9 +1,10 @@ import { useEffect, useRef, useState } from 'react'; import { ActivityIndicator, + Alert, Animated, Easing, - Keyboard, + KeyboardAvoidingView, Modal, Platform, ScrollView, @@ -18,6 +19,7 @@ import * as Haptics from 'expo-haptics'; import { Ionicons } from '@expo/vector-icons'; import { RiveAvatar } from '../RiveAvatar'; import { StarRating } from './StarRating'; +import { Button } from '../Button'; import { useColors } from '../../lib/theme'; import { apiFetch } from '../../lib/api'; @@ -59,36 +61,7 @@ export function GameOverScreen({ const colors = useColors(); const insets = useSafeAreaInsets(); - // Slide-In Spring für den Sheet-Auftritt (eigene Bouncy-Animation behalten) const slideAnim = useRef(new Animated.Value(500)).current; - // Keyboard-Lift via plain RN Keyboard.addListener (funktioniert in Modals, - // anders als react-native-keyboard-controller's useKeyboardAnimation). - const keyboardLift = useRef(new Animated.Value(0)).current; - - useEffect(() => { - const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; - const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; - const showSub = Keyboard.addListener(showEvent, (e) => { - Animated.timing(keyboardLift, { - toValue: e.endCoordinates.height, - duration: Platform.OS === 'ios' ? (e.duration ?? 250) : 220, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }).start(); - }); - const hideSub = Keyboard.addListener(hideEvent, (e) => { - Animated.timing(keyboardLift, { - toValue: 0, - duration: Platform.OS === 'ios' ? (e?.duration ?? 250) : 220, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }).start(); - }); - return () => { - showSub.remove(); - hideSub.remove(); - }; - }, [keyboardLift]); const [rating, setRating] = useState(0); const [feedback, setFeedback] = useState(''); @@ -97,7 +70,9 @@ export function GameOverScreen({ const [shareSectionOpen, setShareSectionOpen] = useState(false); const [shareText, setShareText] = useState(''); + const lyraShareTextRef = useRef(''); const [shareTextLoading, setShareTextLoading] = useState(false); + const [regenLoading, setRegenLoading] = useState(false); const [sharing, setSharing] = useState(false); const [posted, setPosted] = useState(false); const [postError, setPostError] = useState(false); @@ -118,14 +93,12 @@ export function GameOverScreen({ }).start(); }, []); - // Negativer Lift — translateY -keyboardHeight schiebt Sheet nach oben. - const keyboardLiftY = Animated.multiply(keyboardLift, -1); - function handleExit() { Animated.timing(slideAnim, { toValue: 500, duration: 220, useNativeDriver: true, + easing: Easing.in(Easing.cubic), }).start(() => onExit()); } @@ -149,29 +122,69 @@ export function GameOverScreen({ } } + async function fetchShareText() { + const data = await apiFetch<{ text: string }>('/api/games/share-text', { + method: 'POST', + body: { + gameName: gameName.toLowerCase(), + score, + scoreLabel, + bestScore, + isNewRecord: score > bestScore, + mode: 'game', + }, + }); + return data.text || `${gameName}: ${score} ${scoreLabel ?? 'Punkte'}\n${t('gameOver.share_challenge')}`; + } + async function openShareSection() { setShareTextLoading(true); setShareSectionOpen(true); try { - const data = await apiFetch<{ text: string }>('/api/games/share-text', { - method: 'POST', - body: { - gameName: gameName.toLowerCase(), - score, - scoreLabel, - bestScore, - isNewRecord: score > bestScore, - mode: 'game', - }, - }); - setShareText(data.text || `${gameName}: ${score} ${scoreLabel ?? 'Punkte'}\n${t('gameOver.share_challenge')}`); + const text = await fetchShareText(); + lyraShareTextRef.current = text; + setShareText(text); } catch { - setShareText(`${gameName}: ${score} ${scoreLabel ?? 'Punkte'}\n${t('gameOver.share_challenge')}`); + const fallback = `${gameName}: ${score} ${scoreLabel ?? 'Punkte'}\n${t('gameOver.share_challenge')}`; + lyraShareTextRef.current = fallback; + setShareText(fallback); } finally { setShareTextLoading(false); } } + function regenerateShareText() { + if (regenLoading || shareTextLoading) return; + + const doRegen = async () => { + setRegenLoading(true); + try { + const text = await fetchShareText(); + lyraShareTextRef.current = text; + setShareText(text); + } catch { + // keep existing text on error + } finally { + setRegenLoading(false); + } + }; + + const userModified = shareText.trim() !== lyraShareTextRef.current.trim(); + + if (userModified) { + Alert.alert( + t('gameOver.regen_confirm_title'), + t('gameOver.regen_confirm_body'), + [ + { text: t('common.cancel'), style: 'cancel' }, + { text: t('gameOver.regen_confirm_ok'), onPress: doRegen }, + ] + ); + } else { + doRegen(); + } + } + async function submitCommunityPost() { if (!shareText.trim()) return; setSharing(true); @@ -202,20 +215,19 @@ export function GameOverScreen({ return ( - + {/* Grab-handle */} @@ -231,10 +243,11 @@ export function GameOverScreen({ }} /> + {/* Scrollable body */} {/* Lyra avatar + message */} @@ -319,77 +332,16 @@ export function GameOverScreen({ textAlignVertical: 'top', }} /> - - - {saving ? t('common.loading') : t('gameOver.save_rating')} - - + loading={saving} + variant="primary" + size="md" + /> ) : null} - {/* Primary action row */} - - { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {}); - onRetry(); - }} - activeOpacity={0.85} - style={{ - flex: 1, - backgroundColor: '#007AFF', - borderRadius: 12, - minHeight: 40, - paddingVertical: 10, - paddingHorizontal: 16, - alignItems: 'center', - justifyContent: 'center', - }} - > - - {t('gameOver.retry')} - - - - { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {}); - handleExit(); - }} - activeOpacity={0.75} - style={{ - flex: 1, - backgroundColor: '#e5e7eb', - borderRadius: 12, - minHeight: 40, - paddingVertical: 14, - paddingHorizontal: 20, - alignItems: 'center', - justifyContent: 'center', - borderWidth: 1, - borderColor: 'rgba(0,0,0,0.08)', - }} - > - - {t('gameOver.exit')} - - - - {/* Share section */} {posted ? ( @@ -439,6 +391,21 @@ export function GameOverScreen({ /> )} + {/* Regenerate suggestion button */} + {!shareTextLoading ? ( + +