542 lines
24 KiB
JavaScript
542 lines
24 KiB
JavaScript
// ------------------------------------------------------------------------
|
||
// components/DayTabs.js - Day-tab strip, pro-prompt card, confidence
|
||
// band bar, and filter-profile row for the UTCIForecast app.
|
||
//
|
||
// Extracted from app.js to keep the main component under the AI edit
|
||
// safe-zone. All state lives in useAppState - this component is purely
|
||
// presentational and receives everything it needs as props.
|
||
//
|
||
// Props:
|
||
// days - array of day objects from buildHourlyRows
|
||
// selectedDay - index of the active day tab
|
||
// setSelectedDay - setter for selectedDay
|
||
// isPro - boolean Pro status
|
||
// FREE_DAYS - number of free days
|
||
// proPromptDay - index of locked day that was clicked
|
||
// setProPromptDay - setter for proPromptDay
|
||
// proPromptSource - string key for upsell copy
|
||
// setProPromptSource - setter for proPromptSource
|
||
// activeProfile - current filter profile key
|
||
// activateProfile - function to switch profile
|
||
// activeCols - effective column set for current profile
|
||
// visibleCols - object of col-key -> boolean visibility
|
||
// activityOptions - dropdown options for Activities selector
|
||
// placeOptions - dropdown options for Places selector
|
||
// workOptions - dropdown options for Work selector
|
||
// activityValue - current activity dropdown value
|
||
// activityLabel - current activity dropdown label
|
||
// placeValue - current place dropdown value
|
||
// placeLabel - current place dropdown label
|
||
// workValue - current work dropdown value
|
||
// workLabel - current work dropdown label
|
||
// outdoorsVariant - current outdoors sub-variant key
|
||
// setOutdoorsVariantAndSave - setter that also persists to localStorage
|
||
// setActiveProfile - raw setter for activeProfile
|
||
// setVisibleCols - raw setter for visibleCols
|
||
// setIndoorMode - setter for indoorMode
|
||
// setIndoorManaged - setter for indoorManaged
|
||
// dayTabsRef - ref for the scrollable tab strip element
|
||
// canScrollLeft - boolean for left-fade chevron
|
||
// canScrollRight - boolean for right-fade chevron
|
||
// scrollDayTabs - function(dir) to scroll strip left/right
|
||
// ------------------------------------------------------------------------
|
||
|
||
import { h, Fragment } from '../../vendor/preact.js';
|
||
import htm from '../../vendor/htm.js';
|
||
import { confidenceBand } from '../utils.js';
|
||
import { FREE_DAYS, FILTER_PROFILES, OUTDOORS_VARIANTS, profileButtonOrder, activityVariantKeys, placeVariantKeys, workVariantKeys } from '../config.js';
|
||
import { CloudIcon, PrecipIcon, CustomSelect } from '../components.js';
|
||
|
||
const html = htm.bind(h);
|
||
|
||
// Converts a base RGB colour into the same 135° gradient used by the thermal
|
||
// bands legend: 50% white blend to pastelise, then a subtle light→dark sweep.
|
||
function weatherGradient(r, g, b) {
|
||
const blend = (c) => Math.round(c + (255 - c) * 0.68);
|
||
const [mr, mg, mb] = [blend(r), blend(g), blend(b)];
|
||
const lighten = (c) => Math.min(255, Math.round(c + (255 - c) * 0.30));
|
||
const darken = (c) => Math.round(c * 0.96);
|
||
const light = `rgb(${lighten(mr)},${lighten(mg)},${lighten(mb)})`;
|
||
const mid = `rgb(${mr},${mg},${mb})`;
|
||
const dark = `rgb(${darken(mr)},${darken(mg)},${darken(mb)})`;
|
||
return `linear-gradient(135deg, ${light} 0%, ${mid} 60%, ${dark} 100%)`;
|
||
}
|
||
|
||
export function DayTabs({
|
||
days,
|
||
selectedDay, setSelectedDay,
|
||
isPro,
|
||
proPromptDay, setProPromptDay,
|
||
proPromptSource, setProPromptSource,
|
||
activeProfile, activateProfile, activeCols,
|
||
visibleCols,
|
||
activityOptions, placeOptions, workOptions,
|
||
activityValue, activityLabel,
|
||
placeValue, placeLabel,
|
||
workValue, workLabel,
|
||
outdoorsVariant, setOutdoorsVariantAndSave,
|
||
setActiveProfile, setVisibleCols,
|
||
setIndoorMode, setIndoorManaged, setBuildingType,
|
||
dayTabsRef, canScrollLeft, canScrollRight, scrollDayTabs,
|
||
}) {
|
||
return html`
|
||
<${Fragment}>
|
||
|
||
<div class=${`utci-day-tabs-wrap${canScrollLeft ? ' fade-left' : ''}${canScrollRight ? ' fade-right' : ''}`}>
|
||
<button
|
||
class=${`utci-day-scroll left${canScrollLeft ? '' : ' hidden'}`}
|
||
onClick=${() => scrollDayTabs(-1)}
|
||
aria-label="Scroll days left"
|
||
type="button">‹</button>
|
||
<button
|
||
class=${`utci-day-scroll right${canScrollRight ? '' : ' hidden'}`}
|
||
onClick=${() => scrollDayTabs(1)}
|
||
aria-label="Scroll days right"
|
||
type="button">›</button>
|
||
<div class="utci-day-tabs" ref=${dayTabsRef}>
|
||
${days.map((d, i) => {
|
||
const band = confidenceBand(i);
|
||
const locked = !isPro && i >= FREE_DAYS;
|
||
const isActive = i === selectedDay;
|
||
const dDate = new Date(d.key + 'T00:00Z');
|
||
const dayName = i === 0 ? 'Today'
|
||
: i === 1 ? 'Tomorrow'
|
||
: dDate.toLocaleDateString('en-GB', { weekday: 'short', timeZone: 'UTC' });
|
||
const utcipVals = d.rows.map(r => r.utciAdj).filter(v => isFinite(v));
|
||
const dayHi = utcipVals.length ? Math.round(Math.max(...utcipVals)) : null;
|
||
const dayLo = utcipVals.length ? Math.round(Math.min(...utcipVals)) : null;
|
||
|
||
// Day-tab weather icon - use daytime rows where available
|
||
const dayRows = d.rows.filter(r => r.elev > 0);
|
||
const repRows = dayRows.length > 0 ? dayRows : d.rows;
|
||
const dayPrecip = repRows.reduce((s, r) => s + (r.precip || 0), 0);
|
||
const daySnow = repRows.reduce((s, r) => s + (r.snow || 0), 0);
|
||
const catCounts = {};
|
||
repRows.forEach(r => { if (r.cloudCat) catCounts[r.cloudCat] = (catCounts[r.cloudCat] || 0) + 1; });
|
||
const modalCloudCat = Object.keys(catCounts).sort((a, b) => catCounts[b] - catCounts[a])[0] || 'clear';
|
||
const midRow = repRows[Math.floor(repRows.length / 2)];
|
||
const repElev = midRow ? midRow.elev : 45;
|
||
const repDt = midRow ? midRow.dt : dDate;
|
||
const showPrecip = dayPrecip >= 0.3 || daySnow >= 0.1;
|
||
|
||
// Base RGB for each weather condition — pastelised by weatherGradient()
|
||
const [wR, wG, wB] = (daySnow >= 0.1) ? [100, 160, 230]
|
||
: (dayHi > 38) ? [210, 60, 30]
|
||
: showPrecip ? [ 50, 120, 200]
|
||
: (dayHi > 32) ? [220, 110, 30]
|
||
: (dayHi > 26) ? [220, 180, 30]
|
||
: modalCloudCat === 'overcast' ? [100, 110, 130]
|
||
: (dayHi !== null && dayHi < 9) ? [ 60, 110, 180]
|
||
: modalCloudCat === 'scattered' ? [160, 150, 130]
|
||
: modalCloudCat === 'wispy' ? [200, 180, 130]
|
||
: [220, 190, 50];
|
||
const wBg = weatherGradient(wR, wG, wB);
|
||
const wText = '#2a1d10';
|
||
|
||
return html`
|
||
<button
|
||
key=${d.key}
|
||
class=${`utci-day-tab ${isActive ? 'active' : ''}${locked ? ' locked' : ''}`}
|
||
onClick=${() => {
|
||
if (locked) {
|
||
setProPromptSource('day');
|
||
setProPromptDay(i);
|
||
} else {
|
||
setSelectedDay(i);
|
||
setProPromptDay(null);
|
||
}
|
||
}}
|
||
title=${locked
|
||
? `${band.label} · SunScope Extra unlocks day ${i + 1}`
|
||
: `${band.label} · day ${i + 1} of 14`}
|
||
style=${{
|
||
background: wBg,
|
||
color: wText,
|
||
border: '1px solid #c9b08a',
|
||
opacity: locked ? 0.5 : 1,
|
||
cursor: locked ? 'not-allowed' : 'pointer',
|
||
position: 'relative',
|
||
filter: isActive ? 'saturate(1.1) brightness(1.04)' : 'none',
|
||
}}
|
||
>
|
||
<span style=${{
|
||
position: 'absolute',
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
height: isActive ? '5px' : '4px',
|
||
background: band.bg,
|
||
borderRadius: '0 0 2px 2px',
|
||
transition: 'height 0.1s',
|
||
}} aria-hidden="true"></span>
|
||
${locked && html`
|
||
<span style=${{ position: 'absolute', top: '3px', right: '5px', fontSize: '10px', opacity: 0.75 }}>🔒</span>`}
|
||
${dayName}
|
||
<span class="utci-day-date" style=${{ color: '#5a3f24' }}>
|
||
${dDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })}
|
||
</span>
|
||
<span style=${{ display: 'block', margin: '3px auto 0', lineHeight: 1 }}>
|
||
${showPrecip
|
||
? html`<${PrecipIcon} precip=${dayPrecip} snow=${daySnow} size=${22} />`
|
||
: html`<${CloudIcon} category=${modalCloudCat} elev=${repElev} dt=${repDt} size=${22} />`}
|
||
</span>
|
||
${dayHi !== null && html`
|
||
<span style=${{
|
||
display: 'block',
|
||
fontFamily: 'Fraunces, serif',
|
||
fontStyle: 'italic',
|
||
fontSize: '11px',
|
||
marginTop: '2px',
|
||
letterSpacing: 0,
|
||
textTransform: 'none',
|
||
}}>
|
||
<span style=${{ color: '#c0622a', fontWeight: 600 }}>${dayHi}°</span>
|
||
<span style=${{ opacity: 0.5 }}> / </span>
|
||
<span style=${{ color: '#3a6080' }}>${dayLo}°</span>
|
||
</span>`}
|
||
</button>`;
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
${proPromptDay !== null && days[proPromptDay] && (() => {
|
||
const promptDate = new Date(days[proPromptDay].key + 'T00:00Z');
|
||
const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long', timeZone: 'UTC' });
|
||
const extraPromptCopy = {
|
||
// Profile buttons
|
||
'profile:alltemps': {
|
||
title: 'Temps is part of SunScope Extra',
|
||
detail: 'Extra unlocks the temperature comparison view — air, soil, concrete, vehicle interior, and indoor estimates side by side in one place.',
|
||
},
|
||
'profile:showall': {
|
||
title: 'Show All is part of SunScope Extra',
|
||
detail: 'Extra unlocks the full column set — every data point SunScope tracks, visible at once for deep-dive analysis.',
|
||
},
|
||
'profile:custom': {
|
||
title: 'Custom columns are part of SunScope Extra',
|
||
detail: 'Extra lets you hand-pick exactly which columns appear. Mix and match air, dew, soil, UV, UTCI and more to build your perfect view.',
|
||
},
|
||
// Places
|
||
'variant:construction': {
|
||
title: 'Construction is part of SunScope Extra',
|
||
detail: 'Extra adds the Construction profile with felt temperature, wind, rain probability, visibility, and air quality — the key factors for safe site working.',
|
||
},
|
||
// Activities
|
||
'variant:hiking': {
|
||
title: 'Hiking is part of SunScope Extra',
|
||
detail: 'Extra adds the Hiking profile with felt temperature, UV index, burn time, wind direction, and visibility — essential for longer days on exposed terrain.',
|
||
},
|
||
'variant:photography': {
|
||
title: 'Photography is part of SunScope Extra',
|
||
detail: 'Extra adds the Photography profile with direct and diffuse radiation, sun elevation, cloud cover, and visibility — the conditions that make or break a shoot.',
|
||
},
|
||
'variant:sailing': {
|
||
title: 'Sailing is part of SunScope Extra',
|
||
detail: 'Extra adds the Sailing profile with wind speed and direction, dew point, UV, burn time, sun elevation, and visibility for on-water planning.',
|
||
},
|
||
'variant:wintersports': {
|
||
title: 'Winter Sports is part of SunScope Extra',
|
||
detail: 'Extra adds the Winter Sports profile with UV-A and UV-B, burn time, sun elevation, wind direction, and visibility for snow and slope conditions.',
|
||
},
|
||
'variant:naturist': {
|
||
title: 'Naturist is part of SunScope Extra',
|
||
detail: 'Extra adds the Naturist profile with full skin-exposure detail — UV, burn time, felt temperature, dew point, humidity, and air quality.',
|
||
},
|
||
// Work
|
||
'variant:market': {
|
||
title: 'Market Trading is part of SunScope Extra',
|
||
detail: 'Extra adds the Market Trading profile with felt temperature, wind direction, rain probability, and visibility — the key factors for planning stall days.',
|
||
},
|
||
'variant:windowcleaning': {
|
||
title: 'Window Cleaning is part of SunScope Extra',
|
||
detail: 'Extra adds the Window Cleaning profile focused on wind speed and direction, rain, and felt temperature — the conditions that determine whether work is safe and worthwhile.',
|
||
},
|
||
'variant:office': {
|
||
title: 'Office is part of SunScope Extra',
|
||
detail: 'Extra adds the Office profile with solar gain, managed indoor temperature, humidity, air quality, and rain probability — useful for commute planning and building comfort.',
|
||
},
|
||
};
|
||
const promptCopy = extraPromptCopy[proPromptSource] || {
|
||
title: `${dayName}'s forecast is part of SunScope Extra`,
|
||
detail: 'Extra unlocks the full 14-day forecast, customisable columns, specialist profiles, and an ad-free view.',
|
||
};
|
||
return html`
|
||
<div style=${{
|
||
margin: '12px 0',
|
||
padding: '18px 22px',
|
||
background: '#fdf8ee',
|
||
border: '1.5px solid #c9b08a',
|
||
borderLeft: '4px solid #c8922a',
|
||
borderRadius: '0 4px 4px 0',
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
gap: '14px',
|
||
}}>
|
||
<div style=${{ flex: '1 1 320px', minWidth: '260px' }}>
|
||
<div style=${{
|
||
fontFamily: 'Fraunces, serif',
|
||
fontStyle: 'italic',
|
||
fontSize: '19px',
|
||
fontWeight: 700,
|
||
color: '#1e1208',
|
||
marginBottom: '6px',
|
||
lineHeight: 1.25,
|
||
}}>
|
||
<span style=${{ fontStyle: 'normal' }}>🔒</span> ${promptCopy.title}
|
||
</div>
|
||
<div style=${{
|
||
fontFamily: 'Manrope, sans-serif',
|
||
fontSize: '13.5px',
|
||
color: '#4a3420',
|
||
lineHeight: 1.65,
|
||
}}>
|
||
${promptCopy.detail}
|
||
</div>
|
||
<div style=${{
|
||
fontFamily: 'Manrope, sans-serif',
|
||
fontWeight: 700,
|
||
fontSize: '11px',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.07em',
|
||
color: '#c8922a',
|
||
marginTop: '10px',
|
||
}}>
|
||
£2 / month · cancel any time
|
||
</div>
|
||
</div>
|
||
<div style=${{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '8px',
|
||
alignItems: 'flex-end',
|
||
}}>
|
||
<a
|
||
href="https://buy.stripe.com/9B63cw7vl8k15Ei9DQd7q00"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
style=${{
|
||
display: 'inline-block',
|
||
padding: '10px 18px',
|
||
background: '#c8922a',
|
||
color: '#fff',
|
||
textDecoration: 'none',
|
||
fontFamily: 'Manrope, sans-serif',
|
||
fontWeight: 700,
|
||
fontSize: '11px',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.07em',
|
||
borderRadius: '3px',
|
||
whiteSpace: 'nowrap',
|
||
}}
|
||
>
|
||
Subscribe — £2/month
|
||
</a>
|
||
<a
|
||
href="/restore.php"
|
||
style=${{
|
||
fontFamily: 'Manrope, sans-serif',
|
||
fontWeight: 700,
|
||
fontSize: '10px',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.07em',
|
||
color: '#c8922a',
|
||
textDecoration: 'none',
|
||
borderBottom: '1px solid rgba(200,146,42,0.4)',
|
||
paddingBottom: '1px',
|
||
}}
|
||
>
|
||
Already subscribed? Restore access →
|
||
</a>
|
||
<a
|
||
href="https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
style=${{
|
||
fontFamily: 'Manrope, sans-serif',
|
||
fontWeight: 700,
|
||
fontSize: '10px',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.07em',
|
||
color: '#b09870',
|
||
textDecoration: 'none',
|
||
borderBottom: '1px solid rgba(176,152,112,0.4)',
|
||
paddingBottom: '1px',
|
||
}}
|
||
>
|
||
Manage or cancel subscription →
|
||
</a>
|
||
<button
|
||
onClick=${() => setProPromptDay(null)}
|
||
style=${{
|
||
background: 'transparent',
|
||
border: 'none',
|
||
cursor: 'pointer',
|
||
fontFamily: 'Manrope, sans-serif',
|
||
fontWeight: 700,
|
||
fontSize: '10px',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.07em',
|
||
color: '#b09870',
|
||
padding: '2px 4px',
|
||
}}
|
||
>
|
||
dismiss
|
||
</button>
|
||
</div>
|
||
</div>`;
|
||
})()}
|
||
|
||
${(() => {
|
||
const band = confidenceBand(selectedDay);
|
||
const isOutlook = selectedDay >= 7;
|
||
return html`
|
||
<div style=${{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '10px',
|
||
margin: '8px 0 4px',
|
||
padding: '6px 10px',
|
||
background: band.tint,
|
||
borderLeft: `3px solid ${band.edge}`,
|
||
borderRadius: '0 4px 4px 0',
|
||
fontSize: '12px',
|
||
color: '#3a2a18',
|
||
fontFamily: 'Manrope, sans-serif',
|
||
fontWeight: 600,
|
||
flexWrap: 'wrap',
|
||
}}>
|
||
<span style=${{ letterSpacing: '0.06em', textTransform: 'uppercase', fontWeight: 700 }}>
|
||
Day ${selectedDay + 1} of 14 · Forecast clarity: ${band.label}
|
||
</span>
|
||
${isOutlook && html`
|
||
<span style=${{ opacity: 0.7, fontStyle: 'italic' }}>
|
||
forecast skill is reduced — treat hourly detail as trend, not precision
|
||
</span>`}
|
||
</div>`;
|
||
})()}
|
||
|
||
<div class="filter-profiles" style=${{ borderBottom: isPro ? 'none' : '' }}>
|
||
<span class="filter-profiles-label">Profile:</span>
|
||
${profileButtonOrder.slice(0, 3).map((key) => {
|
||
const profile = FILTER_PROFILES[key];
|
||
const locked = profile.proOnly && !isPro;
|
||
return html`
|
||
<button
|
||
key=${key}
|
||
class=${`profile-btn${activeProfile === key ? ' active' : ''}${locked ? ' locked' : ''}`}
|
||
style=${{ opacity: locked ? 0.6 : 1, cursor: locked ? 'not-allowed' : 'pointer', position: 'relative' }}
|
||
title=${locked ? `🔒 ${profile.label} is part of SunScope Extra` : ''}
|
||
onClick=${() => {
|
||
if (locked) {
|
||
setProPromptSource(`profile:${key}`);
|
||
setProPromptDay(0);
|
||
return;
|
||
}
|
||
activateProfile(key);
|
||
}}
|
||
>${profile.icon} ${profile.label}${locked ? ' 🔒' : ''}</button>`;
|
||
})}
|
||
<span class="profile-divider" aria-hidden="true"></span>
|
||
<${CustomSelect}
|
||
key="places"
|
||
value=${placeValue}
|
||
isOn=${activeProfile === 'outdoors' && placeVariantKeys.includes(outdoorsVariant)}
|
||
noHide=${true}
|
||
hideLabel="🌤️ Places"
|
||
buttonLabel=${placeValue === 'off' ? '🌤️ Places' : `${placeLabel}`}
|
||
options=${placeOptions}
|
||
onChange=${(v) => {
|
||
if (v === 'off') { activateProfile('basic'); return; }
|
||
if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) {
|
||
setProPromptSource(`variant:${v}`);
|
||
setProPromptDay(0);
|
||
return;
|
||
}
|
||
try { localStorage.setItem('sunscope_profile', 'outdoors'); } catch (e) { /* ignore */ }
|
||
setActiveProfile('outdoors');
|
||
setOutdoorsVariantAndSave(v);
|
||
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
|
||
setIndoorMode('off');
|
||
setIndoorManaged(false);
|
||
}}
|
||
/>
|
||
<${CustomSelect}
|
||
key="activities"
|
||
value=${activityValue}
|
||
isOn=${activeProfile === 'outdoors' && activityVariantKeys.includes(outdoorsVariant)}
|
||
noHide=${true}
|
||
hideLabel="🎯 Activities"
|
||
buttonLabel=${activityValue === 'off' ? '🎯 Activities' : ` ${activityLabel}`}
|
||
options=${activityOptions}
|
||
onChange=${(v) => {
|
||
if (v === 'off') { activateProfile('basic'); return; }
|
||
if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) {
|
||
setProPromptSource(`variant:${v}`);
|
||
setProPromptDay(0);
|
||
return;
|
||
}
|
||
try { localStorage.setItem('sunscope_profile', 'outdoors'); } catch (e) { /* ignore */ }
|
||
setActiveProfile('outdoors');
|
||
setOutdoorsVariantAndSave(v);
|
||
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
|
||
setIndoorMode('off');
|
||
setIndoorManaged(false);
|
||
}}
|
||
/>
|
||
<${CustomSelect}
|
||
key="work"
|
||
value=${workValue}
|
||
isOn=${activeProfile === 'farming' || (activeProfile === 'outdoors' && workVariantKeys.includes(outdoorsVariant))}
|
||
noHide=${true}
|
||
hideLabel="💼 Work"
|
||
buttonLabel=${workValue === 'off' ? '💼 Work' : ` ${workLabel}`}
|
||
options=${workOptions}
|
||
onChange=${(v) => {
|
||
if (v === 'off') { activateProfile('basic'); return; }
|
||
if (v === 'farming') { activateProfile('farming'); return; }
|
||
if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) {
|
||
setProPromptSource(`variant:${v}`);
|
||
setProPromptDay(0);
|
||
return;
|
||
}
|
||
try { localStorage.setItem('sunscope_profile', 'outdoors'); } catch (e) { /* ignore */ }
|
||
setActiveProfile('outdoors');
|
||
setOutdoorsVariantAndSave(v);
|
||
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
|
||
if (v === 'office') {
|
||
setBuildingType('office');
|
||
setIndoorMode('on');
|
||
setIndoorManaged(false);
|
||
} else {
|
||
setIndoorMode('off');
|
||
setIndoorManaged(false);
|
||
}
|
||
}}
|
||
/>
|
||
<span class="profile-divider" aria-hidden="true"></span>
|
||
${profileButtonOrder.slice(3).map((key) => {
|
||
const profile = FILTER_PROFILES[key];
|
||
const locked = profile.proOnly && !isPro;
|
||
return html`
|
||
<button
|
||
key=${key}
|
||
class=${`profile-btn${activeProfile === key ? ' active' : ''}${locked ? ' locked' : ''}`}
|
||
style=${{ opacity: locked ? 0.6 : 1, cursor: locked ? 'not-allowed' : 'pointer', position: 'relative' }}
|
||
title=${locked ? `🔒 ${profile.label} is part of SunScope Extra` : ''}
|
||
onClick=${() => {
|
||
if (locked) {
|
||
setProPromptSource(`profile:${key}`);
|
||
setProPromptDay(0);
|
||
return;
|
||
}
|
||
activateProfile(key);
|
||
}}
|
||
>${profile.icon} ${profile.label}${locked ? ' 🔒' : ''}</button>`;
|
||
})}
|
||
</div>
|
||
|
||
</${Fragment}>`;
|
||
}
|