120 lines
3.9 KiB
HTML
120 lines
3.9 KiB
HTML
<!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>
|