Skip to content

Commit e667a7d

Browse files
committed
Loading screen
1 parent bc111a6 commit e667a7d

File tree

3 files changed

+140
-19
lines changed

3 files changed

+140
-19
lines changed

mobile/app/_layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export default function RootLayout() {
8181
<Stack.Screen name="party/[code]" options={{ headerShown: false }} />
8282
<Stack.Screen name="friends" options={{ presentation: 'modal' }} />
8383
<Stack.Screen name="history/[gameId]" options={{ presentation: 'modal' }} />
84-
<Stack.Screen name="user/[username]" options={{ headerShown: false, animation: 'slide_from_right' }} />
84+
<Stack.Screen name="user/[username]" options={{ headerShown: false, animation: 'fade' }} />
8585
</Stack>
8686
</SafeAreaProvider>
8787
</GestureHandlerRootView>

mobile/app/game/[id].tsx

Lines changed: 139 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { useState, useEffect, useCallback, useRef } from 'react';
22
import {
33
View,
44
Text,
5+
Image,
6+
ImageBackground,
57
StyleSheet,
68
Pressable,
7-
ActivityIndicator,
89
useWindowDimensions,
910
Animated,
1011
Easing,
@@ -92,7 +93,12 @@ export default function GameScreen() {
9293
// Mount the map eagerly once loading completes to avoid first-touch issues
9394
const [mapMounted, setMapMounted] = useState(false);
9495

96+
// Street view loading state — true = panorama not yet ready
97+
const [streetViewLoaded, setStreetViewLoaded] = useState(false);
98+
const showLoadingBanner = isLoading || !streetViewLoaded;
99+
95100
// Animation values
101+
const loadingOpacity = useRef(new Animated.Value(1)).current;
96102
const mapSlideAnim = useRef(new Animated.Value(0)).current; // 0 = hidden, 1 = shown
97103
const mapBtnsAnim = useRef(new Animated.Value(0)).current; // 0 = hidden, 1 = shown
98104
const bannerSlideAnim = useRef(new Animated.Value(300)).current;
@@ -108,6 +114,50 @@ export default function GameScreen() {
108114
}
109115
}, [isLoading, mapMounted]);
110116

117+
// Ref to prevent the useEffect from snapping opacity when handleNextRound
118+
// is already running a manual fade-in animation
119+
const isManualFadeIn = useRef(false);
120+
121+
// Fade loading banner in/out
122+
const fadeOutTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
123+
useEffect(() => {
124+
if (fadeOutTimer.current) {
125+
clearTimeout(fadeOutTimer.current);
126+
fadeOutTimer.current = null;
127+
}
128+
129+
if (showLoadingBanner) {
130+
// If handleNextRound is running a manual fade-in, don't snap
131+
if (!isManualFadeIn.current) {
132+
loadingOpacity.setValue(1);
133+
}
134+
} else {
135+
isManualFadeIn.current = false;
136+
// Delay before fading out so the StreetView has time to paint its first frame
137+
fadeOutTimer.current = setTimeout(() => {
138+
Animated.timing(loadingOpacity, {
139+
toValue: 0,
140+
duration: 600,
141+
easing: Easing.out(Easing.cubic),
142+
useNativeDriver: true,
143+
}).start();
144+
}, 400);
145+
}
146+
147+
return () => {
148+
if (fadeOutTimer.current) clearTimeout(fadeOutTimer.current);
149+
};
150+
}, [showLoadingBanner]);
151+
152+
// Reset streetViewLoaded when round changes (between rounds)
153+
useEffect(() => {
154+
setStreetViewLoaded(false);
155+
}, [gameState.currentRound]);
156+
157+
const handleStreetViewLoad = useCallback(() => {
158+
setStreetViewLoaded(true);
159+
}, []);
160+
111161
// Animate map slide in/out
112162
useEffect(() => {
113163
if (gameState.isShowingResult) {
@@ -336,14 +386,28 @@ export default function GameScreen() {
336386
return;
337387
}
338388

339-
setGameState((prev) => ({
340-
...prev,
341-
currentRound: prev.currentRound + 1,
342-
isShowingResult: false,
343-
}));
344-
setGuessPosition(null);
345-
setMiniMapShown(false);
346-
roundStartTimeRef.current = Date.now();
389+
// Mark that we're doing a manual fade-in so the useEffect doesn't snap opacity
390+
isManualFadeIn.current = true;
391+
setStreetViewLoaded(false);
392+
393+
// Animate loading banner fade-in OVER the map/end banner
394+
Animated.timing(loadingOpacity, {
395+
toValue: 1,
396+
duration: 400,
397+
easing: Easing.out(Easing.cubic),
398+
useNativeDriver: true,
399+
}).start(() => {
400+
// Only after the banner fully covers the screen, change game state
401+
// (which removes the map/end banner underneath)
402+
setGameState((prev) => ({
403+
...prev,
404+
currentRound: prev.currentRound + 1,
405+
isShowingResult: false,
406+
}));
407+
setGuessPosition(null);
408+
setMiniMapShown(false);
409+
roundStartTimeRef.current = Date.now();
410+
});
347411
}, [gameState, router]);
348412

349413
const handleQuit = () => {
@@ -362,12 +426,22 @@ export default function GameScreen() {
362426
return `${Math.round(km).toLocaleString()} km`;
363427
};
364428

365-
// Loading state
429+
// Loading state — no longer an early return; we render the overlay on top
366430
if (isLoading) {
367431
return (
368-
<View style={[styles.container, styles.centerContent]}>
369-
<ActivityIndicator size="large" color={colors.primary} />
370-
<Text style={styles.loadingText}>Loading game...</Text>
432+
<View style={styles.container}>
433+
<ImageBackground
434+
source={require('../../assets/street2.jpg')}
435+
style={StyleSheet.absoluteFillObject}
436+
resizeMode="cover"
437+
/>
438+
<View style={styles.loadingDarkOverlay} />
439+
<View style={styles.loadingBannerCenter}>
440+
<View style={styles.loadingBannerContent}>
441+
<Image source={require('../../assets/loader.gif')} style={styles.loadingSpinner} />
442+
<Text style={styles.loadingBannerText}>Loading...</Text>
443+
</View>
444+
</View>
371445
</View>
372446
);
373447
}
@@ -395,6 +469,7 @@ export default function GameScreen() {
395469
<StreetViewWebView
396470
lat={currentLocation.lat}
397471
long={currentLocation.long}
472+
onLoad={handleStreetViewLoad}
398473
/>
399474
</View>
400475

@@ -587,11 +662,6 @@ export default function GameScreen() {
587662
{lastGuess.points.toLocaleString()} points
588663
</Text>
589664

590-
{/* Running total */}
591-
{/* <Text style={styles.endBannerTotal}>
592-
Total: {gameState.totalScore.toLocaleString()} / {gameState.totalRounds * 5000}
593-
</Text> */}
594-
595665
{/* Next Round / View Results button */}
596666
<Pressable
597667
onPress={handleNextRound}
@@ -614,6 +684,26 @@ export default function GameScreen() {
614684
</View>
615685
</Animated.View>
616686
)}
687+
688+
{/* ═══ LOADING BANNER OVERLAY — initial load & between rounds ═══ */}
689+
<Animated.View
690+
style={[
691+
styles.loadingBannerOverlay,
692+
{ opacity: loadingOpacity },
693+
]}
694+
pointerEvents={showLoadingBanner ? 'auto' : 'none'}
695+
>
696+
<ImageBackground
697+
source={require('../../assets/street2.jpg')}
698+
style={StyleSheet.absoluteFillObject}
699+
resizeMode="cover"
700+
/>
701+
<View style={styles.loadingDarkOverlay} />
702+
<View style={styles.loadingBannerContent}>
703+
<Image source={require('../../assets/loader.gif')} style={styles.loadingSpinner} />
704+
<Text style={styles.loadingBannerText}>Loading...</Text>
705+
</View>
706+
</Animated.View>
617707
</View>
618708
);
619709
}
@@ -878,4 +968,35 @@ const styles = StyleSheet.create({
878968
fontSize: fontSizes.lg,
879969
fontFamily: 'Lexend-SemiBold',
880970
},
971+
972+
// ── Loading Banner Overlay ─────────────────────────────────
973+
loadingBannerOverlay: {
974+
...StyleSheet.absoluteFillObject,
975+
zIndex: 2000,
976+
justifyContent: 'center',
977+
alignItems: 'center',
978+
},
979+
loadingDarkOverlay: {
980+
...StyleSheet.absoluteFillObject,
981+
backgroundColor: 'rgba(0, 0, 0, 0.6)',
982+
},
983+
loadingBannerCenter: {
984+
flex: 1,
985+
justifyContent: 'center',
986+
alignItems: 'center',
987+
},
988+
loadingBannerContent: {
989+
flexDirection: 'row',
990+
alignItems: 'center',
991+
gap: 12,
992+
},
993+
loadingSpinner: {
994+
width: 80,
995+
height: 80,
996+
},
997+
loadingBannerText: {
998+
color: '#fff',
999+
fontSize: 42,
1000+
fontFamily: 'Lexend-Bold',
1001+
},
8811002
});

mobile/assets/loader.gif

187 KB
Loading

0 commit comments

Comments
 (0)