feat: Implement core frontend application with task management, user selection, and a collapsible sidebar.

This commit is contained in:
2026-03-07 22:18:36 -05:00
parent 2bee7dce43
commit e661e15bbf
7 changed files with 189 additions and 70 deletions

View File

@@ -144,7 +144,7 @@
<div class="w-full max-w-6xl mx-auto space-y-8">
<!-- Header -->
<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>
<!-- Status Badge -->
@@ -175,6 +175,39 @@
{#if currentView === 'dashboard'}
<!-- 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 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
@@ -285,38 +318,6 @@
</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'}
<!-- Task Manager View -->
<div class="bg-bg-card border border-border rounded-xl p-6 shadow-xl">

View File

@@ -1,6 +1,6 @@
<script>
let { currentView = 'dashboard', onNavigate = () => {} } = $props();
let collapsed = $state(false);
let collapsed = $state(typeof window !== 'undefined' && window.innerWidth <= 768);
const navItems = [
{ id: 'dashboard', label: 'Dashboard', icon: '🏠' },

View File

@@ -8,13 +8,15 @@
// Add task form state
let newTitle = $state('');
let newDueDate = $state('');
let newDueDay = $state('');
let newDueTime = $state('');
let showAddForm = $state(false);
// Edit state
let editingTaskId = $state(null);
let editTitle = $state('');
let editDueDate = $state('');
let editDueDay = $state('');
let editDueTime = $state('');
// Confirm delete
let confirmDeleteId = $state(null);
@@ -45,13 +47,14 @@
async function handleAddTask(e) {
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 {
await addTask(selectedUserId, newTitle.trim(), dueTimestamp);
newTitle = '';
newDueDate = '';
newDueDay = '';
newDueTime = '';
showAddForm = false;
await fetchTasks();
} catch (e) {
@@ -71,14 +74,16 @@
function startEditing(task) {
editingTaskId = task.id;
editTitle = task.title;
editDueDate = formatDateForInput(task.due_date);
const parts = formatDateForInput(task.due_date);
editDueDay = parts.day;
editDueTime = parts.time;
}
async function saveEdit(e) {
e.preventDefault();
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 {
await updateTask(editingTaskId, {
title: editTitle.trim(),
@@ -103,7 +108,18 @@
function formatDateForInput(timestamp) {
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) {
@@ -135,12 +151,13 @@
<div class="task-header">
<h2 class="task-title">Tasks</h2>
{#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}
</div>
{#if showAddForm}
<form class="add-task-form" onsubmit={handleAddTask}>
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
bind:value={newTitle}
@@ -148,14 +165,19 @@
class="task-input"
autofocus
/>
<input
type="datetime-local"
bind:value={newDueDate}
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={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">
<button type="submit" class="btn-primary" disabled={!newTitle.trim() || !newDueDate}>Add</button>
<button type="button" class="btn-secondary" onclick={() => { showAddForm = false; newTitle = ''; newDueDate = ''; }}>Cancel</button>
<button type="submit" class="btn-primary" disabled={!newTitle.trim() || !newDueDay || !newDueTime}>Add</button>
<button type="button" class="btn-secondary" onclick={() => { showAddForm = false; newTitle = ''; newDueDay = ''; newDueTime = ''; }}>Cancel</button>
</div>
</form>
{/if}
@@ -166,7 +188,16 @@
{#if editingTaskId === task.id}
<form class="edit-form" onsubmit={saveEdit}>
<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">
<button type="submit" class="btn-primary btn-sm">Save</button>
<button type="button" class="btn-secondary btn-sm" onclick={() => editingTaskId = null}>Cancel</button>
@@ -274,6 +305,29 @@
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 {
padding: 8px 12px;
border-radius: 8px;
@@ -283,9 +337,40 @@
font-size: 13px;
outline: none;
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);
}

View File

@@ -59,9 +59,13 @@
<div class="user-bar">
<div class="user-chips">
{#each users as user}
<button
<!-- svelte-ignore node_invalid_placement_ssr -->
<div
class="user-chip {selectedUserId === user.id ? 'selected' : ''}"
onclick={() => selectedUserId = user.id}
role="button"
tabindex="0"
onkeydown={(e) => { if (e.key === 'Enter') selectedUserId = user.id; }}
>
<span class="user-name">{user.name}</span>
{#if confirmDeleteId === user.id}
@@ -76,11 +80,12 @@
title="Remove user"
></button>
{/if}
</button>
</div>
{/each}
{#if showAddForm}
<form class="add-user-form" onsubmit={(e) => { e.preventDefault(); handleAddUser(); }}>
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
bind:value={newUserName}