{{#partial post add}}
{{#param text maxlength=100}}
{{#let current_total = COUNT(*) from todos}}
{{#if :current_total < 20}}
{{#insert into todos(text) values (:text)}}
{{/if}}
{{/partial}}
{{#partial post :id/toggle}}
{{#update todos set completed = 1 - completed WHERE id = :id}}
{{/partial}}
{{#partial patch :id}}
{{#param text maxlength=100}}
{{#update todos set text = :text WHERE id = :id}}
{{/partial}}
{{#partial post toggle_all}}
{{#let active_count = COUNT(*) from todos WHERE completed = 0}}
{{#update todos set completed = IIF(:active_count = 0, 0, 1)}}
{{/partial}}
{{#partial delete :id}}
{{#delete from todos WHERE id = :id}}
{{/partial}}
{{#partial post clear_completed}}
{{#delete from todos WHERE completed = 1}}
{{/partial}}
{{#param filter default='all' pattern="^(all|active|completed)$"}}
{{!-- Ensure the table exists (harmless if already created) --}}
{{#create table if not exists todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
completed INTEGER DEFAULT 0 CHECK(completed IN (0,1))
)}}
{{#let active_count = COUNT(*) from todos WHERE completed = 0}}
{{#let completed_count = COUNT(*) from todos WHERE completed = 1}}
{{#let total_count = COUNT(*) from todos}}
{{#let all_complete = (:active_count == 0 AND :total_count > 0)}}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>TODOMVC</title>
<style>
body { font-family: Arial, sans-serif; margin: 2rem; }
ul { list-style: none; padding: 0; }
li { margin-bottom: 0.5rem; }
li.completed label { text-decoration: line-through; color: #777; }
.filters { margin-top: 1rem; }
.filters a { text-decoration: none; margin: 0 0.5rem; }
.filters a.selected { font-weight: bold; }
.back-link {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #ccc;
font-size: 1.8rem;
font-weight: bold;
}
.back-link a {
color: #be5028;
text-decoration: none;
}
.code-column {
overflow-x: auto;
}
/* Mobile styles */
@media (max-width: 768px) {
body {
margin: 0;
padding: 0;
}
.back-link {
margin-bottom: 1rem;
}
}
</style>
</head>
<body>
<div class="back-link"><a href="/">← PageQL</a> <span style="font-size: 1.8rem; font-weight: bold; color:rgb(40, 170, 190);">TODOMVC</span></div>
<h1>TODOMVC</h1>
<div>
{{#if :total_count < 20}}
<input name="text" placeholder="What needs to be done?" maxlength="100" autofocus autocomplete="off"
hx-post="/todos/add" hx-trigger="keyup[key=='Enter']" hx-on:htmx:after-on-load="this.value=''">
{{/if}}
<ul>
{{#from todos
WHERE (:filter == 'all')
OR (:filter == 'active' AND completed = 0)
OR (:filter == 'completed' AND completed = 1)
ORDER BY id}}
<li {{#if completed}}class="completed"{{/if}}>
<input hx-post="/todos/{{id}}/toggle" class="toggle" type="checkbox" {{#if completed}}checked{{/if}}>
<label
contenteditable="false"
onclick="this.contentEditable=true;this.focus();"
onblur="this.contentEditable=false;"
onkeydown="if(event.key==='Enter'){event.preventDefault();this.blur();}"
oninput="if(this.innerText.length>100){this.innerText=this.innerText.slice(0,100);}"
hx-patch="/todos/{{id}}"
hx-trigger="blur"
hx-vals='js:{text: event.target.innerText.slice(0, 100)}'
hx-swap="none"
>{{text}}</label>
<button hx-delete="/todos/{{id}}" class="destroy"
style="cursor:pointer; background:none; border:none; color:#ac4a1a;">✕</button>
</li>
{{/from}}
</ul>
<input id="toggle-all" class="toggle-all" type="checkbox"
{{#if all_complete}}checked{{/if}} hx-post="/todos/toggle_all">
<label for="toggle-all">Mark all as complete</label>
<span class="todo-count">
<strong>{{active_count}}</strong>
item{{#if :active_count != 1}}s{{/if}} left
</span>
{{#if :completed_count > 0}}
<button class="clear-completed" hx-post="/todos/clear_completed">Clear completed</button>
{{/if}}
<div class="filters">
<a {{#if :filter == 'all'}}class="selected"{{/if}} href="/todos?filter=all">All</a> |
<a {{#if :filter == 'active'}}class="selected"{{/if}} href="/todos?filter=active">Active</a> |
<a {{#if :filter == 'completed'}}class="selected"{{/if}} href="/todos?filter=completed">Completed</a>
</div>
</div>
</body>
</html>