@@ -2,9 +2,10 @@ import { useState, useEffect, useCallback, useRef } from 'react';
22import {
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} ) ;
0 commit comments