5.5
New big and small scope sunset and sunrise simulation New calculations for the day tabs Added missing icons to simple view New record popups Week ahead description popups Camping profile Tweak to some of the indoor tems
This commit is contained in:
+202
-28
@@ -47,6 +47,21 @@ import { exportDayXls } from './export.js';
|
||||
|
||||
const html = htm.bind(h);
|
||||
|
||||
// ── Quick Record hand-off ───────────────────────────────────────────────
|
||||
// The event banner's "Record" button doesn't save anything itself — the
|
||||
// journal lives behind the dashboard's own session (api/journal-save.php),
|
||||
// which this page has no form for. It parks the entry it wants in
|
||||
// localStorage and sends you to the dashboard, which picks it up on the
|
||||
// first sign-in-complete load and opens its wizard on the review page,
|
||||
// filled in. See "Quick Record hand-off" in dashboard.html for the other
|
||||
// half, and keep the key and shape below in step with it.
|
||||
const QUICK_RECORD_KEY = 'sunscope_journal_prefill';
|
||||
// The journal can only compute a reading for the four profiles its own
|
||||
// API accepts (see api/journal-save.php). Anything else — Farming, an
|
||||
// Outdoors variant, Custom — hands over no profile at all, and the
|
||||
// dashboard keeps whichever one it already had.
|
||||
const QUICK_RECORD_PROFILES = new Set(['basic', 'home', 'vehicle', 'pets']);
|
||||
|
||||
// ── Scope time-lapse playback ───────────────────────────────────────────
|
||||
// Drives the big scope through the next 24h so you can watch the sky/ground
|
||||
// gradients and weather evolve. Speed: 15 simulated minutes every 1 real
|
||||
@@ -246,6 +261,7 @@ export function UTCIForecast() {
|
||||
handleEventTagClick, handleEventTagEnter, handleEventTagLeave,
|
||||
handleEventTagPopupEnter, handleEventTagPopupLeave,
|
||||
closePopup, closeEventTagPopup,
|
||||
weekPopup, weekPopupRef, handleWeekRowClick, closeWeekPopup,
|
||||
tableCanScrollLeft, tableCanScrollRight, handleBodyScroll,
|
||||
hourlyRows, days, utcOffsetMs,
|
||||
visible, tableRows, nowLocalISO, currentRow, currentCat,
|
||||
@@ -397,6 +413,45 @@ export function UTCIForecast() {
|
||||
};
|
||||
}, [activeEvents.length]);
|
||||
|
||||
// ── Quick Record ─────────────────────────────────────────────────────
|
||||
// Sends an event to the journal, along with the location, profile and
|
||||
// solar model it was read under, so the reading the dashboard works out
|
||||
// is the same one this page is showing. Three ways in: the banner, which
|
||||
// files under today; an event tag in the table, which files under the day
|
||||
// that row belongs to — the table can be showing any of the 14, and a
|
||||
// meteor shower tagged on Thursday is a Thursday entry; and a "What's
|
||||
// Coming" card, which files under that event's peak date. A peak years out
|
||||
// is fine — the dashboard finds no forecast for it and saves the entry
|
||||
// note-only, which is exactly what planning a night out under the Perseids
|
||||
// wants.
|
||||
//
|
||||
// Today here is today in the *location's* local time, not the browser's —
|
||||
// a stargazing note for a place eight hours away belongs on that place's
|
||||
// date. See QUICK_RECORD_KEY at the top of this file.
|
||||
const quickRecord = (ev, dateISO) => {
|
||||
if (!ev) return;
|
||||
const today = (nowLocalISO || new Date().toISOString()).slice(0, 10);
|
||||
const blurb = ev.message || ev.desc || '';
|
||||
const payload = {
|
||||
savedAt: Date.now(),
|
||||
eventId: ev.id,
|
||||
title: ev.title,
|
||||
date: /^\d{4}-\d{2}-\d{2}$/.test(dateISO || '') ? dateISO : today,
|
||||
// Title and message both, as one line: the entries list renders a note
|
||||
// as a single run of text, so a wrapped-in paragraph adds nothing.
|
||||
// Banner and tag events call their copy `message`; the almanac entries
|
||||
// behind "What's Coming" call theirs `desc` — either way it's the one
|
||||
// sentence describing the event.
|
||||
note: `${ev.emoji} ${ev.title}${blurb ? ` — ${blurb}` : ''}`,
|
||||
location: { name: location.name, lat: location.lat, lon: location.lon },
|
||||
profile: QUICK_RECORD_PROFILES.has(activeProfile) ? activeProfile : null,
|
||||
solarEnv: utciEnv,
|
||||
};
|
||||
try { localStorage.setItem(QUICK_RECORD_KEY, JSON.stringify(payload)); } catch (e) { /* ignore, e.g. private mode */ }
|
||||
// `location` is the forecast location in this scope, not the browser's.
|
||||
window.location.href = './dashboard.html';
|
||||
};
|
||||
|
||||
const [colTogglesOpen, setColTogglesOpen] = useState(false);
|
||||
// Which column pills are offered in the Columns bar. The bar as a whole is
|
||||
// Extra-only (see the isPro gate around col-toggles-body), so this just asks
|
||||
@@ -493,6 +548,14 @@ export function UTCIForecast() {
|
||||
}
|
||||
}, [activeProfile, outdoorsVariant]);
|
||||
|
||||
// Keep Quick view's Indoors selection in step with the table: follow the
|
||||
// Managed toggle, and fall back to SunSoak if Indoors is switched off.
|
||||
useEffect(() => {
|
||||
if (simpleTemp !== 'indoorT' && simpleTemp !== 'managedT') return;
|
||||
if (indoorMode !== 'on') setSimpleTemp('utciAdj');
|
||||
else setSimpleTemp(indoorManaged ? 'managedT' : 'indoorT');
|
||||
}, [indoorMode, indoorManaged, simpleTemp]);
|
||||
|
||||
// Focus the input the moment the search opens.
|
||||
useEffect(() => {
|
||||
if (searchOpen) searchInputRef.current?.focus();
|
||||
@@ -583,7 +646,7 @@ export function UTCIForecast() {
|
||||
// columns are skipped entirely. Hour always gets a blank lead cell.
|
||||
const groupLabelRow = () => {
|
||||
const groups = Object.keys(GROUP_ORDER);
|
||||
const cells = [html`<th class="grp-label-hour" scope="col"><span class="grp-label-date">${glanceDate}</span></th>`];
|
||||
const cells = [html`<th class="grp-label-hour" scope="col"><span class="grp-label-date">${glanceDateShort}</span></th>`];
|
||||
for (const g of groups) {
|
||||
const span = GROUP_ORDER[g].filter(isColVisible).length;
|
||||
if (span === 0) continue;
|
||||
@@ -956,19 +1019,39 @@ export function UTCIForecast() {
|
||||
onClick=${() => noteGoTo(i)}
|
||||
/>`)}
|
||||
</div>`}
|
||||
<button
|
||||
type="button"
|
||||
class="event-note-close"
|
||||
aria-label="Close event banner"
|
||||
title="Close"
|
||||
onClick=${noteClose}
|
||||
>
|
||||
<span class="event-note-close-label">Close</span>
|
||||
<svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true">
|
||||
<path d="M1.5 1.5 L10.5 10.5 M10.5 1.5 L1.5 10.5"
|
||||
fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
${/* Close sits above Quick Record so the two stack in the strip's
|
||||
top-right corner rather than running side by side. Quick Record
|
||||
acts on whatever slide is showing — it sits outside the stack
|
||||
so the strip keeps one button rather than one per event. */''}
|
||||
<div class="event-note-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="event-note-close"
|
||||
aria-label="Close event banner"
|
||||
title="Close"
|
||||
onClick=${noteClose}
|
||||
>
|
||||
<span class="event-note-close-label">Close</span>
|
||||
<svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true">
|
||||
<path d="M1.5 1.5 L10.5 10.5 M10.5 1.5 L1.5 10.5"
|
||||
fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="event-note-record"
|
||||
aria-label=${`Record “${ev.title}” in your journal`}
|
||||
title="Add this to your weather journal"
|
||||
onClick=${() => quickRecord(ev)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="11" height="11" aria-hidden="true">
|
||||
<rect x="4" y="3" width="16" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="2.4" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
|
||||
<line x1="8" y1="12" x2="16" y2="12" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
|
||||
</svg>
|
||||
<span class="event-note-record-label">Quick Record</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
})()}
|
||||
@@ -1167,6 +1250,7 @@ export function UTCIForecast() {
|
||||
wind=${scopeRow?.va ?? 0}
|
||||
gust=${scopeRow?.gust ?? 0}
|
||||
wd=${scopeRow?.wd ?? null}
|
||||
rh=${scopeRow?.RH ?? null}
|
||||
/>
|
||||
${hourlyRows.length > 0 && html`
|
||||
<div class="scope-daycycle-wrap">
|
||||
@@ -1381,7 +1465,9 @@ export function UTCIForecast() {
|
||||
const showSolar = visibleCols.utciP;
|
||||
const showShade = visibleCols.shadeT;
|
||||
const showVehicle = visibleCols.vehicleT;
|
||||
const showIndoor = visibleCols.indoorT || visibleCols.managedT;
|
||||
// Same rule as the table's Indoors/Managed columns (indoorMode),
|
||||
// so the tab and the column always appear together.
|
||||
const showIndoor = indoorMode === 'on';
|
||||
if (!showFur && !showSolar && !showShade && !showVehicle && !showIndoor) return null;
|
||||
const furOn = simpleTemp === 'furSurfaceT';
|
||||
const solarOn = simpleTemp === 'utciAdj';
|
||||
@@ -1506,7 +1592,8 @@ export function UTCIForecast() {
|
||||
return null;
|
||||
})();
|
||||
const windMph = Math.round((r.gust ?? r.va) * 2.237);
|
||||
// Tag shade = the exact gradient stop the curve is painted
|
||||
// Same event emojis the table's hour cell shows, for this card's hours.
|
||||
const cardEvents = getCellTagEvents(selectedDayEvents, r); // Tag shade = the exact gradient stop the curve is painted
|
||||
// with at this card's landing hour (fsc-grad, whiteMix 0.42),
|
||||
// so the connector line lands on the colour the tag is
|
||||
// wearing. Band label and weight still come from the band.
|
||||
@@ -1532,9 +1619,20 @@ export function UTCIForecast() {
|
||||
}}>
|
||||
<div class="fsc-time">${localHHMMs}</div>
|
||||
<div class="fsc-scope-wrap">
|
||||
<${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${42} />
|
||||
<${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${42}
|
||||
cc=${r.cc} ccLow=${r.ccLow} ccMid=${r.ccMid} ccHigh=${r.ccHigh} visKm=${r.visKm} rh=${r.RH} />
|
||||
${domIcon && html`<div class="fsc-wx-overlay">${domIcon}</div>`}
|
||||
</div>
|
||||
<div class="fsc-ev-row">
|
||||
${cardEvents.map((ev, idx) => html`
|
||||
<span key=${ev.id} class="event-cell-tag"
|
||||
onClick=${(e) => handleEventTagClick(cardEvents, idx, r.iso.slice(0, 10), e)}
|
||||
onMouseEnter=${(e) => handleEventTagEnter(cardEvents, idx, r.iso.slice(0, 10), e)}
|
||||
onMouseLeave=${handleEventTagLeave}
|
||||
role="button" tabIndex="0" aria-label=${ev.title}
|
||||
>${ev.emoji}</span>
|
||||
`)}
|
||||
</div>
|
||||
<div class="fsc-meta">
|
||||
<span class="fsc-meta-row fsc-meta-rain">${r.precipProb}%</span>
|
||||
<span class="fsc-meta-row fsc-meta-wind">${windMph}mph${r.compass ? html` <span class="fsc-meta-dir">${r.compass.label}</span>` : ''}</span>
|
||||
@@ -1656,14 +1754,15 @@ export function UTCIForecast() {
|
||||
const rowEvents = getCellTagEvents(selectedDayEvents, r);
|
||||
return html`
|
||||
<span class="utci-time-inner">
|
||||
<${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${38} />
|
||||
<${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${38}
|
||||
cc=${r.cc} ccLow=${r.ccLow} ccMid=${r.ccMid} ccHigh=${r.ccHigh} visKm=${r.visKm} rh=${r.RH} />
|
||||
<span>${localHHMM}</span>
|
||||
${rowEvents.length > 0 && html`
|
||||
<span class="utci-time-tags">
|
||||
${rowEvents.map((ev, idx) => html`
|
||||
<span key=${ev.id} class="event-cell-tag"
|
||||
onClick=${(e) => handleEventTagClick(rowEvents, idx, e)}
|
||||
onMouseEnter=${(e) => handleEventTagEnter(rowEvents, idx, e)}
|
||||
onClick=${(e) => handleEventTagClick(rowEvents, idx, r.iso.slice(0, 10), e)}
|
||||
onMouseEnter=${(e) => handleEventTagEnter(rowEvents, idx, r.iso.slice(0, 10), e)}
|
||||
onMouseLeave=${handleEventTagLeave}
|
||||
role="button" tabIndex="0" aria-label=${ev.title}
|
||||
>${ev.emoji}</span>
|
||||
@@ -1874,19 +1973,39 @@ export function UTCIForecast() {
|
||||
<div class="insight-panel insight-panel--week">
|
||||
<div class="insight-panel-title">The Week Ahead</div>
|
||||
<div class="insight-panel-sub">${titleCaseText(bestDaysLabel(activeProfile, outdoorsVariant))}</div>
|
||||
${weekAhead.map(({ icon, iconTitle, label, value, score }) => html`
|
||||
<div class=${`insight-row${score == null ? ' insight-row--week-note' : ''}`} key=${label}>
|
||||
<span class="insight-icon" title=${iconTitle}>${icon}</span>
|
||||
${weekAhead.map(({ icon, iconTitle, label, value, score }) => {
|
||||
const clickable = score != null;
|
||||
const popupPayload = clickable ? {
|
||||
key: label,
|
||||
title: titleCaseText(label),
|
||||
sub: titleCaseText(value),
|
||||
rows: [
|
||||
...(iconTitle ? [{ icon, text: iconTitle }] : []),
|
||||
{ icon: '📊', text: bestDaysHint(activeProfile, outdoorsVariant, score) },
|
||||
],
|
||||
} : null;
|
||||
return html`
|
||||
<div
|
||||
class=${`insight-row${score == null ? ' insight-row--week-note' : ''}${clickable ? ' insight-row--clickable' : ''}${weekPopup?.key === label ? ' is-open' : ''}`}
|
||||
key=${label}
|
||||
role=${clickable ? 'button' : null}
|
||||
tabIndex=${clickable ? 0 : null}
|
||||
aria-expanded=${clickable ? (weekPopup?.key === label ? 'true' : 'false') : null}
|
||||
onClick=${clickable ? (e) => handleWeekRowClick(popupPayload, e) : null}
|
||||
onKeyDown=${clickable ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleWeekRowClick(popupPayload, e); } } : null}
|
||||
>
|
||||
<span class="insight-icon">${icon}</span>
|
||||
<span class="insight-label">${titleCaseText(label)}</span>
|
||||
<span class="insight-value">${titleCaseText(value)}</span>
|
||||
${score != null && html`
|
||||
<span class="week-score" title=${bestDaysHint(activeProfile, outdoorsVariant, score)}>
|
||||
<span class="week-score">
|
||||
<span class="week-score__track">
|
||||
<span class="week-score__fill" style=${`width:${score}%;background:${scoreFillColor(score)}`}></span>
|
||||
</span>
|
||||
<span class="week-score__pct">${score}%</span>
|
||||
</span>`}
|
||||
</div>`)}
|
||||
</div>`;
|
||||
})}
|
||||
</div>`}
|
||||
${visible.length > 0 && html`
|
||||
<div class="export-panel">
|
||||
@@ -1954,6 +2073,23 @@ export function UTCIForecast() {
|
||||
<strong class="col-info-title">${ev.emoji} ${ev.title}</strong>
|
||||
<p class="col-info-desc">${ev.message}</p>
|
||||
</div>
|
||||
${/* Files under the day the tag sits on, not today — see
|
||||
quickRecord. Outside the stage, so the crossfade
|
||||
between events doesn't take the button with it. */''}
|
||||
<button
|
||||
type="button"
|
||||
class="ev-popup-record"
|
||||
aria-label=${`Record “${ev.title}” in your journal`}
|
||||
title="Add this to your weather journal"
|
||||
onClick=${() => quickRecord(ev, eventTagPopup.date)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="11" height="11" aria-hidden="true">
|
||||
<rect x="4" y="3" width="16" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="2.4" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
|
||||
<line x1="8" y1="12" x2="16" y2="12" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
|
||||
</svg>
|
||||
Quick Record
|
||||
</button>
|
||||
${evs.length > 1 && html`
|
||||
<div class="ev-popup-dots">
|
||||
${evs.map((_, i) => html`
|
||||
@@ -1968,6 +2104,27 @@ export function UTCIForecast() {
|
||||
</div>`;
|
||||
})()}
|
||||
|
||||
${weekPopup && html`
|
||||
<div
|
||||
ref=${weekPopupRef}
|
||||
class=${`col-info-popup col-info-popup--week${weekPopup.below ? ' col-info-popup--below' : ''}`}
|
||||
style=${{
|
||||
position: 'fixed',
|
||||
left: `${weekPopup.x}px`,
|
||||
top: `${weekPopup.y}px`,
|
||||
transform: weekPopup.below ? 'translateX(-50%)' : 'translateX(-50%) translateY(-100%)',
|
||||
'--arrow-left': `${weekPopup.arrowLeft}px`,
|
||||
}}
|
||||
>
|
||||
<button class="col-info-close" onClick=${closeWeekPopup} aria-label="Close">×</button>
|
||||
<strong class="col-info-title">${weekPopup.title}</strong>
|
||||
${weekPopup.sub && html`<p class="col-info-desc col-info-desc--lead">${weekPopup.sub}</p>`}
|
||||
<ul class="week-popup-list">
|
||||
${weekPopup.rows.map((r, i) => html`
|
||||
<li key=${i}><span class="week-popup-icon" aria-hidden="true">${r.icon}</span>${r.text}</li>`)}
|
||||
</ul>
|
||||
</div>`}
|
||||
|
||||
<div class="utci-legend">
|
||||
<span class="utci-legend-label">Thermal stress bands</span>
|
||||
<div class="utci-legend-row">
|
||||
@@ -2065,9 +2222,26 @@ export function UTCIForecast() {
|
||||
<span class="almanac-entry-countdown">${countdownLabel(ev.daysUntil)}</span>
|
||||
</div>
|
||||
<div class="almanac-entry-desc">${ev.desc}</div>
|
||||
${ev.visibilityNote && html`
|
||||
<span class="almanac-entry-visibility">📍 ${ev.visibilityNote}</span>
|
||||
`}
|
||||
${/* Files under the event's peak, not today — see quickRecord. */''}
|
||||
<div class="almanac-entry-foot">
|
||||
${ev.visibilityNote && html`
|
||||
<span class="almanac-entry-visibility">📍 ${ev.visibilityNote}</span>
|
||||
`}
|
||||
<button
|
||||
type="button"
|
||||
class="almanac-entry-record"
|
||||
aria-label=${`Record “${ev.title}” in your journal`}
|
||||
title="Add this to your weather journal"
|
||||
onClick=${() => quickRecord(ev, ev.peak)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="10" height="10" aria-hidden="true">
|
||||
<rect x="4" y="3" width="16" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="2.4" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
|
||||
<line x1="8" y1="12" x2="16" y2="12" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
|
||||
</svg>
|
||||
Quick Record
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
Reference in New Issue
Block a user