5151
5252 .mapbox-field {
5353 width : 100% ;
54+ max-width : 100% ;
5455 border : 1px solid # cfd5e6 ;
5556 border-radius : 10px ;
5657 padding : 14px 12px ;
8586 }
8687 .mapbox-suggestions button : hover { background : # f2f5ff ; }
8788
88- .schedule-control { position : relative; }
89- .schedule-input { padding-left : 60px ; }
89+ .schedule-control { position : relative; width : 100 % ; }
90+ .schedule-input { padding-left : 60px ; width : 100 % ; display : block; }
9091 .swap-inline {
9192 position : absolute;
9293 left : 12px ;
119120 text-align : center;
120121 }
121122 .metric .k { font-size : 13px ;color : # FFFFFF ; margin-bottom : 6px ; }
122- .metric .v { font-size : 38px ;font-weight : 800 ;color : # FFFFFF ; font-family : "JetBrains Mono" , "Source Code Pro" , monospace; }
123+ .metric .v { font-size : 38px ;font-weight : 800 ;color : # FFFFFF ; font-family : "JetBrains Mono" , "Source Code Pro" , monospace; transition : filter .2s ease, opacity .2s ease; }
124+ .metric .v .loading { filter : blur (3px ); opacity : 0.5 ; }
123125 .metric .note {
124126 margin-top : 8px ;
125127 font-size : 15px ;
@@ -258,6 +260,7 @@ <h1>ELR Fare Estimate</h1>
258260const MIN_QUERY_LENGTH = 4 ;
259261const DEBOUNCE_MS = 350 ;
260262const CACHE_TTL_MS = 24 * 60 * 60 * 1000 ; // 24h
263+ const RECENT_LIMIT = 6 ;
261264
262265const timestampEl = document . getElementById ( "timestamp" ) ;
263266const distanceValue = document . getElementById ( "distanceValue" ) ;
@@ -389,6 +392,28 @@ <h1>ELR Fare Estimate</h1>
389392 try { localStorage . setItem ( key , JSON . stringify ( { ts :Date . now ( ) , data} ) ) ; } catch { }
390393}
391394
395+ // Recents helpers
396+ function loadRecents ( ) {
397+ try {
398+ const raw = localStorage . getItem ( "recentPlaces" ) ;
399+ if ( ! raw ) return [ ] ;
400+ const arr = JSON . parse ( raw ) ;
401+ if ( ! Array . isArray ( arr ) ) return [ ] ;
402+ const now = Date . now ( ) ;
403+ const fresh = arr . filter ( item => item ?. ts && now - item . ts < CACHE_TTL_MS && item . label && item . coords ) ;
404+ return fresh . slice ( 0 , RECENT_LIMIT ) ;
405+ } catch { return [ ] ; }
406+ }
407+ function saveRecent ( entry ) {
408+ try {
409+ const recents = loadRecents ( ) ;
410+ const filtered = recents . filter ( r => ! ( r . mapboxId && entry . mapboxId && r . mapboxId === entry . mapboxId ) && r . label !== entry . label ) ;
411+ filtered . unshift ( { ...entry , ts :Date . now ( ) } ) ;
412+ const trimmed = filtered . slice ( 0 , RECENT_LIMIT ) ;
413+ localStorage . setItem ( "recentPlaces" , JSON . stringify ( trimmed ) ) ;
414+ } catch { }
415+ }
416+
392417// Debounce helper
393418function debounce ( fn , delay ) {
394419 let t ;
@@ -448,6 +473,9 @@ <h1>ELR Fare Estimate</h1>
448473}
449474function setSelectedPlace ( key , data ) {
450475 selectedPlaces [ key ] = data ;
476+ if ( data ?. coords && data ?. label ) {
477+ saveRecent ( { label :data . label , mapboxId :data . mapboxId || null , coords :data . coords } ) ;
478+ }
451479 updateDistanceUI ( ) ;
452480 maybeComputeDistance ( ) ;
453481 updateSwapState ( ) ;
@@ -501,10 +529,11 @@ <h1>ELR Fare Estimate</h1>
501529 price . textContent = "$0" ;
502530 distanceValue . textContent = "--" ;
503531 distanceSuffix . textContent = opts . error ;
532+ price . classList . remove ( "loading" ) ;
504533 return ;
505534 }
506535 if ( opts . loading ) {
507- price . textContent = "..." ;
536+ price . classList . add ( "loading" ) ;
508537 distanceValue . textContent = "..." ;
509538 distanceSuffix . textContent = "Waiting for distance to estimate fare." ;
510539 return ;
@@ -513,11 +542,13 @@ <h1>ELR Fare Estimate</h1>
513542 const fare = calcFare ( opts . miles ) ;
514543 price . textContent = `$${ fare } ` ;
515544 distanceSuffix . textContent = "miles driving" ;
545+ price . classList . remove ( "loading" ) ;
516546 return ;
517547 }
518548 price . textContent = "$0" ;
519549 distanceValue . textContent = "--" ;
520550 distanceSuffix . textContent = "miles driving" ;
551+ price . classList . remove ( "loading" ) ;
521552}
522553
523554async function fetchMapboxSuggestions ( q ) {
@@ -639,9 +670,31 @@ <h1>ELR Fare Estimate</h1>
639670
640671function clearResults ( field ) { field . results . innerHTML = "" ; }
641672
673+ function renderRecents ( field ) {
674+ const recents = loadRecents ( ) ;
675+ if ( ! recents . length ) { clearResults ( field ) ; return ; }
676+ const ul = document . createElement ( "ul" ) ;
677+ recents . forEach ( item => {
678+ const btn = document . createElement ( "button" ) ;
679+ btn . innerHTML = `<span aria-hidden="true" style="margin-right:8px;">⏱</span>${ item . label } ` ;
680+ btn . type = "button" ;
681+ btn . addEventListener ( "click" , ( ) => {
682+ showChip ( field , item . label ) ;
683+ clearResults ( field ) ;
684+ setSelectedPlace ( field . key , { label :item . label , mapboxId :item . mapboxId || null , coords :item . coords } ) ;
685+ field . input . focus ( ) ;
686+ } ) ;
687+ const li = document . createElement ( "li" ) ;
688+ li . appendChild ( btn ) ;
689+ ul . appendChild ( li ) ;
690+ } ) ;
691+ field . results . innerHTML = "" ;
692+ field . results . appendChild ( ul ) ;
693+ }
694+
642695function handleInput ( field ) {
643696 const q = field . input . value . trim ( ) ;
644- if ( ! q ) { clearResults ( field ) ; return ; }
697+ if ( ! q ) { renderRecents ( field ) ; return ; }
645698 if ( q . length < MIN_QUERY_LENGTH ) { clearResults ( field ) ; return ; }
646699 hideChip ( field ) ;
647700 fetchMapboxSuggestions ( q )
@@ -652,6 +705,7 @@ <h1>ELR Fare Estimate</h1>
652705Object . values ( mapboxFields ) . forEach ( field => {
653706 const debounced = debounce ( ( ) => handleInput ( field ) , DEBOUNCE_MS ) ;
654707 field . input . addEventListener ( "input" , debounced ) ;
708+ field . input . addEventListener ( "focus" , ( ) => { if ( ! field . input . value . trim ( ) ) renderRecents ( field ) ; } ) ;
655709 field . chipClear . addEventListener ( "click" , ( ) => {
656710 field . input . value = "" ;
657711 hideChip ( field ) ;
0 commit comments