2.2.3
Mobile fixes
This commit is contained in:
+103
-42
@@ -21,6 +21,7 @@
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import { h, render, Fragment } from '../vendor/preact.js';
|
||||
import { useState, useRef, useEffect } from '../vendor/preact-hooks.js';
|
||||
import htm from '../vendor/htm.js';
|
||||
import {
|
||||
utciCategory, UTCI_BANDS, bandGradient,
|
||||
@@ -45,7 +46,7 @@ export function UTCIForecast() {
|
||||
const {
|
||||
location, setLocationAndSave,
|
||||
forecast, airQuality, loading, error, now, fetchedAt,
|
||||
searchQuery, setSearchQuery, searchResults, searching,
|
||||
searchQuery, setSearchQuery, searchResults, setSearchResults, searching,
|
||||
selectedDay, setSelectedDay,
|
||||
proPromptDay, setProPromptDay,
|
||||
proPromptSource, setProPromptSource,
|
||||
@@ -84,7 +85,38 @@ export function UTCIForecast() {
|
||||
bannerVisible, bannerStageRef,
|
||||
bannerSlideTo, dismissBanner,
|
||||
} = useAppState();
|
||||
|
||||
|
||||
// Search is hidden by default and revealed via the magnifier next to the
|
||||
// location. The input then overlays the location line to save space.
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const searchWrapRef = useRef(null);
|
||||
const searchInputRef = useRef(null);
|
||||
|
||||
// Focus the input the moment the search opens.
|
||||
useEffect(() => {
|
||||
if (searchOpen) searchInputRef.current?.focus();
|
||||
}, [searchOpen]);
|
||||
|
||||
// Close the search on outside-click or Escape, clearing any stray query.
|
||||
useEffect(() => {
|
||||
if (!searchOpen) return;
|
||||
const onDown = (e) => {
|
||||
if (searchWrapRef.current && !searchWrapRef.current.contains(e.target)) {
|
||||
setSearchOpen(false);
|
||||
setSearchQuery('');
|
||||
}
|
||||
};
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'Escape') { setSearchOpen(false); setSearchQuery(''); }
|
||||
};
|
||||
document.addEventListener('mousedown', onDown);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDown);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
};
|
||||
}, [searchOpen, setSearchQuery]);
|
||||
|
||||
// Columns that mark the start of a logical group - used to draw a faint
|
||||
// vertical border separating groups in the forecast table.
|
||||
const GROUP_ORDER = {
|
||||
@@ -317,48 +349,77 @@ export function UTCIForecast() {
|
||||
<span class="title-sun">SUN</span><span class="title-scope">Scope</span>
|
||||
</h1>
|
||||
<div class="utci-tagline">See the sun the way your skin does.</div>
|
||||
<div class="utci-search-wrap">
|
||||
<input
|
||||
class="utci-search"
|
||||
type="text"
|
||||
placeholder="Search any town or city…"
|
||||
value=${searchQuery}
|
||||
onInput=${(e) => setSearchQuery(e.currentTarget.value)}
|
||||
/>
|
||||
${searchResults.length > 0 && html`
|
||||
<div class="utci-results">
|
||||
${searchResults.map((r) => html`
|
||||
<div
|
||||
key=${`${r.id}-${r.latitude}`}
|
||||
class="utci-result"
|
||||
onClick=${() => {
|
||||
setLocationAndSave({
|
||||
name: `${r.name}${r.admin1 ? ', ' + r.admin1 : ''}`,
|
||||
lat: r.latitude,
|
||||
lon: r.longitude,
|
||||
country: r.country_code,
|
||||
});
|
||||
setSearchQuery('');
|
||||
setSearchResults([]);
|
||||
setSelectedDay(0);
|
||||
}}
|
||||
>
|
||||
<div>${r.name}${r.admin1 ? `, ${r.admin1}` : ''}</div>
|
||||
<div class="utci-result-meta">
|
||||
${r.country} · ${r.latitude.toFixed(2)}°, ${r.longitude.toFixed(2)}°
|
||||
</div>
|
||||
</div>`)}
|
||||
<div class="utci-loc-search" ref=${searchWrapRef}>
|
||||
<div class="utci-current-loc">
|
||||
<button
|
||||
type="button"
|
||||
class="utci-loc-name"
|
||||
aria-label="Search for a town or city"
|
||||
aria-expanded=${searchOpen}
|
||||
onClick=${() => setSearchOpen((v) => !v)}
|
||||
>
|
||||
<svg class="utci-loc-pin" viewBox="0 0 24 24" width="15" height="15" 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>
|
||||
${location.name}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="utci-search-toggle"
|
||||
aria-label="Search for a town or city"
|
||||
aria-expanded=${searchOpen}
|
||||
onClick=${() => setSearchOpen((v) => !v)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
||||
<circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<line x1="15.5" y1="15.5" x2="21" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<span class="utci-loc-coords">
|
||||
${location.lat.toFixed(3)}°, ${location.lon.toFixed(3)}°
|
||||
</span>
|
||||
</div>
|
||||
${fetchedAt && !loading && html`
|
||||
<div class="utci-fetch-time">Last update: ${fetchedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>`}
|
||||
${searchOpen && html`
|
||||
<div class="utci-search-wrap utci-search-overlay">
|
||||
<input
|
||||
ref=${searchInputRef}
|
||||
class="utci-search"
|
||||
type="text"
|
||||
placeholder="Search any town or city…"
|
||||
value=${searchQuery}
|
||||
onInput=${(e) => setSearchQuery(e.currentTarget.value)}
|
||||
/>
|
||||
${searchResults.length > 0 && html`
|
||||
<div class="utci-results">
|
||||
${searchResults.map((r) => html`
|
||||
<div
|
||||
key=${`${r.id}-${r.latitude}`}
|
||||
class="utci-result"
|
||||
onClick=${() => {
|
||||
setLocationAndSave({
|
||||
name: `${r.name}${r.admin1 ? ', ' + r.admin1 : ''}`,
|
||||
lat: r.latitude,
|
||||
lon: r.longitude,
|
||||
country: r.country_code,
|
||||
});
|
||||
setSearchQuery('');
|
||||
setSearchResults([]);
|
||||
setSelectedDay(0);
|
||||
setSearchOpen(false);
|
||||
}}
|
||||
>
|
||||
<div>${r.name}${r.admin1 ? `, ${r.admin1}` : ''}</div>
|
||||
<div class="utci-result-meta">
|
||||
${r.country} · ${r.latitude.toFixed(2)}°, ${r.longitude.toFixed(2)}°
|
||||
</div>
|
||||
</div>`)}
|
||||
</div>`}
|
||||
${searching && html`<div class="utci-searching">Searching…</div>`}
|
||||
</div>`}
|
||||
${searching && html`<div class="utci-searching">Searching…</div>`}
|
||||
</div>
|
||||
<div class="utci-current-loc">
|
||||
↳ ${location.name}
|
||||
<span class="utci-loc-coords">
|
||||
${location.lat.toFixed(3)}°, ${location.lon.toFixed(3)}°
|
||||
</span>
|
||||
</div>
|
||||
${fetchedAt && !loading && html`
|
||||
<div class="utci-fetch-time">Last update: ${fetchedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>`}
|
||||
</div>
|
||||
|
||||
<div class="scope-col">
|
||||
|
||||
Reference in New Issue
Block a user