87 lines
3.1 KiB
Svelte
87 lines
3.1 KiB
Svelte
<script lang="ts">
|
|
import { tick } from "svelte"
|
|
import { gsap } from "gsap"
|
|
import { Flip } from "gsap/Flip"
|
|
import { createRouter, type Todo } from "@<@var(context.project.name)>/rpc"
|
|
|
|
gsap.registerPlugin(Flip)
|
|
const router = createRouter("http://127.0.0.1:8080")
|
|
// Changes made elsewhere (another tab, the mobile app) show up within this interval.
|
|
const syncIntervalMs = 2000
|
|
let todos = $state<Todo[]>([])
|
|
let offline = $state(false)
|
|
let todoToCreateTask = $state<string>("")
|
|
|
|
// Like the mobile app: rows spring into their new place when the list re-sorts, new rows fade in.
|
|
const showTodos = async (next: Todo[]) => {
|
|
const rows = Flip.getState(".todo-row")
|
|
todos = next
|
|
await tick()
|
|
Flip.from(rows, {
|
|
targets: ".todo-row",
|
|
duration: 0.5,
|
|
ease: "back.out(1.4)",
|
|
onEnter: (entering) => gsap.fromTo(entering, { opacity: 0 }, { opacity: 1, duration: 0.3 }),
|
|
})
|
|
}
|
|
|
|
const fetchTodos = () => {
|
|
router.todos.listTodos({}).then((r) => {
|
|
offline = false
|
|
// Polling returns the same list most of the time; skip re-rendering it.
|
|
if (JSON.stringify($state.snapshot(todos)) !== JSON.stringify(r.todos)) showTodos(r.todos)
|
|
}).catch(() => offline = true)
|
|
}
|
|
$effect(() => {
|
|
fetchTodos()
|
|
const interval = setInterval(fetchTodos, syncIntervalMs)
|
|
return () => clearInterval(interval)
|
|
})
|
|
|
|
const setTodoDone = (id: string, task: string) => {
|
|
return (event: Event & { currentTarget: HTMLInputElement }) => {
|
|
router.todos.updateTodo({ todo: { id, task, done: event.currentTarget.checked } }).then(fetchTodos, () => offline = true)
|
|
}
|
|
}
|
|
|
|
const deleteTodo = (id: string) => {
|
|
return async (event: Event & { currentTarget: HTMLElement }) => {
|
|
await gsap.to(event.currentTarget.closest(".todo-row"), { opacity: 0, duration: 0.3 })
|
|
router.todos.deleteTodo({ todo: { id } }).then(fetchTodos, () => offline = true)
|
|
}
|
|
}
|
|
|
|
const createTodo = () => {
|
|
const task = todoToCreateTask.trim()
|
|
if (!task) return
|
|
router.todos.createTodo({ todo: { id: crypto.randomUUID(), task } }).then(() => {
|
|
todoToCreateTask = ""
|
|
fetchTodos()
|
|
}, () => offline = true)
|
|
}
|
|
</script>
|
|
|
|
<main>
|
|
<h1>Create Todo</h1>
|
|
{#if offline}<p class="offline">Can't reach the API, retrying…</p>{/if}
|
|
<form onsubmit={(event) => { event.preventDefault(); createTodo() }}>
|
|
<input bind:value={todoToCreateTask} type="text" placeholder="What needs to be done?"/>
|
|
<button>create Todo</button>
|
|
</form>
|
|
{#each todos as todo (todo.id)}
|
|
<div class="todo-row" data-flip-id={todo.id}>
|
|
<span>{todo.task}</span>
|
|
<input type="checkbox" checked={todo.done} onchange={setTodoDone(todo.id || "", todo.task)}/>
|
|
<button onclick={deleteTodo(todo.id || "")}>delete</button>
|
|
</div>
|
|
{/each}
|
|
</main>
|
|
|
|
<style>
|
|
main { max-width: 32rem; margin: 2rem auto; font-family: system-ui, sans-serif; }
|
|
form, .todo-row { display: flex; gap: 0.75rem; align-items: center; margin-bottom: 0.75rem; }
|
|
input[type="text"] { flex: 1; padding: 0.5rem; border: 1px solid #8e8e93; border-radius: 8px; }
|
|
.todo-row span { flex: 1; }
|
|
.offline { color: #d32f2f; }
|
|
</style>
|