@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
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
|
||||
const todos = ref<Todo[]>([])
|
||||
const offline = ref(false)
|
||||
const todoToCreateTask = ref("")
|
||||
|
||||
// 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.value = next
|
||||
await nextTick()
|
||||
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.value = false
|
||||
// Polling returns the same list most of the time; skip re-rendering it.
|
||||
if (JSON.stringify(todos.value) !== JSON.stringify(r.todos)) showTodos(r.todos)
|
||||
}).catch(() => offline.value = true)
|
||||
}
|
||||
let interval: ReturnType<typeof setInterval> | undefined
|
||||
onMounted(() => {
|
||||
fetchTodos()
|
||||
interval = setInterval(fetchTodos, syncIntervalMs)
|
||||
})
|
||||
onUnmounted(() => clearInterval(interval))
|
||||
|
||||
const setTodoDone = (todo: Todo, event: Event) => {
|
||||
const done = (event.target as HTMLInputElement).checked
|
||||
router.todos.updateTodo({ todo: { id: todo.id, task: todo.task, done } }).then(fetchTodos, () => offline.value = true)
|
||||
}
|
||||
|
||||
const deleteTodo = async (todo: Todo, event: Event) => {
|
||||
await gsap.to((event.currentTarget as HTMLElement).closest(".todo-row"), { opacity: 0, duration: 0.3 })
|
||||
router.todos.deleteTodo({ todo: { id: todo.id } }).then(fetchTodos, () => offline.value = true)
|
||||
}
|
||||
|
||||
const createTodo = () => {
|
||||
const task = todoToCreateTask.value.trim()
|
||||
if (!task) return
|
||||
router.todos.createTodo({ todo: { id: crypto.randomUUID(), task } }).then(() => {
|
||||
todoToCreateTask.value = ""
|
||||
fetchTodos()
|
||||
}, () => offline.value = true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>Create Todo</h1>
|
||||
<p v-if="offline" class="offline">Can't reach the API, retrying…</p>
|
||||
<form @submit.prevent="createTodo">
|
||||
<input v-model="todoToCreateTask" type="text" placeholder="What needs to be done?">
|
||||
<button>create Todo</button>
|
||||
</form>
|
||||
<div v-for="todo in todos" :key="todo.id" class="todo-row" :data-flip-id="todo.id">
|
||||
<span>{{ todo.task }}</span>
|
||||
<input type="checkbox" :checked="todo.done" @change="setTodoDone(todo, $event)">
|
||||
<button @click="deleteTodo(todo, $event)">delete</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user