Add browser UI for todo testing

This commit is contained in:
2026-08-08 21:14:10 -05:00
parent e1600da8b1
commit dd4abe85ae
4 changed files with 128 additions and 6 deletions
+119
View File
@@ -0,0 +1,119 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Todo Back</title>
<style>
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
body { max-width: 680px; margin: 3rem auto; padding: 0 1rem; }
form, li { display: flex; gap: .6rem; align-items: center; }
input[type="text"] { flex: 1; padding: .7rem; }
button { padding: .6rem .8rem; cursor: pointer; }
ul { list-style: none; padding: 0; }
li { padding: .7rem 0; border-bottom: 1px solid #8886; }
li span { flex: 1; }
li.completed span { text-decoration: line-through; opacity: .6; }
#status { min-height: 1.5rem; color: #d55; }
</style>
</head>
<body>
<h1>Todo Back</h1>
<form id="new-todo">
<input id="title" type="text" maxlength="255" placeholder="What needs doing?" required autofocus>
<button type="submit">Add</button>
</form>
<p id="status" role="status"></p>
<ul id="todos"></ul>
<script>
const list = document.querySelector('#todos')
const form = document.querySelector('#new-todo')
const title = document.querySelector('#title')
const status = document.querySelector('#status')
async function api(path = '', options = {}) {
const response = await fetch(`/todos${path}`, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options,
})
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`)
return response.status === 204 ? null : response.json()
}
function render(todos) {
list.replaceChildren(...todos.map(todo => {
const item = document.createElement('li')
item.className = todo.completed ? 'completed' : ''
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = todo.completed
checkbox.title = 'Mark complete'
checkbox.addEventListener('change', async () => {
await update(todo.id, { completed: checkbox.checked })
})
const text = document.createElement('span')
text.textContent = todo.title
const edit = document.createElement('button')
edit.textContent = 'Edit'
edit.addEventListener('click', async () => {
const nextTitle = prompt('Todo title', todo.title)
if (nextTitle?.trim()) await update(todo.id, { title: nextTitle.trim() })
})
const remove = document.createElement('button')
remove.textContent = 'Delete'
remove.addEventListener('click', async () => {
if (!confirm(`Delete “${todo.title}”?`)) return
await run(() => api(`/${todo.id}`, { method: 'DELETE' }))
})
item.append(checkbox, text, edit, remove)
return item
}))
}
async function load() {
render(await api())
}
async function run(action) {
status.textContent = ''
try {
await action()
await load()
} catch (error) {
status.textContent = `Request failed: ${error.message}`
}
}
async function update(id, attributes) {
await run(() => api(`/${id}`, {
method: 'PATCH',
body: JSON.stringify({ todo: attributes }),
}))
}
form.addEventListener('submit', async event => {
event.preventDefault()
const value = title.value.trim()
if (!value) return
await run(() => api('', {
method: 'POST',
body: JSON.stringify({ todo: { title: value, completed: false } }),
}))
title.value = ''
title.focus()
})
load().catch(error => {
status.textContent = `Request failed: ${error.message}`
})
</script>
</body>
</html>