154 lines
4.1 KiB
Svelte
154 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
|
import { useConvexClient, useQuery } from 'convex-svelte';
|
|
|
|
let newTodoText = $state('');
|
|
let isAdding = $state(false);
|
|
let addError = $state<Error | null>(null);
|
|
let togglingId = $state<Id<'todos'> | null>(null);
|
|
let toggleError = $state<Error | null>(null);
|
|
let deletingId = $state<Id<'todos'> | null>(null);
|
|
let deleteError = $state<Error | null>(null);
|
|
|
|
const client = useConvexClient();
|
|
|
|
const todosQuery = useQuery(api.todos.getAll, {});
|
|
|
|
async function handleAddTodo(event: SubmitEvent) {
|
|
event.preventDefault();
|
|
const text = newTodoText.trim();
|
|
if (!text || isAdding) return;
|
|
|
|
isAdding = true;
|
|
addError = null;
|
|
try {
|
|
await client.mutation(api.todos.create, { text });
|
|
newTodoText = '';
|
|
} catch (err) {
|
|
console.error('Failed to add todo:', err);
|
|
addError = err instanceof Error ? err : new Error(String(err));
|
|
} finally {
|
|
isAdding = false;
|
|
}
|
|
}
|
|
|
|
async function handleToggleTodo(id: Id<'todos'>, completed: boolean) {
|
|
if (togglingId === id || deletingId === id) return;
|
|
|
|
togglingId = id;
|
|
toggleError = null;
|
|
try {
|
|
await client.mutation(api.todos.toggle, { id, completed: !completed });
|
|
} catch (err) {
|
|
console.error('Failed to toggle todo:', err);
|
|
toggleError = err instanceof Error ? err : new Error(String(err));
|
|
} finally {
|
|
if (togglingId === id) {
|
|
togglingId = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleDeleteTodo(id: Id<'todos'>) {
|
|
if (togglingId === id || deletingId === id) return;
|
|
|
|
deletingId = id;
|
|
deleteError = null;
|
|
try {
|
|
await client.mutation(api.todos.deleteTodo, { id });
|
|
} catch (err) {
|
|
console.error('Failed to delete todo:', err);
|
|
deleteError = err instanceof Error ? err : new Error(String(err));
|
|
} finally {
|
|
if (deletingId === id) {
|
|
deletingId = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const canAdd = $derived(!isAdding && newTodoText.trim().length > 0);
|
|
const isLoadingTodos = $derived(todosQuery.isLoading);
|
|
const todos = $derived(todosQuery.data ?? []);
|
|
const hasTodos = $derived(todos.length > 0);
|
|
</script>
|
|
|
|
<div class="p-4">
|
|
<h1 class="mb-4 text-xl">Todos (Convex)</h1>
|
|
|
|
<form onsubmit={handleAddTodo} class="mb-4 flex gap-2">
|
|
<input
|
|
type="text"
|
|
bind:value={newTodoText}
|
|
placeholder="New task..."
|
|
disabled={isAdding}
|
|
class="flex-grow p-1"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={!canAdd}
|
|
class="rounded bg-blue-500 px-3 py-1 text-white disabled:opacity-50"
|
|
>
|
|
{#if isAdding}Adding...{:else}Add{/if}
|
|
</button>
|
|
</form>
|
|
|
|
{#if isLoadingTodos}
|
|
<p>Loading...</p>
|
|
{:else if !hasTodos}
|
|
<p>No todos yet.</p>
|
|
{:else}
|
|
<ul class="space-y-1">
|
|
{#each todos as todo (todo._id)}
|
|
{@const isTogglingThis = togglingId === todo._id}
|
|
{@const isDeletingThis = deletingId === todo._id}
|
|
{@const isDisabled = isTogglingThis || isDeletingThis}
|
|
<li class="flex items-center justify-between p-2" class:opacity-50={isDisabled}>
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id={`todo-${todo._id}`}
|
|
checked={todo.completed}
|
|
onchange={() => handleToggleTodo(todo._id, todo.completed)}
|
|
disabled={isDisabled}
|
|
/>
|
|
<label for={`todo-${todo._id}`} class:line-through={todo.completed}>
|
|
{todo.text}
|
|
</label>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onclick={() => handleDeleteTodo(todo._id)}
|
|
disabled={isDisabled}
|
|
aria-label="Delete todo"
|
|
class="px-1 text-red-500 disabled:opacity-50"
|
|
>
|
|
{#if isDeletingThis}Deleting...{:else}X{/if}
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|
|
|
|
{#if todosQuery.error}
|
|
<p class="mt-4 text-red-500">
|
|
Error loading: {todosQuery.error?.message ?? 'Unknown error'}
|
|
</p>
|
|
{/if}
|
|
{#if addError}
|
|
<p class="mt-4 text-red-500">
|
|
Error adding: {addError.message ?? 'Unknown error'}
|
|
</p>
|
|
{/if}
|
|
{#if toggleError}
|
|
<p class="mt-4 text-red-500">
|
|
Error updating: {toggleError.message ?? 'Unknown error'}
|
|
</p>
|
|
{/if}
|
|
{#if deleteError}
|
|
<p class="mt-4 text-red-500">
|
|
Error deleting: {deleteError.message ?? 'Unknown error'}
|
|
</p>
|
|
{/if}
|
|
</div>
|