1.5.7
Profile memory Comment cleanup
This commit is contained in:
+42
-119
@@ -261,13 +261,20 @@ export function UTCIForecast() {
|
||||
|
||||
// (FREE_DAYS, FILTER_PROFILES and OUTDOORS_VARIANTS now live in ./config.js.)
|
||||
|
||||
// Current active filter profile
|
||||
const [activeProfile, setActiveProfile] = useState('basic');
|
||||
// Current active filter profile - persisted in localStorage
|
||||
const [activeProfile, setActiveProfile] = useState(() => {
|
||||
try { return localStorage.getItem('sunscope_profile') || 'basic'; } catch (e) { return 'basic'; }
|
||||
});
|
||||
|
||||
// Which columns appear in the hourly table by default.
|
||||
// true = visible on first load (and the only ones free users see)
|
||||
// false = hidden by default (Pro users can toggle these on)
|
||||
const [visibleCols, setVisibleCols] = useState({ ...FILTER_PROFILES.basic.cols });
|
||||
const [visibleCols, setVisibleCols] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('sunscope_profile') || 'basic';
|
||||
return { ...(FILTER_PROFILES[saved]?.cols ?? FILTER_PROFILES.basic.cols) };
|
||||
} catch (e) { return { ...FILTER_PROFILES.basic.cols }; }
|
||||
});
|
||||
const toggleCol = (col) => setVisibleCols(prev => ({ ...prev, [col]: !prev[col] }));
|
||||
|
||||
// Skin type for the sunburn-time column. Fitzpatrick II is typical UK fair.
|
||||
@@ -276,8 +283,14 @@ export function UTCIForecast() {
|
||||
// Vehicle type for the cabin heat column. 'car' is the default preset.
|
||||
const [vehicleType, setVehicleType] = useState('car');
|
||||
|
||||
// Places profile sub-variant (urban / beach / events / festival / wintersports / naturist)
|
||||
const [outdoorsVariant, setOutdoorsVariant] = useState('urban');
|
||||
// Places profile sub-variant - persisted in localStorage
|
||||
const [outdoorsVariant, setOutdoorsVariant] = useState(() => {
|
||||
try { return localStorage.getItem('sunscope_outdoors_variant') || 'urban'; } catch (e) { return 'urban'; }
|
||||
});
|
||||
const setOutdoorsVariantAndSave = (v) => {
|
||||
try { localStorage.setItem('sunscope_outdoors_variant', v); } catch (e) { /* ignore */ }
|
||||
setOutdoorsVariant(v);
|
||||
};
|
||||
|
||||
// Vehicle ventilation — true = windows open (high convective loss).
|
||||
const [vehicleVent, setVehicleVent] = useState(false);
|
||||
@@ -287,7 +300,13 @@ export function UTCIForecast() {
|
||||
// 'off' = hidden, 'on' = indoorT shown (managed or not depending on indoorManaged).
|
||||
const [buildingType, setBuildingType] = useState('brick');
|
||||
const [indoorManaged, setIndoorManaged] = useState(false);
|
||||
const [indoorMode, setIndoorMode] = useState('off');
|
||||
const [indoorMode, setIndoorMode] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('sunscope_profile') || 'basic';
|
||||
const cols = FILTER_PROFILES[saved]?.cols ?? FILTER_PROFILES.basic.cols;
|
||||
return (cols['indoorT'] || cols['managedT']) ? 'on' : 'off';
|
||||
} catch (e) { return 'off'; }
|
||||
});
|
||||
|
||||
// Pollen type for the pollen column. Persisted in localStorage.
|
||||
const [pollenType, setPollenType] = useState(() => {
|
||||
@@ -304,6 +323,7 @@ export function UTCIForecast() {
|
||||
|
||||
const activateProfile = (key) => {
|
||||
const profile = FILTER_PROFILES[key];
|
||||
try { localStorage.setItem('sunscope_profile', key); } catch (e) { /* ignore */ }
|
||||
setActiveProfile(key);
|
||||
if (key !== 'custom') {
|
||||
setVisibleCols({ ...profile.cols });
|
||||
@@ -544,7 +564,7 @@ export function UTCIForecast() {
|
||||
</nav>
|
||||
<main class="utci-shell">
|
||||
|
||||
<!-- ── EVENT BANNER ── slideshow when multiple events active ── -->
|
||||
|
||||
<div class=${`event-banner-wrap${bannerVisible ? ' visible' : ''}`}>
|
||||
${activeEvents.length > 0 && (() => {
|
||||
const ev = activeEvents[bannerIndex] || activeEvents[0];
|
||||
@@ -626,24 +646,7 @@ export function UTCIForecast() {
|
||||
glob=${currentRow?.glob ?? 0}
|
||||
activeEvent=${lensEvent}
|
||||
/>
|
||||
{/* ── FUTURE FEATURE v2: Historical Context Line ─────────────────
|
||||
Show a subtle single line directly below the dial readout:
|
||||
e.g. "3.2°C above the May average for this location"
|
||||
"Near normal for late August"
|
||||
|
||||
Design notes:
|
||||
• Must be visually elegant — same Fraunces/Manrope type pairing
|
||||
as the rest of the dial area, small and muted
|
||||
• Positive delta: warm amber tone; negative delta: cool blue tone
|
||||
• Source: Open-Meteo has a free /climate endpoint that returns
|
||||
monthly climate normals (ERA5 reanalysis) for any lat/lon.
|
||||
Fetch once on location change, cache in a ref. Compare today's
|
||||
peak air temp (or UTCI) to that month's normal.
|
||||
• Could also show min/max historical context for the week:
|
||||
"Warmest day forecast this week" / "Coolest night since March"
|
||||
• The fetch is separate from the main forecast — handle its own
|
||||
loading/error state independently so it doesn't block the UI.
|
||||
─────────────────────────────────────────────────────────────── */}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
@@ -694,12 +697,7 @@ export function UTCIForecast() {
|
||||
|
||||
${forecast && days.length > 0 && html`
|
||||
<${Fragment}>
|
||||
<!--
|
||||
DAY TABS — one button per day, coloured by confidence band.
|
||||
Days 4+ get 🔒'd when isPro is false. To change the lock
|
||||
behaviour (e.g. open a paywall modal instead of doing
|
||||
nothing), edit the onClick handler below.
|
||||
-->
|
||||
|
||||
<div class=${`utci-day-tabs-wrap${canScrollLeft ? ' fade-left' : ''}${canScrollRight ? ' fade-right' : ''}`}>
|
||||
<button
|
||||
class=${`utci-day-scroll left${canScrollLeft ? '' : ' hidden'}`}
|
||||
@@ -803,13 +801,7 @@ export function UTCIForecast() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
PRO UPSELL CARD — shown when a locked day is clicked.
|
||||
Visible only while proPromptDay !== null. To change the
|
||||
copy or pricing, edit the strings below. The "Notify me"
|
||||
button is a mailto: link — replace with a real signup
|
||||
form when you have one.
|
||||
-->
|
||||
|
||||
${proPromptDay !== null && days[proPromptDay] && (() => {
|
||||
const promptDate = new Date(days[proPromptDay].key + 'T00:00Z');
|
||||
const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long', timeZone: 'UTC' });
|
||||
@@ -1001,13 +993,7 @@ export function UTCIForecast() {
|
||||
</div>`;
|
||||
})()}
|
||||
|
||||
<!--
|
||||
FILTER PROFILE SELECTOR — presets shown to all users.
|
||||
Extra profiles are shown in place with a gentle 🔒 and clicking
|
||||
them triggers the same upsell prompt as locked days.
|
||||
The bottom border is removed only when the col-toggles bar
|
||||
follows (Pro users), so the two bars merge into one panel.
|
||||
-->
|
||||
|
||||
<div class="filter-profiles" style=${{ borderBottom: isPro ? 'none' : '' }}>
|
||||
<span class="filter-profiles-label">Profile:</span>
|
||||
${profileButtonOrder.slice(0, 3).map((key) => {
|
||||
@@ -1048,8 +1034,9 @@ export function UTCIForecast() {
|
||||
setProPromptDay(0);
|
||||
return;
|
||||
}
|
||||
try { localStorage.setItem('sunscope_profile', 'outdoors'); } catch (e) { /* ignore */ }
|
||||
setActiveProfile('outdoors');
|
||||
setOutdoorsVariant(v);
|
||||
setOutdoorsVariantAndSave(v);
|
||||
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
|
||||
setIndoorMode('off');
|
||||
setIndoorManaged(false);
|
||||
@@ -1077,8 +1064,9 @@ export function UTCIForecast() {
|
||||
setProPromptDay(0);
|
||||
return;
|
||||
}
|
||||
try { localStorage.setItem('sunscope_profile', 'outdoors'); } catch (e) { /* ignore */ }
|
||||
setActiveProfile('outdoors');
|
||||
setOutdoorsVariant(v);
|
||||
setOutdoorsVariantAndSave(v);
|
||||
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
|
||||
setIndoorMode('off');
|
||||
setIndoorManaged(false);
|
||||
@@ -1108,12 +1096,7 @@ export function UTCIForecast() {
|
||||
|
||||
</div>
|
||||
|
||||
<!--
|
||||
COLUMN TOGGLES — in exact table column order.
|
||||
Buttons visible to all users when profile includes that col.
|
||||
Burn + Vehicle dropdowns always shown (free + pro).
|
||||
Pro users see all toggles; free users see profile-filtered subset.
|
||||
-->
|
||||
|
||||
${(isPro || activeCols['burn'] || activeCols['vehicleT'] || activeCols['indoorT'] || activeCols['managedT'] || activeCols['pollen']) && html`
|
||||
<div class="col-toggles">
|
||||
<span class="col-toggles-label">Columns:</span>
|
||||
@@ -1244,68 +1227,13 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
|
||||
|
||||
</div>`}
|
||||
|
||||
<!--
|
||||
HOURLY TABLE — each row is one hour from the selected day.
|
||||
Each column is wrapped in a visibleCols.X check, so it only
|
||||
shows when its toggle is on. To force a column to always
|
||||
show, remove the visibleCols check around it. To rename a
|
||||
heading, edit the text inside the matching <th>.
|
||||
-->
|
||||
{/* ── FUTURE FEATURE v2: Sticky "Now" Row + Resizable Table ────────
|
||||
TWO related ideas for the table UX:
|
||||
|
||||
1. AUTO-SCROLL TO NOW ON LOAD
|
||||
On first render (or day change to today), scroll the tbody
|
||||
so the current-hour row is visible — ideally centred or at
|
||||
the top third of the viewport. Use a ref on the now-row <tr>
|
||||
and call scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
inside a useEffect that fires when days[selectedDay] changes.
|
||||
The now row already has `currentRow` identified — just need
|
||||
to attach a ref and fire the scroll.
|
||||
|
||||
2. VERTICALLY RESIZABLE TABLE (extends existing drag-to-scroll)
|
||||
The table already has horizontal drag-to-scroll (useTableScroll).
|
||||
For v2, make the tbody height resizable — a drag handle on the
|
||||
bottom edge of the table, similar to how devtools panels resize.
|
||||
Store the preferred height in localStorage ('sunscope_table_height').
|
||||
Combined with the now-row auto-scroll, the user sets the table to
|
||||
exactly the number of rows they want to see at once and it always
|
||||
opens at the current hour. Very clean UX.
|
||||
────────────────────────────────────────────────────────────────── */}
|
||||
{/* ── FUTURE FEATURE v2: Mobile Card Layout ─────────────────────────
|
||||
On small screens the wide table is painful. Consider a responsive
|
||||
breakpoint (e.g. < 640px) that switches from the table to a
|
||||
vertical stack of hour cards:
|
||||
|
||||
┌──────────────────────────────────┐
|
||||
│ 14:00 🌤️ SkyScope porthole │
|
||||
│ Air 24°C Wind 12 km/h SW │
|
||||
│ UTCI+P ████░░░░ 28.4°C Warm │
|
||||
│ UV 6 · Burn 35 min · Precip – │
|
||||
└──────────────────────────────────┘
|
||||
|
||||
Each card shows only the columns that are active in the current
|
||||
profile — same visibility logic, different layout. The card design
|
||||
should stay true to the brass/parchment aesthetic.
|
||||
|
||||
Implementation approach:
|
||||
• CSS media query switches .utci-table-wrap to display:none
|
||||
and shows a .utci-card-list instead (same data, different markup)
|
||||
• OR: render the cards in JS from the same hourlyRows array,
|
||||
conditionally based on a useWindowWidth() hook
|
||||
• Needs careful thought on which columns to show in "summary"
|
||||
view vs an expandable "detail" tap — don't want to overwhelm
|
||||
the card but also don't want to hide too much.
|
||||
• General spacing and layout passes needed for mobile regardless
|
||||
of whether the card view is implemented in v2.
|
||||
────────────────────────────────────────────────────────────────── */}
|
||||
|
||||
|
||||
|
||||
<div class=${`utci-table-wrap${tableCanScrollLeft ? ' scroll-fade-left' : ''}${tableCanScrollRight ? ' scroll-fade-right' : ''}`} ref=${tableWrapRef}>
|
||||
<span class="utci-scroll-chevron left" aria-hidden="true">‹</span>
|
||||
<span class="utci-scroll-chevron right" aria-hidden="true">›</span>
|
||||
<!-- Sticky header strip — locks to viewport top. Clipped
|
||||
horizontally; the inner .utci-thead-track is shifted
|
||||
via translateX from JS to follow the body's scrollLeft.
|
||||
See handleBodyScroll + useLayoutEffect above. -->
|
||||
|
||||
<div class="utci-thead-sticky" ref=${headStickyRef}>
|
||||
<div class="utci-thead-track" ref=${headTrackRef}>
|
||||
<table class="utci-table utci-table-head" ref=${headTableRef}>
|
||||
@@ -1344,9 +1272,7 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Body scroller — owns the horizontal scrollbar. The
|
||||
onScroll handler translates the header track to keep
|
||||
columns aligned with the visible body columns. -->
|
||||
|
||||
<div class="utci-tbody-scroll" ref=${bodyScrollRef} onScroll=${handleBodyScroll}>
|
||||
<table class="utci-table utci-table-body" ref=${bodyTableRef}>
|
||||
<tbody>
|
||||
@@ -1655,10 +1581,7 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
ALMANAC — upcoming cosmic events in the next 3 months.
|
||||
Location-aware: visibility notes adjust by latitude.
|
||||
-->
|
||||
|
||||
${(() => {
|
||||
const upcoming = getUpcomingEvents(location, 90);
|
||||
const formatPeak = (iso) => {
|
||||
|
||||
Reference in New Issue
Block a user