From 350dafe591cde934913fad6d65d71979d77763d7 Mon Sep 17 00:00:00 2001 From: Theo Weijmer Date: Sun, 13 Sep 2026 10:27:09 +0200 Subject: [PATCH] feat: tik op een module om die bovenaan te zetten met een groter temperatuurgetal Tik op een van de drie binnenmodules zet die bovenaan de lijst (de overige twee behouden hun onderlinge volgorde) en vergroot alleen het temperatuurgetal van 50 naar 92px. Nogmaals tikken op de bovenste (vastgezette) module zet 'm weer los. Puur client-side, geen nieuwe API-velden: state.pinnedRoomId + de pure functie pinRoom() (met tests) in format.js. De keuze overleeft de 5-minuten-verversing, reset bij een volledige herlaad. Servicewerker naar v6. Co-Authored-By: Claude Sonnet 5 --- docs/weerscherm-spec.md | 4 ++++ public/app.js | 22 ++++++++++++++++------ public/format.js | 14 ++++++++++++++ public/styles.css | 3 ++- public/sw.js | 2 +- tests/js/format.test.js | 23 ++++++++++++++++++++++- 6 files changed, 59 insertions(+), 9 deletions(-) diff --git a/docs/weerscherm-spec.md b/docs/weerscherm-spec.md index 29c98d0..6b9b5c5 100644 --- a/docs/weerscherm-spec.md +++ b/docs/weerscherm-spec.md @@ -252,6 +252,10 @@ De iconen `sun`, `partly`, `rain` en `showers` staan in de mockup. Maak `cloud`, - Daaronder de gradiëntbalk van `--cold` naar `--warm`. De marker staat op `(temp − min) / (max − min)`, begrensd op 0–100%. Bij `max == min` staat hij op 50%. - Onder de balk: "9,2° laagst om 06:10" en "19,8° hoogst om 13:05". - **Binnen (rechts):** 3 rijen met naam, temperatuur (1 decimaal) en "↓ min° ↑ max°". Een onbereikbare module toont "–" in `--muted`. + _Toegevoegd 2026-09-13: een module is tikbaar. Een tik zet die module bovenaan de lijst (de overige twee + behouden hun onderlinge volgorde) en vergroot alleen het temperatuurgetal van 50 naar 92px. Nogmaals tikken + op de bovenste (vastgezette) module maakt hem weer los. De keuze blijft staan bij de 5-minuten-verversing en + reset bij een volledige herlaad. Zie `pinRoom()` in `public/format.js`._ - **Regen (linksonder):** - "Vandaag" groot, in mm met 1 decimaal. - Daarnaast 4 staafjes: de 3 vorige dagen (labels "Di", "Wo", "Do") en vandaag, uitgelicht. De hoogte is `mm / max(4 waarden) × 70px`, met een minimum van 2px. Zijn alle waarden 0, dan zijn alle staafjes 2px. diff --git a/public/app.js b/public/app.js index 903dfe3..12dfe15 100644 --- a/public/app.js +++ b/public/app.js @@ -2,7 +2,7 @@ import { f1, fInt, fmm, markerPos, barHeights, wmoIcon, trendLabel, formatDate, formatTime, dayAbbr, monthName, isOlderThan, msUntil, co2Level, fPct, fHpa, seriesRange, sparklinePath, - windArrowDeg, fKmh, formatHour, formatFullDate, + windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, } from './format.js'; import { icon, windArrow } from './icons.js'; @@ -22,10 +22,11 @@ const stage = $('stage'); const viewport = $('viewport'); const state = { - data: null, // laatste payload, ook als die uit de SW-cache komt + data: null, // laatste payload, ook als die uit de SW-cache komt tz: FALLBACK_TZ, - lastOk: null, // Date van de laatste geslaagde live-fetch - offlineSince: null // Date vanaf wanneer er geen verbinding is + lastOk: null, // Date van de laatste geslaagde live-fetch + offlineSince: null, // Date vanaf wanneer er geen verbinding is + pinnedRoomId: null, // id van de module die vastgezet is bovenaan; blijft staan tot opnieuw getikt of een volledige herlaad }; /* ---------- schaal ---------- */ @@ -71,11 +72,13 @@ function renderOutdoor(o) { } function renderRooms(rooms) { - const html = rooms.map((r) => { + const ordered = pinRoom(rooms, state.pinnedRoomId); + const html = ordered.map((r) => { const off = !r.reachable || r.temp === null; const co2 = co2Level(off ? null : r.co2); const pill = `${co2.level === 'unknown' ? '–' : r.co2 + ' ppm'}`; - return `
+ const pinned = r.id === state.pinnedRoomId; + return `

${esc(r.name)}${pill}

${off ? '–' : f1(r.temp) + '°'}

↓ ${off ? '–' : f1(r.min) + '°'}↑ ${off ? '–' : f1(r.max) + '°'}RV ${off ? '–' : fPct(r.humidity)}

@@ -318,6 +321,13 @@ $('forecast').addEventListener('click', (e) => { }); $('hour-overlay').addEventListener('click', closeHourOverlay); +$('rooms').addEventListener('click', (e) => { + const room = e.target.closest('.a-room'); + if (!room || !state.data) return; + state.pinnedRoomId = state.pinnedRoomId === room.dataset.id ? null : room.dataset.id; + renderRooms(state.data.indoor); +}); + window.addEventListener('resize', fit); window.addEventListener('orientationchange', () => setTimeout(fit, 50)); document.addEventListener('visibilitychange', () => { diff --git a/public/format.js b/public/format.js index d823be5..d2c1e11 100644 --- a/public/format.js +++ b/public/format.js @@ -205,3 +205,17 @@ export function formatFullDate(dateStr, tz) { if (Number.isNaN(d.getTime())) return DASH; return formatDate(d, tz); } + +/** + * Herschikt kamers zodat de kamer met `pinnedId` vooraan staat; de rest behoudt de onderlinge volgorde. + * Zonder pinnedId, of als die niet voorkomt of al vooraan staat, blijft de oorspronkelijke array (referentie) terug. + */ +export function pinRoom(rooms, pinnedId) { + if (!pinnedId) return rooms; + const index = rooms.findIndex((r) => r.id === pinnedId); + if (index <= 0) return rooms; + const copy = rooms.slice(); + const [pinned] = copy.splice(index, 1); + copy.unshift(pinned); + return copy; +} diff --git a/public/styles.css b/public/styles.css index 05c08ad..798f6c8 100644 --- a/public/styles.css +++ b/public/styles.css @@ -108,8 +108,9 @@ html, body { .pill.medium { background: var(--warm); } .pill.bad { background: var(--bad); } .pill.unknown { background: var(--line); color: var(--muted); } -.a-rtemp { font-size: 50px; font-weight: 300; line-height: 1.05; } +.a-rtemp { font-size: 50px; font-weight: 300; line-height: 1.05; transition: font-size .25s ease; } .a-room.off .a-rtemp { color: var(--muted); } +.a-room.pinned .a-rtemp { font-size: 92px; } .a-rmm { font-size: 17px; display: flex; gap: 18px; } .a-rmm .rv { color: var(--muted); } diff --git a/public/sw.js b/public/sw.js index 042f5d9..f5ab7c7 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,6 +1,6 @@ /* Service worker: app-shell cache-first, API network-first met timeout en fallback op de laatste response. */ -const VERSION = 'weerscherm-v5'; +const VERSION = 'weerscherm-v6'; const SHELL = [ './', './index.html', diff --git a/tests/js/format.test.js b/tests/js/format.test.js index ed81746..28d8cda 100644 --- a/tests/js/format.test.js +++ b/tests/js/format.test.js @@ -121,7 +121,7 @@ test('msUntil computes the wait until the next local time', () => { import { co2Level, fPct, fHpa, seriesRange, sparklinePath, sparklinePoints, - windArrowDeg, fKmh, formatHour, formatFullDate, + windArrowDeg, fKmh, formatHour, formatFullDate, pinRoom, } from '../../public/format.js'; test('co2Level uses the Netatmo thresholds', () => { @@ -187,3 +187,24 @@ test('formatFullDate matches formatDate for the same calendar date', () => { assert.equal(formatFullDate('2026-09-14', TZ), 'Maandag 14 september'); assert.equal(formatFullDate('kapot', TZ), '–'); }); + +test('pinRoom moves the matching room to the front, keeping the others in order', () => { + const rooms = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]; + assert.deepEqual(pinRoom(rooms, 'b').map((r) => r.id), ['b', 'a', 'c']); + assert.deepEqual(pinRoom(rooms, 'c').map((r) => r.id), ['c', 'a', 'b']); +}); + +test('pinRoom returns the original array unchanged without a match, id, or when already first', () => { + const rooms = [{ id: 'a' }, { id: 'b' }]; + assert.equal(pinRoom(rooms, null), rooms); + assert.equal(pinRoom(rooms, undefined), rooms); + assert.equal(pinRoom(rooms, 'x'), rooms, 'onbekend id: geen match, ongewijzigd terug'); + assert.equal(pinRoom(rooms, 'a'), rooms, 'al vooraan: geen kopie nodig'); +}); + +test('pinRoom does not mutate the input array', () => { + const rooms = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]; + const original = rooms.slice(); + pinRoom(rooms, 'c'); + assert.deepEqual(rooms, original); +});