Skip to content

Instantly share code, notes, and snippets.

@ThanosPapathanasiou
Last active July 21, 2025 16:22
Show Gist options
  • Save ThanosPapathanasiou/2bbce68de3a7529a9d54341ae8f4a31b to your computer and use it in GitHub Desktop.
Save ThanosPapathanasiou/2bbce68de3a7529a9d54341ae8f4a31b to your computer and use it in GitHub Desktop.
todo app with dotnet10, htmx and bulma
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
using System.IO;
using System.Text;
using System.Text.Json;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
// application startup
var todoFileLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "todo.json");
Console.WriteLine($"Location of todo.json file: {todoFileLocation}");
if (!File.Exists(todoFileLocation)) {
var todos = new List<TodoItem>() { new TodoItem(Guid.NewGuid(), "Use this application.", true) };
string json = JsonSerializer.Serialize(todos);
File.WriteAllText(todoFileLocation, json);
}
var todoRepository = new TodoRepository(todoFileLocation);
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) => { context.Response.ContentType = "text/html"; await next(); });
app.MapGet("/", () => Views.Index(todoRepository.GetAll()));
app.MapPost("/todos", ([FromForm] string todo_description) => {
var newTodo = todoRepository.Create(todo_description);
return newTodo.ToHtmlView();
}).DisableAntiforgery();
app.MapPut("/todos/{id}/update_checked", (Guid id, [FromForm] bool todo_checked) => {
var updatedTodo = todoRepository.Update(id, todo_checked);
return updatedTodo.ToHtmlView();
}).DisableAntiforgery();
app.MapPut("/todos/{id}/update_description", (Guid id, [FromForm] string todo_description) => {
var updatedTodo = todoRepository.Update(id, todo_description);
return updatedTodo.ToHtmlView();
}).DisableAntiforgery();
app.MapDelete("/todos/{id}", (Guid id) => {
todoRepository.Delete(id);
return string.Empty;
});
app.Run();
// 'Views'
public static class Views {
public static string Index(IEnumerable<TodoItem> todos) =>
$"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Simple todo application</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
<script src="https://unpkg.com/[email protected]"></script>
{ CustomCss }
{ CustomJs }
</head>
<body>
<div class="columns is-centered">
<section class="hero column is-half-desktop">
<div class="hero-body">
<h1 class="title">TodoMVC</h1>
<h2 class="subtitle">A simple single-page single-file application.</h2>
<nav class="panel mt-6">
<p class="panel-heading">
TODOS
</p>
<div class="control is-expanded">
<input
id="new-todo"
class="input"
name="todo_description"
placeholder="What needs to be done?"
autofocus
hx-target="#todo-list"
hx-post="/todos"
hx-trigger="keyup[key=='Enter']"
hx-swap="beforeend"
hx-on::after-request="if(event.detail.successful) this.value=''"
/>
</br>
<ul id="todo-list">
{ todos.ToHtmlView() }
</ul>
</div>
</nav>
</div>
</section>
</div>
<footer class="footer">
<div class="content has-text-centered">
<p>
This todo application was built by
<a href="https://github.com/ThanosPapathanasiou">Thanos</a>
using <a href="https://dotnet.microsoft.com/en-us/download/dotnet/10.0">dotnet10</a>
and <a href="https://htmx.org">htmx</a>
</p>
</div>
</footer>
</body>
</html>
""";
public static string ToHtmlView(this IEnumerable<TodoItem> todos) =>
string.Join(Environment.NewLine, todos.Select(x => x.ToHtmlView()));
public static string ToHtmlView(this TodoItem todo) =>
$"""
<li hx-target="this" hx-swap="outerHTML" class="is-flex is-align-items-center">
<input
type="checkbox"
class="checkbox big-checkbox m-1"
is="boolean-checkbox"
name="todo_checked"
{Completed(todo.Done)}
hx-put="/todos/{todo.Id}/update_checked"
/>
<input
type="text"
class="input m-1 {Completed(todo.Done)}"
name="todo_description"
value="{todo.Description}"
readonly
ondblclick="this.readOnly = false;"
hx-put="/todos/{todo.Id}/update_description"
/>
<button
class="button m-1 is-danger is-outlined"
hx-delete="/todos/{todo.Id}">X</button>
</li>
""";
public static string Completed(bool done) => done ? "checked" : "not-checked";
public static string CustomJs =>
"""
<script>
class BooleanCheckbox extends HTMLInputElement {
constructor() { super(); }
get checked() { return true; }
get value() {
if (super.checked) { return true; }
else { return false; }
}
}
customElements.define("boolean-checkbox", BooleanCheckbox, { extends: "input" });
</script>
""";
public static string CustomCss =>
"""
<style>
.checked {
text-decoration: line-through;
}
.big-checkbox {
width: 2rem;
height: 2rem;
}
</style>
""";
}
public record TodoItem(Guid Id, string Description, bool Done);
public class TodoRepository(string FileLocation) {
private readonly string _fileLocation = FileLocation;
private readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
private List<TodoItem> ReadTodos() {
string json = File.ReadAllText(_fileLocation);
return JsonSerializer.Deserialize<List<TodoItem>>(json, _jsonOptions) ?? [];
}
private void WriteTodos(List<TodoItem> todos) {
string json = JsonSerializer.Serialize(todos, _jsonOptions);
File.WriteAllText(_fileLocation, json);
}
public TodoItem Create(string description) {
var todos = ReadTodos();
var newTodo = new TodoItem(Guid.NewGuid(), description, false);
todos.Add(newTodo);
WriteTodos(todos);
return newTodo;
}
public List<TodoItem> GetAll() {
return ReadTodos();
}
public TodoItem Update(Guid id, bool isDone) {
var todos = ReadTodos();
var todo = todos.Single(t => t.Id == id);
var updatedTodo = todo with { Done = isDone };
todos[todos.IndexOf(todo)] = updatedTodo;
WriteTodos(todos);
return updatedTodo;
}
public TodoItem Update(Guid id, string description) {
var todos = ReadTodos();
var todo = todos.Single(t => t.Id == id);
var updatedTodo = todo with { Description = description };
todos[todos.IndexOf(todo)] = updatedTodo;
WriteTodos(todos);
return updatedTodo;
}
public bool Delete(Guid id) {
var todos = ReadTodos();
var todo = todos.Single(t => t.Id == id);
todos.Remove(todo);
WriteTodos(todos);
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment