Last active
July 21, 2025 16:22
-
-
Save ThanosPapathanasiou/2bbce68de3a7529a9d54341ae8f4a31b to your computer and use it in GitHub Desktop.
todo app with dotnet10, htmx and bulma
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#: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