feat(frontend): implement task CRUD functionality
- Create TaskItem component with checkbox, meta info, tags - Create TaskList component with loading/empty states - Update home page with task list and filter toggle (pending/completed) - Add new task form with description, project, priority, due date, tags - Add task detail placeholder page - Implement task toggle (complete/uncomplete) - Add filter bar to switch between pending and completed tasks - Support for optimistic UI updates and offline queueing - Visual indicators for priority, due dates, overdue tasks - Mobile-optimized list items with proper touch targets
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { tasksStore } from '$lib/stores/tasks.js';
|
||||
import { generateUUID } from '$lib/utils/uuid.js';
|
||||
import { toUnix } from '$lib/utils/dates.js';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Input from '$lib/components/ui/Input.svelte';
|
||||
import Select from '$lib/components/ui/Select.svelte';
|
||||
|
||||
let description = '';
|
||||
let project = '';
|
||||
let priority = '1';
|
||||
let dueDate = '';
|
||||
let tags = '';
|
||||
let saving = false;
|
||||
let error = '';
|
||||
|
||||
const priorityOptions = [
|
||||
{ value: '0', label: 'Low' },
|
||||
{ value: '1', label: 'Default' },
|
||||
{ value: '2', label: 'Medium' },
|
||||
{ value: '3', label: 'High' }
|
||||
];
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!description.trim()) {
|
||||
error = 'Description is required';
|
||||
return;
|
||||
}
|
||||
|
||||
saving = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
/** @type {Partial<import('$lib/api/types.js').Task>} */
|
||||
const task = {
|
||||
uuid: generateUUID(),
|
||||
description: description.trim(),
|
||||
status: /** @type {'P'} */ ('P'),
|
||||
priority: /** @type {0|1|2|3} */ (parseInt(priority)),
|
||||
created: toUnix(new Date()),
|
||||
modified: toUnix(new Date()),
|
||||
tags: tags.trim() ? tags.split(',').map(t => t.trim()).filter(t => t) : [],
|
||||
project: project.trim() || null,
|
||||
due: dueDate ? toUnix(new Date(dueDate)) : null
|
||||
};
|
||||
|
||||
await tasksStore.add(task);
|
||||
goto('/');
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to create task';
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<a href="/" class="back-btn">
|
||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back
|
||||
</a>
|
||||
<h1>New Task</h1>
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} class="task-form">
|
||||
{#if error}
|
||||
<div class="error-banner">{error}</div>
|
||||
{/if}
|
||||
|
||||
<Input
|
||||
label="Description"
|
||||
placeholder="What needs to be done?"
|
||||
bind:value={description}
|
||||
required
|
||||
id="description"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Project"
|
||||
placeholder="Optional"
|
||||
bind:value={project}
|
||||
id="project"
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Priority"
|
||||
options={priorityOptions}
|
||||
bind:value={priority}
|
||||
id="priority"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Due Date"
|
||||
type="date"
|
||||
bind:value={dueDate}
|
||||
id="due"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Tags"
|
||||
placeholder="Comma separated (e.g., work, urgent)"
|
||||
bind:value={tags}
|
||||
id="tags"
|
||||
/>
|
||||
|
||||
<div class="actions">
|
||||
<Button type="button" variant="secondary" on:click={() => goto('/')}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" loading={saving}>
|
||||
Create Task
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.task-form {
|
||||
background-color: var(--bg-primary);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
background-color: #fee2e2;
|
||||
color: var(--color-danger);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.actions > :global(button) {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user