307 lines
7.6 KiB
Svelte

<script lang="ts">
import {
addTodoItem,
addTodoItemBackend,
getOrCreateTodoList,
removeTodoItem,
removeTodoItemBackend,
serializeTodoList,
setTodoList,
toggleTodoItem,
toggleTodoItemBackend,
todosStore,
type TodoItem,
updateTodoItemText,
updateTodoItemTextBackend
} from "$lib/stores/todos";
import { get } from "svelte/store";
export let openDocumentId = "";
export let openDocumentName = "";
export let openDocumentContent = "";
export let onDocumentContentChange: (content: string) => void = () => {};
let todoItems: TodoItem[] = [];
let lastTodoDocumentId = "";
let newTodoText = "";
let editingTodoId: number | null = null;
let editingTodoText = "";
async function addTodo() {
const text = newTodoText.trim();
if (!text) return;
newTodoText = "";
const backendItem = await addTodoItemBackend(openDocumentId, text);
if (backendItem) {
todoItems = [backendItem, ...todoItems];
} else {
todoItems = addTodoItem(todoItems, text);
}
persistTodosForCurrentList();
}
async function toggleTodoDone(id: number) {
const ok = await toggleTodoItemBackend(openDocumentId, id);
if (!ok) {
todoItems = toggleTodoItem(todoItems, id);
} else {
todoItems = todoItems.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
}
persistTodosForCurrentList();
}
function startEditTodo(id: number) {
const todo = todoItems.find((item) => item.id === id);
if (!todo) return;
editingTodoId = id;
editingTodoText = todo.text;
}
async function saveEditTodo() {
if (editingTodoId === null) return;
const text = editingTodoText.trim();
if (!text) return;
const id = editingTodoId;
editingTodoId = null;
editingTodoText = "";
const ok = await updateTodoItemTextBackend(openDocumentId, id, text);
if (!ok) {
todoItems = updateTodoItemText(todoItems, id, text);
} else {
todoItems = todoItems.map((t) => (t.id === id ? { ...t, text: text.trim() } : t));
}
persistTodosForCurrentList();
}
function cancelEditTodo() {
editingTodoId = null;
editingTodoText = "";
}
async function removeTodo(id: number) {
if (editingTodoId === id) {
cancelEditTodo();
}
const ok = await removeTodoItemBackend(openDocumentId, id);
if (!ok) {
todoItems = removeTodoItem(todoItems, id);
} else {
todoItems = todoItems.filter((t) => t.id !== id);
}
persistTodosForCurrentList();
}
function loadTodosForDocument(documentId: string) {
if (!documentId) {
todoItems = [];
return;
}
const lists = get(todosStore);
const result = getOrCreateTodoList(lists, documentId, openDocumentContent);
if (result.lists !== lists) {
todosStore.set(result.lists);
}
todoItems = result.todos;
}
function persistTodosForCurrentList() {
if (!openDocumentId) return;
const lists = get(todosStore);
todosStore.set(setTodoList(lists, openDocumentId, todoItems));
const markdown = serializeTodoList(openDocumentName, todoItems);
onDocumentContentChange(markdown);
}
$: if (openDocumentId !== lastTodoDocumentId) {
loadTodosForDocument(openDocumentId);
editingTodoId = null;
editingTodoText = "";
newTodoText = "";
lastTodoDocumentId = openDocumentId;
}
</script>
<section class="todo-surface">
<div class="todo-card">
<form class="todo-create" on:submit|preventDefault={addTodo}>
<input
type="text"
placeholder="Add a new to-do"
bind:value={newTodoText}
aria-label="Add to-do"
/>
<button type="submit" class="todo-add-btn">Add</button>
</form>
<ul class="todo-list">
{#each todoItems as todo}
<li class="todo-item">
<label class="todo-check">
<input type="checkbox" checked={todo.done} on:change={() => toggleTodoDone(todo.id)} />
</label>
{#if editingTodoId === todo.id}
<input
type="text"
class="todo-edit-input"
bind:value={editingTodoText}
on:keydown={(event) => {
if (event.key === "Enter") saveEditTodo();
if (event.key === "Escape") cancelEditTodo();
}}
/>
<div class="todo-actions">
<button type="button" class="todo-btn save" on:click={saveEditTodo}>Save</button>
<button type="button" class="todo-btn ghost" on:click={cancelEditTodo}>Cancel</button>
</div>
{:else}
<span class="todo-text" class:is-done={todo.done}>{todo.text}</span>
<div class="todo-actions">
<button type="button" class="todo-btn ghost" on:click={() => startEditTodo(todo.id)}>Edit</button>
<button type="button" class="todo-btn danger" on:click={() => removeTodo(todo.id)}>Remove</button>
</div>
{/if}
</li>
{/each}
</ul>
</div>
</section>
<style>
.todo-surface {
min-height: 0;
flex: 1;
overflow: auto;
padding: 0 14px 14px;
}
.todo-card {
width: min(100%, 920px);
margin: 0 auto;
border: none;
border-radius: 0;
background: transparent;
padding: 28px 36px;
display: flex;
flex-direction: column;
gap: 12px;
max-height: 100%;
overflow: visible;
}
.todo-create {
display: flex;
gap: 8px;
}
.todo-create input,
.todo-edit-input {
width: 100%;
border: 1px solid var(--border-soft);
border-radius: 8px;
background: color-mix(in srgb, var(--surface-1) 88%, var(--bg-editor) 12%);
color: var(--text-primary);
padding: 10px 11px;
font-size: 0.88rem;
}
.todo-add-btn {
border-radius: 8px;
border: 1px solid var(--border-strong);
background: color-mix(in srgb, var(--surface-2) 84%, var(--bg-hover) 16%);
color: var(--text-primary);
padding: 9px 14px;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
}
.todo-add-btn:hover {
background: var(--bg-hover);
}
.todo-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
}
.todo-item {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
border: 1px solid var(--border-soft);
border-radius: 8px;
padding: 10px 12px;
background: color-mix(in srgb, var(--surface-1) 90%, var(--bg-editor) 10%);
}
.todo-check {
display: grid;
place-items: center;
}
.todo-text {
font-size: 0.9rem;
color: var(--text-primary);
line-height: 1.45;
}
.todo-text.is-done {
color: var(--text-dim);
text-decoration: line-through;
}
.todo-actions {
display: flex;
gap: 6px;
}
.todo-btn {
border-radius: 7px;
border: 1px solid var(--border-soft);
background: color-mix(in srgb, var(--surface-1) 90%, var(--bg-editor) 10%);
color: var(--text-muted);
padding: 6px 10px;
font-size: 0.78rem;
cursor: pointer;
}
.todo-btn.save {
border-color: var(--border-strong);
background: color-mix(in srgb, var(--surface-2) 84%, var(--bg-hover) 16%);
color: var(--text-primary);
}
.todo-btn.danger:hover,
.todo-btn.ghost:hover,
.todo-btn.save:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
@media (max-width: 980px) {
.todo-surface {
padding: 4px 8px 10px;
}
.todo-card {
width: 100%;
padding: 18px 16px;
}
.todo-item {
grid-template-columns: auto minmax(0, 1fr);
row-gap: 8px;
}
.todo-actions {
grid-column: 1 / -1;
justify-content: flex-end;
}
}
</style>