11import React , { useState , useEffect , useMemo } from 'react' ;
2- import { loadAllData } from './services/dataLoader' ;
2+ import { loadAllData , getModelKey } from './services/dataLoader' ;
33import OverviewChart from './components/OverviewChart' ;
44import DetailChart from './components/DetailChart' ;
55import ModelListingsView from './components/ModelListingsView' ;
66import NewListings from './components/NewListings' ;
77import NoTeslaToggle from './components/NoTeslaToggle' ;
88import Footer from './components/Footer' ;
9- import { CATEGORY_TABS , DEFAULT_CATEGORY , filterDataByCategory } from './utils/modelCategories' ;
9+ import { CATEGORY_TABS , DEFAULT_CATEGORY , isModelInCategory } from './utils/modelCategories' ;
1010
1111const TIME_RANGE_OPTIONS = [
1212 { id : '7d' , label : '7 Days' , days : 7 } ,
@@ -23,13 +23,16 @@ function getTimeRangeOption(rangeId) {
2323
2424function App ( ) {
2525 const [ data , setData ] = useState ( [ ] ) ;
26- const [ loading , setLoading ] = useState ( true ) ;
26+ const [ dataLoading , setDataLoading ] = useState ( true ) ;
2727 const [ error , setError ] = useState ( null ) ;
2828 const [ selectedModel , setSelectedModel ] = useState ( null ) ;
2929 const [ noTesla , setNoTesla ] = useState ( false ) ;
3030 const [ selectedDate , setSelectedDate ] = useState ( null ) ;
3131 const [ selectedCategory , setSelectedCategory ] = useState ( DEFAULT_CATEGORY ) ;
3232 const [ timeRangeId , setTimeRangeId ] = useState ( DEFAULT_RANGE_ID ) ;
33+ const [ loadedDays , setLoadedDays ] = useState ( 0 ) ;
34+ const [ categoryDataCache , setCategoryDataCache ] = useState ( { } ) ;
35+ const [ categoryLoadedDays , setCategoryLoadedDays ] = useState ( { } ) ;
3336
3437 const activeRangeOption = useMemo (
3538 ( ) => getTimeRangeOption ( timeRangeId ) ,
@@ -107,70 +110,102 @@ function App() {
107110
108111 useEffect ( ( ) => {
109112 let isMounted = true ;
113+ let urlParamsInitialized = false ;
110114
111- loadAllData ( MAX_TIME_RANGE_DAYS )
112- . then ( results => {
113- if ( ! isMounted ) {
114- return ;
115- }
115+ // Read URL params first
116+ const url = new URL ( window . location ) ;
117+ const rangeParam = url . searchParams . get ( 'range' ) ;
118+ const validRangeIds = TIME_RANGE_OPTIONS . map ( option => option . id ) ;
119+ const initialRangeId = rangeParam && validRangeIds . includes ( rangeParam )
120+ ? rangeParam
121+ : DEFAULT_RANGE_ID ;
122+ const initialRange = TIME_RANGE_OPTIONS . find ( opt => opt . id === initialRangeId ) ;
123+ const daysToLoad = initialRange ?. days || 30 ;
124+
125+ // Get initial category from URL
126+ const categoryParam = url . searchParams . get ( 'category' ) ;
127+ const initialCategory = ( categoryParam && CATEGORY_TABS . some ( tab => tab . id === categoryParam ) )
128+ ? categoryParam
129+ : DEFAULT_CATEGORY ;
130+
131+ // Create filter function for the selected category
132+ const categoryFilter = ( listing ) => {
133+ const modelKey = getModelKey ( listing ) ;
134+ return isModelInCategory ( modelKey , initialCategory ) ;
135+ } ;
116136
117- setData ( results ) ;
118- setLoading ( false ) ;
137+ // Load data progressively, starting with what's needed for the current time range
138+ loadAllData ( daysToLoad , {
139+ batchSize : 7 , // Load 7 days at a time
140+ filterListings : categoryFilter , // Only load listings for selected category
141+ onProgress : ( progressData ) => {
142+ if ( ! isMounted ) return ;
119143
120- const uniqueDates = results . length > 0
121- ? [ ...new Set ( results . map ( d => d . scraped_at . split ( 'T' ) [ 0 ] ) ) ] . sort ( ) . reverse ( )
122- : [ ] ;
123- const mostRecentDate = uniqueDates [ 0 ] || null ;
144+ // Update data as each batch arrives
145+ setData ( progressData ) ;
124146
125- const url = new URL ( window . location ) ;
147+ // Initialize URL params only once when we have data
148+ if ( ! urlParamsInitialized && progressData . length > 0 ) {
149+ urlParamsInitialized = true ;
126150
127- const modelParam = url . searchParams . get ( 'model' ) ;
128- if ( modelParam && modelParam !== 'all' ) {
129- setSelectedModel ( modelParam ) ;
130- }
151+ const uniqueDates = [ ...new Set ( progressData . map ( d => d . scraped_at . split ( 'T' ) [ 0 ] ) ) ] . sort ( ) . reverse ( ) ;
152+ const mostRecentDate = uniqueDates [ 0 ] || null ;
131153
132- const noTeslaParam = url . searchParams . get ( 'noTesla ' ) ;
133- if ( noTeslaParam === 'true ') {
134- setNoTesla ( true ) ;
135- }
154+ const modelParam = url . searchParams . get ( 'model ' ) ;
155+ if ( modelParam && modelParam !== 'all ') {
156+ setSelectedModel ( modelParam ) ;
157+ }
136158
137- const categoryParam = url . searchParams . get ( 'category' ) ;
138- if ( categoryParam && CATEGORY_TABS . some ( tab => tab . id === categoryParam ) ) {
139- setSelectedCategory ( categoryParam ) ;
140- } else {
141- setSelectedCategory ( DEFAULT_CATEGORY ) ;
142- }
159+ const noTeslaParam = url . searchParams . get ( 'noTesla' ) ;
160+ if ( noTeslaParam === 'true' ) {
161+ setNoTesla ( true ) ;
162+ }
143163
144- const rangeParam = url . searchParams . get ( 'range' ) ;
145- const validRangeIds = TIME_RANGE_OPTIONS . map ( option => option . id ) ;
146- const resolvedRangeId = rangeParam && validRangeIds . includes ( rangeParam )
147- ? rangeParam
148- : DEFAULT_RANGE_ID ;
149- setTimeRangeId ( resolvedRangeId ) ;
150-
151- if ( rangeParam && rangeParam !== resolvedRangeId ) {
152- const updatedUrl = new URL ( window . location ) ;
153- if ( resolvedRangeId === DEFAULT_RANGE_ID ) {
154- updatedUrl . searchParams . delete ( 'range' ) ;
155- } else {
156- updatedUrl . searchParams . set ( 'range' , resolvedRangeId ) ;
164+ setSelectedCategory ( initialCategory ) ;
165+
166+ setTimeRangeId ( initialRangeId ) ;
167+
168+ if ( rangeParam && rangeParam !== initialRangeId ) {
169+ const updatedUrl = new URL ( window . location ) ;
170+ if ( initialRangeId === DEFAULT_RANGE_ID ) {
171+ updatedUrl . searchParams . delete ( 'range' ) ;
172+ } else {
173+ updatedUrl . searchParams . set ( 'range' , initialRangeId ) ;
174+ }
175+ window . history . replaceState ( { } , '' , updatedUrl ) ;
157176 }
158- window . history . replaceState ( { } , '' , updatedUrl ) ;
159- }
160177
161- const dateParam = url . searchParams . get ( 'date' ) ;
162- if ( dateParam && uniqueDates . includes ( dateParam ) ) {
163- setSelectedDate ( dateParam ) ;
164- } else if ( mostRecentDate ) {
165- setSelectedDate ( mostRecentDate ) ;
178+ const dateParam = url . searchParams . get ( 'date' ) ;
179+ if ( dateParam && uniqueDates . includes ( dateParam ) ) {
180+ setSelectedDate ( dateParam ) ;
181+ } else if ( mostRecentDate ) {
182+ setSelectedDate ( mostRecentDate ) ;
183+ }
166184 }
185+ }
186+ } )
187+ . then ( results => {
188+ if ( ! isMounted ) return ;
189+
190+ setData ( results ) ;
191+ setDataLoading ( false ) ;
192+ setLoadedDays ( daysToLoad ) ;
193+
194+ // Cache the data for this category
195+ setCategoryDataCache ( prev => ( {
196+ ...prev ,
197+ [ initialCategory ] : results
198+ } ) ) ;
199+ setCategoryLoadedDays ( prev => ( {
200+ ...prev ,
201+ [ initialCategory ] : daysToLoad
202+ } ) ) ;
167203 } )
168204 . catch ( err => {
169- if ( ! isMounted ) {
170- return ;
171- }
205+ if ( ! isMounted ) return ;
206+
172207 setError ( err . message ) ;
173- setLoading ( false ) ;
208+ setDataLoading ( false ) ;
174209 } ) ;
175210
176211 return ( ) => {
@@ -254,6 +289,54 @@ function App() {
254289 url . searchParams . set ( 'category' , categoryId ) ;
255290 }
256291 window . history . pushState ( { } , '' , url ) ;
292+
293+ const newRange = TIME_RANGE_OPTIONS . find ( opt => opt . id === timeRangeId ) ;
294+ const daysNeeded = newRange ?. days || 30 ;
295+ const cachedData = categoryDataCache [ categoryId ] ;
296+ const cachedDays = categoryLoadedDays [ categoryId ] || 0 ;
297+
298+ // If we have cached data for this category and it has enough days, use it
299+ if ( cachedData && cachedDays >= daysNeeded ) {
300+ setData ( cachedData ) ;
301+ setLoadedDays ( cachedDays ) ;
302+ return ;
303+ }
304+
305+ // Otherwise, load data with new category filter
306+ setDataLoading ( true ) ;
307+ setData ( [ ] ) ;
308+
309+ const categoryFilter = ( listing ) => {
310+ const modelKey = getModelKey ( listing ) ;
311+ return isModelInCategory ( modelKey , categoryId ) ;
312+ } ;
313+
314+ loadAllData ( daysNeeded , {
315+ batchSize : 7 ,
316+ filterListings : categoryFilter ,
317+ onProgress : ( progressData ) => {
318+ setData ( progressData ) ;
319+ }
320+ } )
321+ . then ( results => {
322+ setData ( results ) ;
323+ setLoadedDays ( daysNeeded ) ;
324+ setDataLoading ( false ) ;
325+
326+ // Cache the data for this category
327+ setCategoryDataCache ( prev => ( {
328+ ...prev ,
329+ [ categoryId ] : results
330+ } ) ) ;
331+ setCategoryLoadedDays ( prev => ( {
332+ ...prev ,
333+ [ categoryId ] : daysNeeded
334+ } ) ) ;
335+ } )
336+ . catch ( err => {
337+ setError ( err . message ) ;
338+ setDataLoading ( false ) ;
339+ } ) ;
257340 } ;
258341
259342 const handleTimeRangeChange = ( rangeId , { replaceHistory = false } = { } ) => {
@@ -262,6 +345,9 @@ function App() {
262345 return ;
263346 }
264347
348+ const newRange = TIME_RANGE_OPTIONS . find ( opt => opt . id === rangeId ) ;
349+ const daysNeeded = newRange ?. days || 30 ;
350+
265351 setTimeRangeId ( rangeId ) ;
266352
267353 const url = new URL ( window . location ) ;
@@ -271,6 +357,43 @@ function App() {
271357 url . searchParams . set ( 'range' , rangeId ) ;
272358 }
273359 window . history [ replaceHistory ? 'replaceState' : 'pushState' ] ( { } , '' , url ) ;
360+
361+ // Load additional data if needed
362+ if ( daysNeeded > loadedDays ) {
363+ setDataLoading ( true ) ;
364+
365+ const categoryFilter = ( listing ) => {
366+ const modelKey = getModelKey ( listing ) ;
367+ return isModelInCategory ( modelKey , selectedCategory ) ;
368+ } ;
369+
370+ loadAllData ( daysNeeded , {
371+ batchSize : 7 ,
372+ filterListings : categoryFilter ,
373+ onProgress : ( progressData ) => {
374+ setData ( progressData ) ;
375+ }
376+ } )
377+ . then ( results => {
378+ setData ( results ) ;
379+ setLoadedDays ( daysNeeded ) ;
380+ setDataLoading ( false ) ;
381+
382+ // Update cache for current category
383+ setCategoryDataCache ( prev => ( {
384+ ...prev ,
385+ [ selectedCategory ] : results
386+ } ) ) ;
387+ setCategoryLoadedDays ( prev => ( {
388+ ...prev ,
389+ [ selectedCategory ] : daysNeeded
390+ } ) ) ;
391+ } )
392+ . catch ( err => {
393+ setError ( err . message ) ;
394+ setDataLoading ( false ) ;
395+ } ) ;
396+ }
274397 } ;
275398
276399 const handleDateSelect = ( date , { replaceHistory = false , force = false } = { } ) => {
@@ -323,16 +446,8 @@ function App() {
323446 } ) ;
324447 } , [ data , rangeDateLabels ] ) ;
325448
326- if ( loading ) {
327- return < div className = "loading" > Loading price data...</ div > ;
328- }
329-
330- if ( error ) {
331- return < div className = "error" > Error: { error } </ div > ;
332- }
333-
449+ // Data is already filtered by category at load time, just need to filter Tesla
334450 const filteredData = filterTesla ( dateFilteredData ) ;
335- const categoryFilteredData = filterDataByCategory ( filteredData , selectedCategory ) ;
336451
337452 const activeCategory = CATEGORY_TABS . find ( tab => tab . id === selectedCategory ) || CATEGORY_TABS [ 0 ] || null ;
338453 const categoryDescription = activeCategory ?. description ?? '' ;
@@ -368,10 +483,12 @@ function App() {
368483 ) }
369484 </ header >
370485 < main className = "container" >
371- { ! selectedModel ? (
486+ { error ? (
487+ < div className = "error" > Error: { error } </ div >
488+ ) : ! selectedModel ? (
372489 < >
373490 < OverviewChart
374- data = { categoryFilteredData }
491+ data = { filteredData }
375492 onModelSelect = { handleModelSelect }
376493 onDateSelect = { handleDateSelect }
377494 selectedDate = { selectedDate }
@@ -380,8 +497,9 @@ function App() {
380497 timeRangeOptions = { TIME_RANGE_OPTIONS }
381498 dateLabels = { rangeDateLabels }
382499 availableDates = { availableRangeDates }
500+ loading = { dataLoading }
383501 />
384- < NewListings data = { categoryFilteredData } selectedDate = { selectedDate } />
502+ < NewListings data = { filteredData } selectedDate = { selectedDate } loading = { dataLoading } />
385503 </ >
386504 ) : (
387505 < >
@@ -400,11 +518,13 @@ function App() {
400518 timeRangeOptions = { TIME_RANGE_OPTIONS }
401519 dateLabels = { rangeDateLabels }
402520 availableDates = { availableRangeDates }
521+ loading = { dataLoading }
403522 />
404523 < ModelListingsView
405524 data = { filteredData }
406525 model = { selectedModel }
407526 selectedDate = { selectedDate }
527+ loading = { dataLoading }
408528 />
409529 </ >
410530 ) }
0 commit comments