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:
@@ -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;
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user