feat(frontend): add API client and Svelte stores

- Create API client with auto-retry and token refresh support
- Add comprehensive API endpoints for tasks, tags, projects, sync, and auth
- Implement authStore for authentication state management
- Implement tasksStore with optimistic updates and offline queue
- Add derived stores for filtered task views (pending, completed, by project)
- Implement syncStore for managing sync state and queue
- Add client ID generation and persistence for sync tracking
This commit is contained in:
2026-01-06 15:45:13 +01:00
parent 41795d1827
commit d99e158a8c
5 changed files with 754 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
import { writable, derived } from 'svelte/store';
import { tasks as tasksAPI } from '$lib/api/endpoints.js';
import { queueChange } from '$lib/utils/sync-queue.js';
/**
* @typedef {import('$lib/api/types.js').Task} Task
* @typedef {import('$lib/api/types.js').TaskFilters} TaskFilters
*/
/**
* Create tasks store
*/
function createTasksStore() {
const { subscribe, set, update } = writable(/** @type {Task[]} */ ([]));
return {
subscribe,
/**
* Load all tasks from API
* @param {TaskFilters} [filters]
*/
async load(filters = {}) {
try {
const tasks = await tasksAPI.list(filters);
set(tasks);
} catch (error) {
console.error('Failed to load tasks:', error);
throw error;
}
},
/**
* Add new task (optimistic update)
* @param {Partial<Task>} task
*/
async add(task) {
try {
const created = await tasksAPI.create(task);
update(tasks => [...tasks, created]);
return created;
} catch (error) {
// Queue for offline sync
queueChange({
type: 'create',
task_uuid: task.uuid,
data: task
});
// Still update UI optimistically
update(tasks => [...tasks, task]);
throw error;
}
},
/**
* Update task (optimistic update)
* @param {string} uuid
* @param {Partial<Task>} updates
*/
async updateTask(uuid, updates) {
// Optimistic update
update(tasks => {
const index = tasks.findIndex(t => t.uuid === uuid);
if (index >= 0) {
tasks[index] = { ...tasks[index], ...updates, modified: Date.now() / 1000 };
}
return tasks;
});
try {
const updated = await tasksAPI.update(uuid, updates);
// Sync with server response
update(tasks => {
const index = tasks.findIndex(t => t.uuid === uuid);
if (index >= 0) {
tasks[index] = updated;
}
return tasks;
});
} catch (error) {
// Queue for offline sync
queueChange({
type: 'update',
task_uuid: uuid,
data: updates
});
throw error;
}
},
/**
* Delete task
* @param {string} uuid
*/
async deleteTask(uuid) {
// Optimistic removal
update(tasks => tasks.filter(t => t.uuid !== uuid));
try {
await tasksAPI.delete(uuid);
} catch (error) {
queueChange({
type: 'delete',
task_uuid: uuid,
data: {}
});
throw error;
}
},
/**
* Complete task
* @param {string} uuid
*/
async complete(uuid) {
try {
const completed = await tasksAPI.complete(uuid);
update(tasks => {
const index = tasks.findIndex(t => t.uuid === uuid);
if (index >= 0) {
tasks[index] = completed;
}
return tasks;
});
} catch (error) {
queueChange({
type: 'update',
task_uuid: uuid,
data: { status: 'C', end: Date.now() / 1000 }
});
throw error;
}
}
};
}
export const tasksStore = createTasksStore();
// Derived stores for filtered views
export const pendingTasks = derived(
tasksStore,
$tasks => $tasks.filter(t => t.status === 'P')
);
export const completedTasks = derived(
tasksStore,
$tasks => $tasks.filter(t => t.status === 'C')
);
export const tasksByProject = derived(
tasksStore,
$tasks => {
const grouped = {};
$tasks.forEach(task => {
const project = task.project || 'No Project';
if (!grouped[project]) grouped[project] = [];
grouped[project].push(task);
});
return grouped;
}
);