From 15c123f402ddfb862e1735f4452da832658458bd Mon Sep 17 00:00:00 2001 From: noahwilde <47438354+noahwilde@users.noreply.github.com> Date: Mon, 15 Sep 2025 00:54:06 -0500 Subject: [PATCH] feat: add overdue presets and dark mode --- server/state_server.py | 14 +++ server/web/app.js | 191 ++++++++++++++++++++++++++++++++++++++--- server/web/index.html | 32 +++++-- server/web/style.css | 120 ++++++++++++++++++++++++-- 4 files changed, 330 insertions(+), 27 deletions(-) diff --git a/server/state_server.py b/server/state_server.py index 8c8d8e3..da0eae2 100644 --- a/server/state_server.py +++ b/server/state_server.py @@ -216,6 +216,20 @@ def do_POST(self): except (KeyError, ValueError, TypeError): pass self.send_error(400, "Bad Request") + elif self.path == "/schedule/delete": + try: + chip = int(data["chip"]) + pin = int(data["pin"]) + for s in list(SCHEDULES): + if s["chip"] == chip and s["pin"] == pin: + SCHEDULES.remove(s) + save_schedules() + self._set_json_headers() + self.wfile.write(b"{}") + return + except (KeyError, ValueError, TypeError): + pass + self.send_error(400, "Bad Request") else: self.send_error(404, "Not Found") diff --git a/server/web/app.js b/server/web/app.js index 9dc3940..0927ac6 100644 --- a/server/web/app.js +++ b/server/web/app.js @@ -1,11 +1,58 @@ const API_BASE = `${window.location.protocol}//${window.location.hostname}:5000`; -function fillSelect(select, max) { - for (let i = 0; i < max; i++) { - const opt = document.createElement('option'); - opt.value = i; - opt.textContent = i; - select.appendChild(opt); +const PIN_LAYOUT = [ + [ [0,1], [0,0], [1,1], [1,0], [2,1], [2,0] ], + [ [0,3], [0,2], [1,3], [1,2], [2,3], [2,2] ], + [ [0,5], [0,4], [1,5], [1,4], [2,5], [2,4] ], +]; + +const PIN_LABELS = [ + '1','2','3','4','5','6', + '7','8','9','10','11','12', + '13','14','15','16','17','18' +]; + +const scheduleMap = {}; + +function initTheme() { + const stored = localStorage.getItem('theme'); + const preferDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const useDark = stored ? stored === 'dark' : preferDark; + document.body.classList.toggle('dark', useDark); +} + +function buildGrid() { + const grid = document.getElementById('pin-grid'); + PIN_LAYOUT.flat().forEach(([chip, pin], idx) => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.textContent = PIN_LABELS[idx]; + btn.dataset.chip = chip; + btn.dataset.pin = pin; + btn.addEventListener('click', () => handlePinClick(chip, pin)); + grid.appendChild(btn); + }); +} + +function selectPin(chip, pin) { + document.getElementById('chip').value = chip; + document.getElementById('pin').value = pin; + document.querySelectorAll('#pin-grid button').forEach(b => b.classList.remove('selected')); + const btn = document.querySelector(`#pin-grid button[data-chip="${chip}"][data-pin="${pin}"]`); + if (btn) btn.classList.add('selected'); +} + +function handlePinClick(chip, pin) { + selectPin(chip, pin); + const sched = scheduleMap[`${chip}-${pin}`]; + if (sched) { + populateForm(sched); + } else { + document.getElementById('form').reset(); + document.querySelectorAll('.repeat-presets button, .overdue-presets button').forEach(b => b.classList.remove('active')); + document.getElementById('repeat-advanced').classList.add('hidden'); + selectPin(chip, pin); + document.getElementById('delete').classList.add('hidden'); } } @@ -14,33 +61,122 @@ async function fetchSchedules() { const data = await res.json(); const tbody = document.querySelector('#schedule-list tbody'); tbody.innerHTML = ''; + Object.keys(scheduleMap).forEach(k => delete scheduleMap[k]); + document.querySelectorAll('#pin-grid button').forEach(b => b.classList.remove('scheduled')); const schedules = data.schedules.slice().reverse(); schedules.forEach(s => { + scheduleMap[`${s.chip}-${s.pin}`] = s; const tr = document.createElement('tr'); const due = new Date(s.due).toLocaleString(); - tr.innerHTML = `${s.name}${s.chip}${s.pin}` + + tr.innerHTML = `${s.name}${labelFor(s.chip, s.pin)}` + `${due}` + `${formatInterval(s.repeat)}${formatInterval(s.overdue)}`; tr.addEventListener('click', () => populateForm(s)); tbody.appendChild(tr); + const btn = document.querySelector(`#pin-grid button[data-chip="${s.chip}"][data-pin="${s.pin}"]`); + if (btn) btn.classList.add('scheduled'); }); } +function labelFor(chip, pin) { + const idx = PIN_LAYOUT.flat().findIndex(([c,p]) => c === chip && p === pin); + return PIN_LABELS[idx] || `${chip}/${pin}`; +} + function formatInterval(obj) { return Object.entries(obj || {}).map(([k, v]) => `${v} ${k}`).join(' '); } function populateForm(s) { - document.getElementById('chip').value = s.chip; - document.getElementById('pin').value = s.pin; + selectPin(s.chip, s.pin); document.getElementById('name').value = s.name || ''; const dueDate = new Date(s.due); dueDate.setMinutes(dueDate.getMinutes() - dueDate.getTimezoneOffset()); document.getElementById('due').value = dueDate.toISOString().slice(0,16); + const adv = document.getElementById('repeat-advanced'); + adv.classList.add('hidden'); document.getElementById('repeat-days').value = s.repeat?.days || ''; document.getElementById('repeat-hours').value = s.repeat?.hours || ''; + document.querySelectorAll('.repeat-presets button').forEach(b => b.classList.remove('active')); + if (s.repeat) { + if ((s.repeat.days === 1 || s.repeat.days === 7) && !s.repeat.hours) { + const type = s.repeat.days === 1 ? 'daily' : 'weekly'; + const btn = document.querySelector(`.repeat-presets button[data-repeat="${type}"]`); + if (btn) btn.classList.add('active'); + } else { + adv.classList.remove('hidden'); + } + } document.getElementById('overdue-days').value = s.overdue?.days || ''; document.getElementById('overdue-hours').value = s.overdue?.hours || ''; + document.querySelectorAll('.overdue-presets button').forEach(b => b.classList.remove('active')); + if (s.overdue) { + if (s.overdue.days === 1 && !s.overdue.hours) { + const btn = document.querySelector('.overdue-presets button[data-overdue="1d"]'); + if (btn) btn.classList.add('active'); + } else if (s.overdue.days === 3 && !s.overdue.hours) { + const btn = document.querySelector('.overdue-presets button[data-overdue="3d"]'); + if (btn) btn.classList.add('active'); + } + } + document.getElementById('delete').classList.remove('hidden'); +} + +function applyDuePreset(type) { + const input = document.getElementById('due'); + const now = new Date(); + let target = new Date(now); + switch(type) { + case 'morning': + target.setHours(8,0,0,0); + if (target <= now) target.setDate(target.getDate()+1); + break; + case 'evening': + target.setHours(18,0,0,0); + if (target <= now) target.setDate(target.getDate()+1); + break; + case 'plus1d': + target = new Date(now.getTime() + 24*60*60*1000); + break; + case 'plus3d': + target = new Date(now.getTime() + 3*24*60*60*1000); + break; + case 'nextweek': + target = new Date(now.getTime() + 7*24*60*60*1000); + break; + default: + return; + } + target.setMinutes(target.getMinutes() - target.getTimezoneOffset()); + input.value = target.toISOString().slice(0,16); +} + +function toggleRepeatPreset(btn) { + const type = btn.dataset.repeat; + const active = btn.classList.contains('active'); + document.querySelectorAll('.repeat-presets button[data-repeat]').forEach(b => b.classList.remove('active')); + const adv = document.getElementById('repeat-advanced'); + adv.classList.add('hidden'); + document.getElementById('repeat-days').value = ''; + document.getElementById('repeat-hours').value = ''; + if (!active) { + btn.classList.add('active'); + if (type === 'daily') document.getElementById('repeat-days').value = 1; + if (type === 'weekly') document.getElementById('repeat-days').value = 7; + } +} + +function toggleOverduePreset(btn) { + const type = btn.dataset.overdue; + const active = btn.classList.contains('active'); + document.querySelectorAll('.overdue-presets button').forEach(b => b.classList.remove('active')); + document.getElementById('overdue-days').value = ''; + document.getElementById('overdue-hours').value = ''; + if (!active) { + btn.classList.add('active'); + if (type === '1d') document.getElementById('overdue-days').value = 1; + if (type === '3d') document.getElementById('overdue-days').value = 3; + } } async function submitForm(e) { @@ -69,10 +205,43 @@ async function submitForm(e) { body: JSON.stringify(payload) }); e.target.reset(); + document.querySelectorAll('.repeat-presets button, .overdue-presets button').forEach(b => b.classList.remove('active')); + document.getElementById('repeat-advanced').classList.add('hidden'); + fetchSchedules(); +} + +async function deleteSchedule() { + const chip = parseInt(document.getElementById('chip').value, 10); + const pin = parseInt(document.getElementById('pin').value, 10); + await fetch(`${API_BASE}/schedule/delete`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ chip, pin }) + }); + document.getElementById('form').reset(); + document.querySelectorAll('.repeat-presets button, .overdue-presets button').forEach(b => b.classList.remove('active')); + document.getElementById('repeat-advanced').classList.add('hidden'); fetchSchedules(); } -fillSelect(document.getElementById('chip'), 3); -fillSelect(document.getElementById('pin'), 6); +initTheme(); +buildGrid(); fetchSchedules(); document.getElementById('form').addEventListener('submit', submitForm); +document.getElementById('delete').addEventListener('click', deleteSchedule); +document.getElementById('show-advanced').addEventListener('click', () => { + document.getElementById('repeat-advanced').classList.remove('hidden'); +}); +document.getElementById('theme-toggle').addEventListener('click', () => { + document.body.classList.toggle('dark'); + localStorage.setItem('theme', document.body.classList.contains('dark') ? 'dark' : 'light'); +}); +document.querySelectorAll('.quick-due button').forEach(btn => { + btn.addEventListener('click', () => applyDuePreset(btn.dataset.due)); +}); +document.querySelectorAll('.repeat-presets button[data-repeat]').forEach(btn => { + btn.addEventListener('click', () => toggleRepeatPreset(btn)); +}); +document.querySelectorAll('.overdue-presets button').forEach(btn => { + btn.addEventListener('click', () => toggleOverduePreset(btn)); +}); diff --git a/server/web/index.html b/server/web/index.html index 5cf0a33..c1ad074 100644 --- a/server/web/index.html +++ b/server/web/index.html @@ -2,16 +2,18 @@ + Chore Schedules
+

Chore Schedules

- +
NameChipPinDueRepeatOverdue
NameSlotDueRepeatOverdue
@@ -22,26 +24,41 @@

Create / Update Schedule

- - +
+ + +
+ + + + + +
Repeat (optional) +
+ + + +
+
Overdue after (optional) +
+ + +
@@ -50,6 +67,7 @@

Create / Update Schedule

+
diff --git a/server/web/style.css b/server/web/style.css index c345651..69158df 100644 --- a/server/web/style.css +++ b/server/web/style.css @@ -1,17 +1,31 @@ +:root { + --bg: #f5f5f5; + --text: #333; + --card-bg: #fff; + --border: #ddd; + --primary: #007bff; + --primary-hover: #0056b3; + --danger: #dc3545; + --danger-hover: #a71d2a; + --scheduled-bg: #28a745; + --scheduled-color: #fff; +} + body { font-family: system-ui, sans-serif; - margin: 2rem; - background: #f5f5f5; - color: #333; + margin: 0; + background: var(--bg); + color: var(--text); } .container { max-width: 800px; margin: auto; - background: #fff; - padding: 2rem; + background: var(--card-bg); + padding: 1rem 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); + position: relative; } table { @@ -22,7 +36,7 @@ table { table th, table td { padding: 0.5rem 0.75rem; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--border); text-align: left; } @@ -33,21 +47,109 @@ form label { } fieldset { - border: 1px solid #ddd; + border: 1px solid var(--border); border-radius: 4px; padding: 1rem; margin-bottom: 1rem; } +.pin-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 4px; + margin-bottom: 1rem; +} + +.pin-grid button { + padding: 0.5rem; + font-size: 0.8rem; +} + +.pin-grid button.scheduled { + background: var(--scheduled-bg); + color: var(--scheduled-color); +} + +.pin-grid button.selected { + outline: 3px solid var(--primary-hover); +} + +.quick-due, .repeat-presets, .overdue-presets { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 1rem; +} + +.hidden { + display: none; +} + +.danger { + background: var(--danger); +} + +.danger:hover { + background: var(--danger-hover); +} + button { padding: 0.5rem 1rem; border: none; - background: #007bff; + background: var(--primary); color: #fff; border-radius: 4px; cursor: pointer; } button:hover { - background: #0056b3; + background: var(--primary-hover); +} + +.repeat-presets button.active, +.overdue-presets button.active { + background: var(--primary-hover); +} + +.theme-toggle { + position: absolute; + top: 0.5rem; + right: 0.5rem; } + +body.dark { + --bg: #111; + --text: #eee; + --card-bg: #222; + --border: #444; + --primary: #3399ff; + --primary-hover: #66b3ff; + --danger: #ff5c5c; + --danger-hover: #cc0000; + --scheduled-bg: #2e8b57; +} + +@media (max-width: 600px) { + .container { + max-width: 100%; + margin: 0; + border-radius: 0; + box-shadow: none; + padding: 1rem; + } + + .pin-grid { + gap: 2px; + } + + .pin-grid button { + font-size: 0.9rem; + } + + .quick-due button, + .repeat-presets button, + .overdue-presets button { + flex: 1; + } +} +