11import '../App.css'
2- import { useCallback , useEffect , useMemo , useState } from 'react'
2+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
33import { Link } from 'react-router-dom'
44
55import MapView from '../MapView'
6- import type { CompareItem } from '../api'
7- import { deleteListing , fetchCompare , reverseGeocode , upsertTarget } from '../api'
6+ import type { CompareItem , ListingSummary } from '../api'
7+ import {
8+ deleteListing ,
9+ fetchCompare ,
10+ fetchListingsSummary ,
11+ reverseGeocode ,
12+ upsertTarget ,
13+ } from '../api'
814
915type SortKey = 'distance' | 'price'
1016type TargetLocationMode = 'address' | 'coords'
@@ -108,6 +114,9 @@ function App() {
108114 const [ loading , setLoading ] = useState ( false )
109115 const [ error , setError ] = useState < string | null > ( null )
110116
117+ const lastListingSummaryRef = useRef < ListingSummary | null > ( null )
118+ const isAutoRefreshingRef = useRef ( false )
119+
111120 useEffect ( ( ) => {
112121 localStorage . setItem ( 'easyrelocate_target_location_mode' , targetLocationMode )
113122 } , [ targetLocationMode ] )
@@ -152,19 +161,22 @@ function App() {
152161 } , [ selectedItem ] )
153162
154163 const refresh = useCallback (
155- async ( opts ?: { nextTargetId ?: string | null } ) => {
164+ async ( opts ?: { nextTargetId ?: string | null ; silent ?: boolean } ) => {
156165 const id = opts ?. nextTargetId ?? targetId
157166 if ( ! id ) return
158- setLoading ( true )
159- setError ( null )
167+ const silent = opts ?. silent ?? false
168+ if ( ! silent ) {
169+ setLoading ( true )
170+ setError ( null )
171+ }
160172 try {
161173 const res = await fetchCompare ( id )
162174 setTarget ( res . target )
163175 setCompareItems ( res . items )
164176 } catch ( e ) {
165- setError ( e instanceof Error ? e . message : String ( e ) )
177+ if ( ! silent ) setError ( e instanceof Error ? e . message : String ( e ) )
166178 } finally {
167- setLoading ( false )
179+ if ( ! silent ) setLoading ( false )
168180 }
169181 } ,
170182 [ targetId ] ,
@@ -175,6 +187,56 @@ function App() {
175187 void refresh ( )
176188 } , [ refresh , targetId ] )
177189
190+ useEffect ( ( ) => {
191+ if ( ! targetId ) return
192+ let cancelled = false
193+
194+ const checkForNewListings = async ( opts ?: { force ?: boolean } ) => {
195+ if ( cancelled ) return
196+ if ( ! opts ?. force && document . hidden ) return
197+ try {
198+ const summary = await fetchListingsSummary ( )
199+ if ( cancelled ) return
200+
201+ const prev = lastListingSummaryRef . current
202+ lastListingSummaryRef . current = summary
203+ const changed =
204+ prev != null &&
205+ ( prev . count !== summary . count ||
206+ prev . latest_id !== summary . latest_id ||
207+ prev . latest_captured_at !== summary . latest_captured_at )
208+
209+ if ( changed && ! isAutoRefreshingRef . current ) {
210+ isAutoRefreshingRef . current = true
211+ try {
212+ await refresh ( { nextTargetId : targetId , silent : true } )
213+ } finally {
214+ isAutoRefreshingRef . current = false
215+ }
216+ }
217+ } catch {
218+ // ignore (offline / backend down)
219+ }
220+ }
221+
222+ void checkForNewListings ( { force : true } )
223+
224+ const interval = window . setInterval ( ( ) => {
225+ void checkForNewListings ( )
226+ } , 7000 )
227+
228+ const onVisibilityChange = ( ) => {
229+ if ( ! document . hidden ) void checkForNewListings ( { force : true } )
230+ }
231+ document . addEventListener ( 'visibilitychange' , onVisibilityChange )
232+
233+ return ( ) => {
234+ cancelled = true
235+ window . clearInterval ( interval )
236+ document . removeEventListener ( 'visibilitychange' , onVisibilityChange )
237+ }
238+ } , [ refresh , targetId ] )
239+
178240 const onSaveTarget = async ( ) => {
179241 const address = targetAddress . trim ( )
180242 const lat = parseNumberOrNull ( targetLat )
@@ -315,6 +377,14 @@ function App() {
315377 } , [ filteredAndSorted ] )
316378
317379 const fitKey = target ? `${ target . id } :${ target . updated_at } ` : 'no-target'
380+ const initialCenter = useMemo ( ( ) => {
381+ if ( target ) return { lat : target . lat , lng : target . lng }
382+ const lat = parseNumberOrNull ( targetLat )
383+ const lng = parseNumberOrNull ( targetLng )
384+ if ( lat == null || lng == null ) return null
385+ if ( ! isWithinUsBounds ( lat , lng ) ) return null
386+ return { lat, lng }
387+ } , [ target , targetLat , targetLng ] )
318388
319389 return (
320390 < div className = "app" >
@@ -665,6 +735,7 @@ function App() {
665735 < main className = "mapWrap" >
666736 < MapView
667737 target = { target }
738+ initialCenter = { initialCenter }
668739 items = { filteredAndSorted }
669740 selectedListingId = { selectedListingId }
670741 onSelectListingId = { setSelectedListingId }
0 commit comments