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:
fraxle
2026-10-08 09:53:41 +01:00
parent 71860b9dd9
commit 862f631beb
22 changed files with 2169 additions and 429 deletions
+34
View File
@@ -504,8 +504,11 @@ export function useAppState() {
const variantCols = OUTDOORS_VARIANTS[v]?.cols;
if (variantCols) {
setVisibleCols({ ...variantCols });
// Indoors state is owned here (and persisted) so Quick, Detailed and
// Table all agree - callers must not override it afterwards.
const hasIndoor = variantCols['indoorT'] || variantCols['managedT'];
setIndoorModeAndSave(hasIndoor ? 'on' : 'off');
setIndoorManagedAndSave(false);
}
// Auto-set UTCI environment modifier to match the selected variant.
const defaultEnv = VARIANT_DEFAULT_ENV[v] ?? 'open';
@@ -516,6 +519,14 @@ export function useAppState() {
setVehicleTypeAndSave('truck');
setVehicleSpeedAndSave('aroad');
}
// Camping shows the Indoor column as the tent's overnight temperature.
if (v === 'camping') {
setBuildingTypeAndSave('tent');
}
// Office models the Indoor column on an office building.
if (v === 'office') {
setBuildingTypeAndSave('office');
}
};
const activeCols = activeProfile === 'outdoors'
@@ -621,11 +632,21 @@ export function useAppState() {
}
}, []);
const closeWelcome = () => {
// First visit only: hand the new user straight on to the profile picker
// so they start from a profile that matches them rather than the default.
// Reopening the popup later from the footer link doesn't re-trigger it.
let firstVisit = false;
try { firstVisit = localStorage.getItem('sunscope_welcome_seen') !== '1'; } catch (e) { /* ignore */ }
try { localStorage.setItem('sunscope_welcome_seen', '1'); } catch (e) { /* ignore */ }
setWelcomeOpen(false);
if (firstVisit) setProfilePickPending(true);
};
const openWelcome = () => setWelcomeOpen(true);
// Set when the welcome popup is dismissed on a first visit; cleared by the
// effect below that actually opens the picker.
const [profilePickPending, setProfilePickPending] = useState(false);
// ── Weekly subscribe nudge (free users only) ─────────────────────────
// At most once every 7 days, and never on the first visit - the welcome
// popup owns that moment. The last-shown date lives in localStorage;
@@ -660,6 +681,16 @@ export function useAppState() {
const openPanel = () => setPanelOpen(true);
const closePanel = () => setPanelOpen(false);
// Completes the first-visit hand-off from the welcome popup, but only once
// the forecast is in: the panel anchors itself to the active-profile row,
// which doesn't exist until the forecast has rendered.
useEffect(() => {
if (!profilePickPending || !forecast) return;
setProfilePickPending(false);
setPanelOpen(true);
track('profile_picker_first_visit');
}, [profilePickPending, forecast]);
const [showUnits, setShowUnits] = useState(() => {
try {
const s = localStorage.getItem('sunscope_show_units');
@@ -778,6 +809,7 @@ export function useAppState() {
handleEventTagClick, handleEventTagEnter, handleEventTagLeave,
handleEventTagPopupEnter, handleEventTagPopupLeave,
closePopup, closeEventTagPopup,
weekPopup, weekPopupRef, handleWeekRowClick, closeWeekPopup,
} = useColumnPopup();
const {
@@ -791,6 +823,7 @@ export function useAppState() {
// The rotated table is a single table with CSS-only sticky edges, so
// none of this hook's dual-table syncing applies to it.
rotated: tableRotated,
tableInterval,
});
// ── 8. GEOCODING SEARCH ───────────────────────────────────────────────
@@ -953,6 +986,7 @@ export function useAppState() {
handleEventTagClick, handleEventTagEnter, handleEventTagLeave,
handleEventTagPopupEnter, handleEventTagPopupLeave,
closePopup, closeEventTagPopup,
weekPopup, weekPopupRef, handleWeekRowClick, closeWeekPopup,
// table scroll
tableCanScrollLeft, tableCanScrollRight, handleBodyScroll,
// computation