3.2.1.1
Recent locations save
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260629-150638_cda0606c7e7a865d": 1782745598290
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260629-150638_cda0606c7e7a865d": "auto"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260629-150638_cda0606c7e7a865d": "新的会话"
|
||||||
|
}
|
||||||
@@ -1122,6 +1122,57 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Recent locations — quick-click stacked list beneath the last-update line. */
|
||||||
|
.utci-recent-locs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 3px;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.utci-recent-label {
|
||||||
|
font-family: Manrope, sans-serif;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #b09870;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
.utci-recent-chip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 220px;
|
||||||
|
padding: 4px 10px 4px 8px;
|
||||||
|
font-family: Manrope, sans-serif;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
color: #7a5e2a;
|
||||||
|
background: #fdf8ee;
|
||||||
|
border: 1.5px solid #d9c39b;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
.utci-recent-chip:hover {
|
||||||
|
background: #f6ecd6;
|
||||||
|
border-color: #c8922a;
|
||||||
|
color: #6a4f1e;
|
||||||
|
}
|
||||||
|
/* Keep long names on one line with an ellipsis rather than wrapping. */
|
||||||
|
.utci-recent-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.utci-recent-pin {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #c8922a;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 10. LEGEND (thermal-stress band colours at the bottom) ────────── */
|
/* ── 10. LEGEND (thermal-stress band colours at the bottom) ────────── */
|
||||||
|
|
||||||
.utci-legend {
|
.utci-legend {
|
||||||
@@ -1306,6 +1357,13 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.utci-recent-locs {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.utci-recent-chip {
|
||||||
|
max-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
.utci-search-wrap {
|
.utci-search-wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-1
@@ -175,7 +175,7 @@ export function UTCIForecast() {
|
|||||||
// All useState, useEffect, useCallback and useRef logic lives in
|
// All useState, useEffect, useCallback and useRef logic lives in
|
||||||
// useAppState. See hooks/useAppState.js for the full reading order.
|
// useAppState. See hooks/useAppState.js for the full reading order.
|
||||||
const {
|
const {
|
||||||
location, setLocationAndSave,
|
location, setLocationAndSave, recentLocations,
|
||||||
forecast, airQuality, loading, error, now, fetchedAt,
|
forecast, airQuality, loading, error, now, fetchedAt,
|
||||||
searchQuery, setSearchQuery, searchResults, setSearchResults, searching,
|
searchQuery, setSearchQuery, searchResults, setSearchResults, searching,
|
||||||
selectedDay, setSelectedDay,
|
selectedDay, setSelectedDay,
|
||||||
@@ -675,6 +675,35 @@ export function UTCIForecast() {
|
|||||||
<div class=${'utci-fetch-time' + (isStale ? ' is-stale' : '')}>
|
<div class=${'utci-fetch-time' + (isStale ? ' is-stale' : '')}>
|
||||||
${isStale ? html`<span class="stale-dot" title="Data is older than expected - retrying">● </span>` : ''}Last update: ${fetchedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
${isStale ? html`<span class="stale-dot" title="Data is older than expected - retrying">● </span>` : ''}Last update: ${fetchedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
</div>`}
|
</div>`}
|
||||||
|
${(() => {
|
||||||
|
const curKey = `${location.lat.toFixed(3)},${location.lon.toFixed(3)}`;
|
||||||
|
const recents = (recentLocations || [])
|
||||||
|
.filter((l) => `${l.lat.toFixed(3)},${l.lon.toFixed(3)}` !== curKey)
|
||||||
|
.slice(0, 3);
|
||||||
|
if (!recents.length) return null;
|
||||||
|
return html`
|
||||||
|
<div class="utci-recent-locs">
|
||||||
|
<span class="utci-recent-label">Recent</span>
|
||||||
|
${recents.map((l) => html`
|
||||||
|
<button
|
||||||
|
key=${`${l.lat.toFixed(3)}-${l.lon.toFixed(3)}`}
|
||||||
|
type="button"
|
||||||
|
class="utci-recent-chip"
|
||||||
|
title=${`See the forecast for ${l.name}`}
|
||||||
|
onClick=${() => {
|
||||||
|
setLocationAndSave(l);
|
||||||
|
setSelectedDay(0);
|
||||||
|
closeSearch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg class="utci-recent-pin" viewBox="0 0 24 24" width="11" height="11" aria-hidden="true">
|
||||||
|
<path fill="currentColor" d="M12 2c-3.87 0-7 3.13-7 7 0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" />
|
||||||
|
<circle cx="12" cy="9" r="2.5" fill="#fffcf2" />
|
||||||
|
</svg>
|
||||||
|
<span class="utci-recent-name">${l.name}</span>
|
||||||
|
</button>`)}
|
||||||
|
</div>`;
|
||||||
|
})()}
|
||||||
${(searchOpen || searchClosing) && html`
|
${(searchOpen || searchClosing) && html`
|
||||||
<div
|
<div
|
||||||
class=${'utci-search-wrap utci-search-overlay' + (searchOpen ? '' : ' is-closing')}
|
class=${'utci-search-wrap utci-search-overlay' + (searchOpen ? '' : ' is-closing')}
|
||||||
|
|||||||
@@ -57,9 +57,29 @@ export function useAppState() {
|
|||||||
return { name: 'London, England', lat: 51.509, lon: -0.126, country: 'GB' };
|
return { name: 'London, England', lat: 51.509, lon: -0.126, country: 'GB' };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Recent locations: the most recently viewed spots, so the user can
|
||||||
|
// hop back with one click. The list is kept newest-first with the
|
||||||
|
// current location at the front; the UI shows the others as chips.
|
||||||
|
const [recentLocations, setRecentLocations] = useState(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('sunscope_recent_locations');
|
||||||
|
if (saved) return JSON.parse(saved);
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const locKey = (l) => `${l.lat.toFixed(3)},${l.lon.toFixed(3)}`;
|
||||||
|
|
||||||
const setLocationAndSave = (loc) => {
|
const setLocationAndSave = (loc) => {
|
||||||
try { localStorage.setItem('sunscope_last_location', JSON.stringify(loc)); } catch (e) { /* ignore */ }
|
try { localStorage.setItem('sunscope_last_location', JSON.stringify(loc)); } catch (e) { /* ignore */ }
|
||||||
setLocation(loc);
|
setLocation(loc);
|
||||||
|
setRecentLocations((prev) => {
|
||||||
|
// Keep the current spot at the front and the 3 previous distinct
|
||||||
|
// ones behind it (4 total, so 3 chips remain after excluding current).
|
||||||
|
const next = [loc, ...prev.filter((l) => locKey(l) !== locKey(loc))].slice(0, 4);
|
||||||
|
try { localStorage.setItem('sunscope_recent_locations', JSON.stringify(next)); } catch (e) { /* ignore */ }
|
||||||
|
return next;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pro tier flag is read early so useForecast can pick its refresh cadence
|
// Pro tier flag is read early so useForecast can pick its refresh cadence
|
||||||
@@ -589,7 +609,7 @@ export function useAppState() {
|
|||||||
// ── RETURN ALL STATE + HANDLERS ───────────────────────────────────────
|
// ── RETURN ALL STATE + HANDLERS ───────────────────────────────────────
|
||||||
return {
|
return {
|
||||||
// location
|
// location
|
||||||
location, setLocationAndSave,
|
location, setLocationAndSave, recentLocations,
|
||||||
// forecast
|
// forecast
|
||||||
forecast, airQuality, loading, error, now, fetchedAt,
|
forecast, airQuality, loading, error, now, fetchedAt,
|
||||||
// search
|
// search
|
||||||
|
|||||||
@@ -115,5 +115,27 @@
|
|||||||
"farming": 3,
|
"farming": 3,
|
||||||
"basic": 1
|
"basic": 1
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"2026-06-29": {
|
||||||
|
"visits": 6,
|
||||||
|
"profiles": {
|
||||||
|
"basic": 6,
|
||||||
|
"home": 6,
|
||||||
|
"vehicle": 4,
|
||||||
|
"alltemps": 3,
|
||||||
|
"showall": 4,
|
||||||
|
"custom": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2026-07-04": {
|
||||||
|
"visits": 4,
|
||||||
|
"profiles": {
|
||||||
|
"home": 3,
|
||||||
|
"vehicle": 3,
|
||||||
|
"alltemps": 4,
|
||||||
|
"showall": 4,
|
||||||
|
"custom": 2,
|
||||||
|
"basic": 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user