feat: Implement core frontend application with task management, user selection, and a collapsible sidebar.
This commit is contained in:
@@ -144,7 +144,7 @@
|
|||||||
<div class="w-full max-w-6xl mx-auto space-y-8">
|
<div class="w-full max-w-6xl mx-auto space-y-8">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="text-2xl font-bold text-accent">Calendink Provider 🚀🚀👑</h1>
|
<h1 class="text-2xl font-bold text-accent">Calendink Provider 🚀👑🥸</h1>
|
||||||
<p class="text-text-secondary text-sm">ESP32-S3 System Dashboard v{__APP_VERSION__}</p>
|
<p class="text-text-secondary text-sm">ESP32-S3 System Dashboard v{__APP_VERSION__}</p>
|
||||||
|
|
||||||
<!-- Status Badge -->
|
<!-- Status Badge -->
|
||||||
@@ -175,6 +175,39 @@
|
|||||||
|
|
||||||
{#if currentView === 'dashboard'}
|
{#if currentView === 'dashboard'}
|
||||||
<!-- Dashboard View -->
|
<!-- Dashboard View -->
|
||||||
|
|
||||||
|
<!-- Upcoming Tasks Section (top priority) -->
|
||||||
|
{#if upcomingData.users.length > 0}
|
||||||
|
<div class="bg-bg-card border border-border rounded-xl overflow-hidden shadow-xl">
|
||||||
|
<div class="px-5 py-3 border-b border-border">
|
||||||
|
<h2 class="text-sm font-semibold text-text-primary uppercase tracking-wider">
|
||||||
|
📋 Upcoming Tasks
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-0 divide-y md:divide-y-0 md:divide-x divide-border">
|
||||||
|
{#each upcomingData.users as user}
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="text-xs font-bold text-accent mb-3 uppercase tracking-wider">{user.name}</h3>
|
||||||
|
{#if user.tasks.length === 0}
|
||||||
|
<p class="text-[11px] text-text-secondary italic">No pending tasks</p>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each user.tasks as task}
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<span class="text-[10px] mt-0.5 {isOverdue(task.due_date) ? 'text-danger' : 'text-text-secondary'} font-mono whitespace-nowrap">
|
||||||
|
{formatRelativeDate(task.due_date)}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-text-primary leading-tight">{task.title}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- 2-Column Grid Layout -->
|
<!-- 2-Column Grid Layout -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
|
||||||
|
|
||||||
@@ -285,38 +318,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upcoming Tasks Section -->
|
|
||||||
{#if upcomingData.users.length > 0}
|
|
||||||
<div class="bg-bg-card border border-border rounded-xl overflow-hidden shadow-xl">
|
|
||||||
<div class="px-5 py-3 border-b border-border">
|
|
||||||
<h2 class="text-sm font-semibold text-text-primary uppercase tracking-wider">
|
|
||||||
📋 Upcoming Tasks
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-0 divide-y md:divide-y-0 md:divide-x divide-border">
|
|
||||||
{#each upcomingData.users as user}
|
|
||||||
<div class="p-4">
|
|
||||||
<h3 class="text-xs font-bold text-accent mb-3 uppercase tracking-wider">{user.name}</h3>
|
|
||||||
{#if user.tasks.length === 0}
|
|
||||||
<p class="text-[11px] text-text-secondary italic">No pending tasks</p>
|
|
||||||
{:else}
|
|
||||||
<div class="space-y-2">
|
|
||||||
{#each user.tasks as task}
|
|
||||||
<div class="flex items-start gap-2">
|
|
||||||
<span class="text-[10px] mt-0.5 {isOverdue(task.due_date) ? 'text-danger' : 'text-text-secondary'} font-mono whitespace-nowrap">
|
|
||||||
{formatRelativeDate(task.due_date)}
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-text-primary leading-tight">{task.title}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{:else if currentView === 'tasks'}
|
{:else if currentView === 'tasks'}
|
||||||
<!-- Task Manager View -->
|
<!-- Task Manager View -->
|
||||||
<div class="bg-bg-card border border-border rounded-xl p-6 shadow-xl">
|
<div class="bg-bg-card border border-border rounded-xl p-6 shadow-xl">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
let { currentView = 'dashboard', onNavigate = () => {} } = $props();
|
let { currentView = 'dashboard', onNavigate = () => {} } = $props();
|
||||||
let collapsed = $state(false);
|
let collapsed = $state(typeof window !== 'undefined' && window.innerWidth <= 768);
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ id: 'dashboard', label: 'Dashboard', icon: '🏠' },
|
{ id: 'dashboard', label: 'Dashboard', icon: '🏠' },
|
||||||
|
|||||||
@@ -8,13 +8,15 @@
|
|||||||
|
|
||||||
// Add task form state
|
// Add task form state
|
||||||
let newTitle = $state('');
|
let newTitle = $state('');
|
||||||
let newDueDate = $state('');
|
let newDueDay = $state('');
|
||||||
|
let newDueTime = $state('');
|
||||||
let showAddForm = $state(false);
|
let showAddForm = $state(false);
|
||||||
|
|
||||||
// Edit state
|
// Edit state
|
||||||
let editingTaskId = $state(null);
|
let editingTaskId = $state(null);
|
||||||
let editTitle = $state('');
|
let editTitle = $state('');
|
||||||
let editDueDate = $state('');
|
let editDueDay = $state('');
|
||||||
|
let editDueTime = $state('');
|
||||||
|
|
||||||
// Confirm delete
|
// Confirm delete
|
||||||
let confirmDeleteId = $state(null);
|
let confirmDeleteId = $state(null);
|
||||||
@@ -45,13 +47,14 @@
|
|||||||
|
|
||||||
async function handleAddTask(e) {
|
async function handleAddTask(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!newTitle.trim() || !newDueDate) return;
|
if (!newTitle.trim() || !newDueDay || !newDueTime) return;
|
||||||
|
|
||||||
const dueTimestamp = Math.floor(new Date(newDueDate).getTime() / 1000);
|
const dueTimestamp = Math.floor(new Date(`${newDueDay}T${newDueTime}`).getTime() / 1000);
|
||||||
try {
|
try {
|
||||||
await addTask(selectedUserId, newTitle.trim(), dueTimestamp);
|
await addTask(selectedUserId, newTitle.trim(), dueTimestamp);
|
||||||
newTitle = '';
|
newTitle = '';
|
||||||
newDueDate = '';
|
newDueDay = '';
|
||||||
|
newDueTime = '';
|
||||||
showAddForm = false;
|
showAddForm = false;
|
||||||
await fetchTasks();
|
await fetchTasks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -71,14 +74,16 @@
|
|||||||
function startEditing(task) {
|
function startEditing(task) {
|
||||||
editingTaskId = task.id;
|
editingTaskId = task.id;
|
||||||
editTitle = task.title;
|
editTitle = task.title;
|
||||||
editDueDate = formatDateForInput(task.due_date);
|
const parts = formatDateForInput(task.due_date);
|
||||||
|
editDueDay = parts.day;
|
||||||
|
editDueTime = parts.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveEdit(e) {
|
async function saveEdit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!editTitle.trim()) return;
|
if (!editTitle.trim()) return;
|
||||||
|
|
||||||
const dueTimestamp = Math.floor(new Date(editDueDate).getTime() / 1000);
|
const dueTimestamp = Math.floor(new Date(`${editDueDay}T${editDueTime}`).getTime() / 1000);
|
||||||
try {
|
try {
|
||||||
await updateTask(editingTaskId, {
|
await updateTask(editingTaskId, {
|
||||||
title: editTitle.trim(),
|
title: editTitle.trim(),
|
||||||
@@ -103,7 +108,18 @@
|
|||||||
|
|
||||||
function formatDateForInput(timestamp) {
|
function formatDateForInput(timestamp) {
|
||||||
const d = new Date(timestamp * 1000);
|
const d = new Date(timestamp * 1000);
|
||||||
return d.toISOString().slice(0, 16); // "YYYY-MM-DDTHH:mm"
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const dayNum = String(d.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(d.getHours()).padStart(2, '0');
|
||||||
|
const mins = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
return { day: `${year}-${month}-${dayNum}`, time: `${hours}:${mins}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNowForNewTask() {
|
||||||
|
const parts = formatDateForInput(Date.now() / 1000);
|
||||||
|
newDueDay = parts.day;
|
||||||
|
newDueTime = parts.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRelativeDate(timestamp) {
|
function formatRelativeDate(timestamp) {
|
||||||
@@ -135,12 +151,13 @@
|
|||||||
<div class="task-header">
|
<div class="task-header">
|
||||||
<h2 class="task-title">Tasks</h2>
|
<h2 class="task-title">Tasks</h2>
|
||||||
{#if !showAddForm}
|
{#if !showAddForm}
|
||||||
<button class="add-task-btn" onclick={() => showAddForm = true}>+ Add Task</button>
|
<button class="add-task-btn" onclick={() => { showAddForm = true; setNowForNewTask(); }}>+ Add Task</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showAddForm}
|
{#if showAddForm}
|
||||||
<form class="add-task-form" onsubmit={handleAddTask}>
|
<form class="add-task-form" onsubmit={handleAddTask}>
|
||||||
|
<!-- svelte-ignore a11y_autofocus -->
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newTitle}
|
bind:value={newTitle}
|
||||||
@@ -148,14 +165,19 @@
|
|||||||
class="task-input"
|
class="task-input"
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
<input
|
<div class="date-time-row">
|
||||||
type="datetime-local"
|
<label class="date-time-field">
|
||||||
bind:value={newDueDate}
|
<span class="field-label">Date</span>
|
||||||
class="task-date-input"
|
<input type="date" bind:value={newDueDay} class="task-date-input" />
|
||||||
/>
|
</label>
|
||||||
|
<label class="date-time-field">
|
||||||
|
<span class="field-label">Time</span>
|
||||||
|
<input type="time" bind:value={newDueTime} class="task-time-input" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn-primary" disabled={!newTitle.trim() || !newDueDate}>Add</button>
|
<button type="submit" class="btn-primary" disabled={!newTitle.trim() || !newDueDay || !newDueTime}>Add</button>
|
||||||
<button type="button" class="btn-secondary" onclick={() => { showAddForm = false; newTitle = ''; newDueDate = ''; }}>Cancel</button>
|
<button type="button" class="btn-secondary" onclick={() => { showAddForm = false; newTitle = ''; newDueDay = ''; newDueTime = ''; }}>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -166,7 +188,16 @@
|
|||||||
{#if editingTaskId === task.id}
|
{#if editingTaskId === task.id}
|
||||||
<form class="edit-form" onsubmit={saveEdit}>
|
<form class="edit-form" onsubmit={saveEdit}>
|
||||||
<input type="text" bind:value={editTitle} class="task-input" />
|
<input type="text" bind:value={editTitle} class="task-input" />
|
||||||
<input type="datetime-local" bind:value={editDueDate} class="task-date-input" />
|
<div class="date-time-row">
|
||||||
|
<label class="date-time-field">
|
||||||
|
<span class="field-label">Date</span>
|
||||||
|
<input type="date" bind:value={editDueDay} class="task-date-input" />
|
||||||
|
</label>
|
||||||
|
<label class="date-time-field">
|
||||||
|
<span class="field-label">Time</span>
|
||||||
|
<input type="time" bind:value={editDueTime} class="task-time-input" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn-primary btn-sm">Save</button>
|
<button type="submit" class="btn-primary btn-sm">Save</button>
|
||||||
<button type="button" class="btn-secondary btn-sm" onclick={() => editingTaskId = null}>Cancel</button>
|
<button type="button" class="btn-secondary btn-sm" onclick={() => editingTaskId = null}>Cancel</button>
|
||||||
@@ -274,6 +305,29 @@
|
|||||||
border-color: var(--color-accent);
|
border-color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-time-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-time-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-time-field:first-child {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.task-date-input {
|
.task-date-input {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -283,9 +337,40 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
outline: none;
|
outline: none;
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-date-input:focus {
|
.task-time-input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
color-scheme: dark;
|
||||||
|
width: 110px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the native picker icon cover the entire input (Chrome/Edge only) */
|
||||||
|
.task-date-input::-webkit-calendar-picker-indicator,
|
||||||
|
.task-time-input::-webkit-calendar-picker-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-date-input:focus, .task-time-input:focus {
|
||||||
border-color: var(--color-accent);
|
border-color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,9 +59,13 @@
|
|||||||
<div class="user-bar">
|
<div class="user-bar">
|
||||||
<div class="user-chips">
|
<div class="user-chips">
|
||||||
{#each users as user}
|
{#each users as user}
|
||||||
<button
|
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||||
|
<div
|
||||||
class="user-chip {selectedUserId === user.id ? 'selected' : ''}"
|
class="user-chip {selectedUserId === user.id ? 'selected' : ''}"
|
||||||
onclick={() => selectedUserId = user.id}
|
onclick={() => selectedUserId = user.id}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onkeydown={(e) => { if (e.key === 'Enter') selectedUserId = user.id; }}
|
||||||
>
|
>
|
||||||
<span class="user-name">{user.name}</span>
|
<span class="user-name">{user.name}</span>
|
||||||
{#if confirmDeleteId === user.id}
|
{#if confirmDeleteId === user.id}
|
||||||
@@ -76,11 +80,12 @@
|
|||||||
title="Remove user"
|
title="Remove user"
|
||||||
>✕</button>
|
>✕</button>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if showAddForm}
|
{#if showAddForm}
|
||||||
<form class="add-user-form" onsubmit={(e) => { e.preventDefault(); handleAddUser(); }}>
|
<form class="add-user-form" onsubmit={(e) => { e.preventDefault(); handleAddUser(); }}>
|
||||||
|
<!-- svelte-ignore a11y_autofocus -->
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newUserName}
|
bind:value={newUserName}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"major": 0,
|
"major": 0,
|
||||||
"minor": 1,
|
"minor": 1,
|
||||||
"revision": 14
|
"revision": 4
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
#include "soc/gpio_num.h"
|
#include "soc/gpio_num.h"
|
||||||
|
|
||||||
|
|
||||||
// Project headers
|
// Project headers
|
||||||
#include "appstate.hpp"
|
#include "appstate.hpp"
|
||||||
#include "types.hpp"
|
#include "types.hpp"
|
||||||
@@ -45,25 +44,23 @@ extern "C" void app_main()
|
|||||||
nvs_handle_t my_handle;
|
nvs_handle_t my_handle;
|
||||||
if (nvs_open("storage", NVS_READWRITE, &my_handle) == ESP_OK)
|
if (nvs_open("storage", NVS_READWRITE, &my_handle) == ESP_OK)
|
||||||
{
|
{
|
||||||
// If we are running from the factory partition, force the www partition to
|
// Read active www partition from NVS
|
||||||
// 0 This ensures that after a USB flash (which only writes to www_0), we
|
err = nvs_get_u8(my_handle, "www_part", &g_Active_WWW_Partition);
|
||||||
// aren't stuck looking at an old www_1.
|
|
||||||
const esp_partition_t *running = esp_ota_get_running_partition();
|
if (err == ESP_ERR_NVS_NOT_FOUND)
|
||||||
if (running && running->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY)
|
|
||||||
{
|
{
|
||||||
printf(
|
// First boot (no NVS key yet): default to www_0
|
||||||
"Running from factory: resetting www_part to 0 for consistency.\n");
|
// This ensures that after a fresh USB flash (which only writes www_0),
|
||||||
|
// we start from the correct partition.
|
||||||
|
printf("No www_part in NVS, defaulting to 0.\n");
|
||||||
g_Active_WWW_Partition = 0;
|
g_Active_WWW_Partition = 0;
|
||||||
nvs_set_u8(my_handle, "www_part", 0);
|
nvs_set_u8(my_handle, "www_part", 0);
|
||||||
nvs_commit(my_handle);
|
nvs_commit(my_handle);
|
||||||
}
|
}
|
||||||
else
|
else if (err != ESP_OK)
|
||||||
{
|
|
||||||
err = nvs_get_u8(my_handle, "www_part", &g_Active_WWW_Partition);
|
|
||||||
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND)
|
|
||||||
{
|
{
|
||||||
printf("Error reading www_part from NVS: %s\n", esp_err_to_name(err));
|
printf("Error reading www_part from NVS: %s\n", esp_err_to_name(err));
|
||||||
}
|
g_Active_WWW_Partition = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (g_Active_WWW_Partition > 1)
|
if (g_Active_WWW_Partition > 1)
|
||||||
|
|||||||
@@ -156,7 +156,38 @@ frontend/src/
|
|||||||
└── app.css # +sidebar theme tokens
|
└── app.css # +sidebar theme tokens
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Summary
|
## 5. Changelog
|
||||||
|
|
||||||
|
### 2026-03-07 — API Refactoring & UI Improvements
|
||||||
|
|
||||||
|
**Backend Refactoring:**
|
||||||
|
- Split monolithic `manage.cpp` and `utils.cpp` into single-responsibility files per endpoint (e.g., `list.cpp`, `add.cpp`, `remove.cpp`)
|
||||||
|
- Renamed `seed.cpp` → `store.cpp` in both `api/users/` and `api/tasks/` (better name for the data-store layer)
|
||||||
|
- Introduced `store.hpp` forward-declaration headers to resolve cross-domain dependencies (tasks needs `find_user`, users/remove needs `remove_tasks_for_user`)
|
||||||
|
- Added `unity.cpp` per domain to simplify `http_server.cpp` includes
|
||||||
|
- `.hpp` files contain structs and constants only; all functions live in `.cpp` files
|
||||||
|
|
||||||
|
**Frontend OTA Bug Fix:**
|
||||||
|
- Fixed `main.cpp` factory-partition check that unconditionally reset `www_part` to 0 on every boot, preventing OTA from ever switching to `www_1`
|
||||||
|
- Now only defaults to `www_0` on first boot (NVS key not found); OTA-written values are respected
|
||||||
|
|
||||||
|
**Date/Time Picker UX:**
|
||||||
|
- Fixed timezone drift bug: `formatDateForInput` used `toISOString()` (UTC) but `datetime-local` inputs use local time, causing ±N hours shift on each edit
|
||||||
|
- Split `datetime-local` into separate `date` + `time` inputs with labels
|
||||||
|
- Default date/time set to "now" when opening the add task form
|
||||||
|
- CSS trick to make Chrome/Edge open the picker on click anywhere in the input (`::-webkit-calendar-picker-indicator` stretched to fill)
|
||||||
|
|
||||||
|
**Dashboard Layout:**
|
||||||
|
- Moved "Upcoming Tasks" section above the system info grid (content-first, debug tools lower)
|
||||||
|
|
||||||
|
**Sidebar:**
|
||||||
|
- Auto-collapses on mobile (viewport ≤ 768px) via `window.innerWidth` check at init
|
||||||
|
|
||||||
|
**Build Warnings Fixed:**
|
||||||
|
- `<button>` inside `<button>` nesting in UserManager → changed user chips to `<div role="button">`
|
||||||
|
- `autofocus` a11y warnings suppressed with `<!-- svelte-ignore -->` comments
|
||||||
|
|
||||||
|
## 6. Summary
|
||||||
|
|
||||||
We implement a **temporary in-memory todo list** using **static BSS arrays** on the ESP32 backend, exposed via a **RESTful API** with 8 new endpoints. The frontend gains a **collapsible sidebar** for navigation between the existing Dashboard and a new **Task Manager** view. **Seed data** is populated on every boot for fast development iteration. The architecture is designed to make the eventual migration to **SQLite on SD card** straightforward — the API contracts and frontend components remain unchanged; only the storage layer swaps out.
|
We implement a **temporary in-memory todo list** using **static BSS arrays** on the ESP32 backend, exposed via a **RESTful API** with 8 new endpoints. The frontend gains a **collapsible sidebar** for navigation between the existing Dashboard and a new **Task Manager** view. **Seed data** is populated on every boot for fast development iteration. The architecture is designed to make the eventual migration to **SQLite on SD card** straightforward — the API contracts and frontend components remain unchanged; only the storage layer swaps out.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user