Use an embedded database
- Add a CLI with subcommands for "serve" and "add-user" - Inline the manifest with serde_json - Move the server to a separate file - Change login to token cookies - Persist the task form between loads
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
.cargo/
|
||||
/target
|
||||
tasks.txt
|
||||
app.db
|
158
Cargo.lock
generated
158
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
@@ -38,6 +38,55 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
version = "1.1.0"
|
||||
@@ -56,6 +105,15 @@ version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.6.0"
|
||||
@@ -128,7 +186,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
@@ -138,6 +199,52 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -240,6 +347,12 @@ dependencies = [
|
||||
"crc32fast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
@@ -420,6 +533,12 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
@@ -671,6 +790,15 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redb"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84b1de48a7cf7ba193e81e078d17ee2b786236eed1d3f7c60f8a09545efc4925"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.7"
|
||||
@@ -785,6 +913,12 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.90"
|
||||
@@ -876,8 +1010,15 @@ dependencies = [
|
||||
name = "todo-app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
"clap",
|
||||
"maud",
|
||||
"redb",
|
||||
"rouille",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -924,6 +1065,21 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
@@ -3,8 +3,13 @@ name = "todo-app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.3"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
clap = { version = "4.5.21", features = ["derive"] }
|
||||
maud = "0.26.0"
|
||||
redb = "2.2.0"
|
||||
rouille = "3.6.2"
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
uuid = { version = "1.11.0", features = ["v4", "v7"] }
|
||||
|
76
src/database.rs
Normal file
76
src/database.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use redb::{Database, ReadableTable, TableDefinition};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::task::Task;
|
||||
|
||||
const DATABASE_FILE: &str = "app.db";
|
||||
const USERS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("users");
|
||||
const AUTH_TABLE: TableDefinition<&str, &str> = TableDefinition::new("sessions");
|
||||
const TASKS_TABLE: TableDefinition<uuid::Bytes, Task> = TableDefinition::new("tasks");
|
||||
|
||||
pub struct AppDatabase(Database);
|
||||
|
||||
impl AppDatabase {
|
||||
pub fn create() -> Result<Self, redb::Error> {
|
||||
let database = Database::create(DATABASE_FILE)?;
|
||||
let transaction = database.begin_write()?;
|
||||
transaction.open_table(USERS_TABLE)?;
|
||||
transaction.open_table(AUTH_TABLE)?;
|
||||
transaction.open_table(TASKS_TABLE)?;
|
||||
transaction.commit()?;
|
||||
Ok(Self(database))
|
||||
}
|
||||
|
||||
pub fn add_user(&self, username: &str, password: &str) -> Result<(), redb::Error> {
|
||||
let transaction = self.0.begin_write()?;
|
||||
let mut users_table = transaction.open_table(USERS_TABLE)?;
|
||||
users_table.insert(username, password)?;
|
||||
drop(users_table);
|
||||
transaction.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_user(&self, username: &str) -> Result<Option<String>, redb::Error> {
|
||||
let transaction = self.0.begin_read()?;
|
||||
let users_table = transaction.open_table(USERS_TABLE)?;
|
||||
let user = users_table.get(username)?.map(|g| g.value().to_string());
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub fn create_auth_token(&self, username: &str) -> Result<String, redb::Error> {
|
||||
let token = Uuid::new_v4().to_string();
|
||||
let transaction = self.0.begin_write()?;
|
||||
let mut auth_table = transaction.open_table(AUTH_TABLE)?;
|
||||
auth_table.insert(token.as_str(), username)?;
|
||||
drop(auth_table);
|
||||
transaction.commit()?;
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub fn get_auth_user(&self, token: &str) -> Result<Option<String>, redb::Error> {
|
||||
let transaction = self.0.begin_read()?;
|
||||
let auth_table = transaction.open_table(AUTH_TABLE)?;
|
||||
let user = auth_table.get(token)?.map(|g| g.value().to_string());
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub fn add_task(&self, task: Task) -> Result<Uuid, redb::Error> {
|
||||
let id = Uuid::now_v7();
|
||||
let transaction = self.0.begin_write()?;
|
||||
let mut tasks_table = transaction.open_table(TASKS_TABLE)?;
|
||||
tasks_table.insert(id.as_bytes(), task)?;
|
||||
drop(tasks_table);
|
||||
transaction.commit()?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn get_tasks(&self) -> Result<Vec<Task>, redb::Error> {
|
||||
let transaction = self.0.begin_read()?;
|
||||
let tasks_table = transaction.open_table(TASKS_TABLE)?;
|
||||
let tasks = tasks_table
|
||||
.iter()?
|
||||
.map(|g| g.unwrap().1.value())
|
||||
.collect::<Vec<_>>();
|
||||
Ok(tasks)
|
||||
}
|
||||
}
|
123
src/main.rs
123
src/main.rs
@@ -1,95 +1,42 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::OpenOptions,
|
||||
io::{BufRead, BufReader, Write},
|
||||
};
|
||||
mod database;
|
||||
mod server;
|
||||
mod task;
|
||||
|
||||
use maud::{html, Markup, DOCTYPE};
|
||||
use rouille::{input::basic_http_auth, post_input, router, try_or_400, Request, Response};
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use database::AppDatabase;
|
||||
use server::serve;
|
||||
|
||||
const TASKS: &str = "tasks.txt";
|
||||
const DEFAULT_PORT: u16 = 8000;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "todo", about = "Todo list web app")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
#[command(name = "add-user", about = "Add an authorized user")]
|
||||
AddUser { username: String, password: String },
|
||||
#[command(name = "serve", about = "Serve the web app")]
|
||||
Serve {
|
||||
/// Defaults to 8000
|
||||
port: Option<u16>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let login: String = env::var("LOGIN").unwrap();
|
||||
let password: String = env::var("PASSWORD").unwrap();
|
||||
|
||||
rouille::start_server("0.0.0.0:8000", move |request| {
|
||||
if !authenticated(&request, &login, &password) {
|
||||
return Response::basic_http_auth_login_required("app");
|
||||
}
|
||||
router!(request,
|
||||
(GET) ["/"] => { index() },
|
||||
(GET) ["/manifest.json"] => {
|
||||
Response::from_data("application/manifest+json", include_bytes!("manifest.json"))
|
||||
},
|
||||
(GET) ["/icon.png"] => {
|
||||
Response::from_data("image/png", include_bytes!("icon.png"))
|
||||
},
|
||||
(POST) ["/add"] => {
|
||||
let input = try_or_400!(post_input!(request, {
|
||||
task: String,
|
||||
}));
|
||||
add_task(&input.task);
|
||||
Response::html(render_tasks())
|
||||
},
|
||||
_ => Response::empty_404()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn authenticated(request: &Request, login: &str, password: &str) -> bool {
|
||||
match basic_http_auth(request) {
|
||||
Some(a) => a.login == login && a.password == password,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn index() -> Response {
|
||||
Response::html(html!(
|
||||
(DOCTYPE)
|
||||
html lang="en" color-mode="user" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
meta name="viewport" content="width=device-width, initial-scale=1";
|
||||
title { "To-do" }
|
||||
link rel="stylesheet" href="https://unpkg.com/mvp.css@1.17.0";
|
||||
link rel="manifest" href="/manifest.json" crossorigin="use-credentials";
|
||||
link rel="icon" href="/icon.png" crossorigin="use-credentials";
|
||||
script src="https://unpkg.com/htmx.org@2.0.3" {}
|
||||
}
|
||||
body {
|
||||
main {
|
||||
h1 { "To-do" }
|
||||
form hx-post="/add" hx-target="#list" hx-swap="innerHTML" hx-on:submit="this.elements.task.value = ''" {
|
||||
input type="text" name="task" placeholder="Task" required;
|
||||
input type="submit" value="Add";
|
||||
}
|
||||
ul id="list" {
|
||||
(render_tasks())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fn add_task(task: &str) {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(TASKS)
|
||||
.unwrap();
|
||||
writeln!(file, "{}", task).unwrap();
|
||||
}
|
||||
|
||||
fn render_tasks() -> Markup {
|
||||
let reader = match OpenOptions::new().read(true).open(TASKS) {
|
||||
Ok(file) => BufReader::new(file),
|
||||
Err(_) => return html! {},
|
||||
};
|
||||
html! {
|
||||
@for line in reader.lines() {
|
||||
li { (line.unwrap()) }
|
||||
let cli = Cli::parse();
|
||||
let database = AppDatabase::create().unwrap();
|
||||
match cli.command {
|
||||
Some(Commands::AddUser { username, password }) => {
|
||||
database.add_user(&username, &password).unwrap();
|
||||
println!("Added user {}", username);
|
||||
}
|
||||
Some(Commands::Serve { port }) => {
|
||||
serve(port.unwrap_or(DEFAULT_PORT), database);
|
||||
}
|
||||
None => Cli::command().print_help().unwrap(),
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"short_name": "To-do",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
]
|
||||
}
|
154
src/server.rs
Normal file
154
src/server.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use std::io::stdout;
|
||||
|
||||
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
||||
use rouille::{post_input, router, try_or_400, Request, Response};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
database::AppDatabase,
|
||||
task::{Task, TaskPriority, TaskStatus},
|
||||
};
|
||||
|
||||
pub fn serve(port: u16, database: AppDatabase) {
|
||||
println!("Server started at http://localhost:{}", port);
|
||||
rouille::start_server(format!("0.0.0.0:{}", port), move |request| {
|
||||
rouille::log(request, stdout(), || route(request, &database))
|
||||
});
|
||||
}
|
||||
|
||||
fn route(request: &Request, database: &AppDatabase) -> Response {
|
||||
let token = rouille::input::cookies(request)
|
||||
.find(|c| c.0 == "token")
|
||||
.map(|c| c.1);
|
||||
let authenticated = token.is_some_and(|t| database.get_auth_user(t).unwrap().is_some());
|
||||
router!(request,
|
||||
(GET) ["/"] => match authenticated {
|
||||
true => Response::html(render_index()),
|
||||
false => Response::redirect_302("/login")
|
||||
},
|
||||
(GET) ["/login"] => {
|
||||
Response::html(render_login())
|
||||
},
|
||||
(GET) ["/manifest.json"] => {
|
||||
Response::json(&json!({
|
||||
"short_name": "To-do",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
]
|
||||
}))
|
||||
},
|
||||
(GET) ["/icon.png"] => {
|
||||
Response::from_data("image/png", include_bytes!("icon.png"))
|
||||
},
|
||||
(POST) ["/login"] => {
|
||||
let input = try_or_400!(post_input!(request, {
|
||||
username: String,
|
||||
password: String,
|
||||
}));
|
||||
if database.get_user(&input.username).unwrap() == Some(input.password) {
|
||||
let token = database.create_auth_token(&input.username).unwrap();
|
||||
Response::empty_204()
|
||||
.with_additional_header("Set-Cookie", format!("token={}; HttpOnly; Secure; SameSite=Strict", token))
|
||||
.with_additional_header("HX-Redirect", "/")
|
||||
} else {
|
||||
Response::text("Invalid credentials")
|
||||
}
|
||||
},
|
||||
(GET) ["/tasks"] => match authenticated {
|
||||
true => Response::html(render_ongoing_tasks(database)),
|
||||
false => Response::text("Unauthorized")
|
||||
.with_status_code(401)
|
||||
.with_additional_header("HX-Redirect", "/login")
|
||||
},
|
||||
(POST) ["/add_task"] => match authenticated {
|
||||
true => {
|
||||
let input = try_or_400!(post_input!(request, {
|
||||
task: String,
|
||||
}));
|
||||
database.add_task(Task::new(&input.task, TaskStatus::Ongoing, TaskPriority::Low)).unwrap();
|
||||
Response::html(render_ongoing_tasks(database))
|
||||
}
|
||||
false => Response::text("Unauthorized")
|
||||
.with_status_code(401)
|
||||
.with_additional_header("HX-Redirect", "/login")
|
||||
},
|
||||
_ => Response::empty_404()
|
||||
)
|
||||
}
|
||||
|
||||
fn render_page(title: &str, content: Markup) -> Markup {
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" color-mode="user" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
meta name="viewport" content="width=device-width, initial-scale=1";
|
||||
title { (title) }
|
||||
link rel="stylesheet" href="https://unpkg.com/mvp.css@1.17.0";
|
||||
link rel="manifest" href="/manifest.json";
|
||||
link rel="icon" href="/icon.png";
|
||||
script src="https://unpkg.com/htmx.org@2.0.3" {}
|
||||
script src="https://unpkg.com/form-persistence@2.0.6" {}
|
||||
}
|
||||
body {
|
||||
(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_index() -> Markup {
|
||||
render_page(
|
||||
"To-do",
|
||||
html! {
|
||||
main {
|
||||
h1 { "To-do" }
|
||||
form #add-task hx-post="/add_task" hx-target="#list" hx-swap="innerHTML" {
|
||||
input #task type="text" name="task" placeholder="Task" required;
|
||||
input type="submit" value="Add";
|
||||
}
|
||||
script {
|
||||
(PreEscaped(
|
||||
r#"document.addEventListener("DOMContentLoaded",()=>FormPersistence.persist(document.getElementById("add-task")));document.addEventListener("htmx:afterRequest",event=>event.detail.successful&&document.getElementById("add-task").reset())"#
|
||||
))
|
||||
}
|
||||
ul #list hx-get="/tasks" hx-trigger="load" {}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_login() -> Markup {
|
||||
render_page(
|
||||
"Login",
|
||||
html! {
|
||||
main {
|
||||
h1 { "Login" }
|
||||
form hx-post="/login" hx-target="#message" hx-swap="innerHTML" {
|
||||
input type="text" name="username" placeholder="Username" required;
|
||||
input type="password" name="password" placeholder="Password" required;
|
||||
input type="submit" value="Login";
|
||||
}
|
||||
p #message style="color: red;" {}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_ongoing_tasks(database: &AppDatabase) -> Markup {
|
||||
let tasks = database
|
||||
.get_tasks()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|t| !t.is_done());
|
||||
html! {
|
||||
@for task in tasks {
|
||||
li { (task.description()) }
|
||||
}
|
||||
}
|
||||
}
|
80
src/task.rs
Normal file
80
src/task.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use redb::{TypeName, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TaskStatus {
|
||||
Ongoing,
|
||||
Completed(DateTime<Utc>),
|
||||
Abandoned(DateTime<Utc>),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TaskPriority {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Task {
|
||||
description: String,
|
||||
status: TaskStatus,
|
||||
priority: TaskPriority,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn new(description: &str, status: TaskStatus, priority: TaskPriority) -> Self {
|
||||
Task {
|
||||
description: description.to_string(),
|
||||
status,
|
||||
priority,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
matches!(
|
||||
self.status,
|
||||
TaskStatus::Completed(_) | TaskStatus::Abandoned(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Value for Task {
|
||||
type SelfType<'a>
|
||||
= Task
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
type AsBytes<'a>
|
||||
= Vec<u8>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
fn fixed_width() -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a>
|
||||
where
|
||||
Self: 'a,
|
||||
{
|
||||
bincode::deserialize(data).unwrap()
|
||||
}
|
||||
|
||||
fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a>
|
||||
where
|
||||
Self: 'a,
|
||||
Self: 'b,
|
||||
{
|
||||
bincode::serialize(value).unwrap()
|
||||
}
|
||||
|
||||
fn type_name() -> TypeName {
|
||||
TypeName::new("Task")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user