From 38dbf10abe2a75ae88ff2fa47d9daee4ba5bad2c Mon Sep 17 00:00:00 2001 From: Nettika Date: Sun, 25 Jan 2026 14:46:29 -0800 Subject: [PATCH] Create a projects board display --- .gitignore | 1 + Cargo.lock | 46 ++++++++++++++ Cargo.toml | 1 + src/db.rs | 173 ++++++++++++++++++++++++++-------------------------- src/main.rs | 30 +++++++++ todo.md | 2 +- 6 files changed, 164 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf..b82c298 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +projects.db diff --git a/Cargo.lock b/Cargo.lock index 7324e24..83aea59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,10 +497,33 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" name = "makeprogress" version = "0.1.0" dependencies = [ + "maud", "rouille", "rusqlite", ] +[[package]] +name = "maud" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa" +dependencies = [ + "itoa", + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "memchr" version = "2.7.6" @@ -617,6 +640,29 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.106" diff --git a/Cargo.toml b/Cargo.toml index 75ffccb..4487223 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2024" [dependencies] rouille = "3.6" rusqlite = { version = "0.32", features = ["bundled"] } +maud = "0.26" diff --git a/src/db.rs b/src/db.rs index 0be33c8..d941482 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,96 +1,93 @@ use crate::project::Project; use rusqlite::{Connection, Result}; -pub struct Database { - conn: Connection, +fn open_connection() -> Result { + let conn = Connection::open("projects.db")?; + conn.execute_batch(include_str!("schema.sql"))?; + Ok(conn) } -impl Database { - pub fn new(path: &str) -> Result { - let conn = Connection::open(path)?; +pub fn create_project(title: String) -> Result { + let conn = open_connection()?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64; - conn.execute_batch(include_str!("schema.sql"))?; + conn.execute( + "INSERT INTO projects (title, created_time, last_modified_time, percentage_completed, archived) + VALUES (?1, ?2, ?3, ?4, ?5)", + (&title, now, now, 0, 0), + )?; - Ok(Self { conn }) - } - - pub fn create_project(&self, title: String) -> Result { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - - self.conn.execute( - "INSERT INTO projects (title, created_time, last_modified_time, percentage_completed, archived) - VALUES (?1, ?2, ?3, ?4, ?5)", - (&title, now, now, 0, 0), - )?; - - Ok(self.conn.last_insert_rowid()) - } - - pub fn update_project_progress(&self, id: i64, percentage: i32) -> Result<()> { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - - self.conn.execute( - "UPDATE projects SET percentage_completed = ?1, last_modified_time = ?2 WHERE id = ?3", - (percentage, now, id), - )?; - - Ok(()) - } - - pub fn archive_project(&self, id: i64) -> Result<()> { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - - self.conn.execute( - "UPDATE projects SET archived = 1, last_modified_time = ?1 WHERE id = ?2", - (now, id), - )?; - - Ok(()) - } - - pub fn unarchive_project(&self, id: i64) -> Result<()> { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - - self.conn.execute( - "UPDATE projects SET archived = 0, last_modified_time = ?1 WHERE id = ?2", - (now, id), - )?; - - Ok(()) - } - - pub fn list_all_projects(&self) -> Result> { - let mut stmt = self.conn.prepare( - "SELECT id, title, created_time, last_modified_time, percentage_completed, archived - FROM projects - ORDER BY last_modified_time DESC", - )?; - - let projects = stmt - .query_map([], |row| { - Ok(Project { - id: row.get(0)?, - title: row.get(1)?, - created_time: row.get(2)?, - last_modified_time: row.get(3)?, - percentage_completed: row.get(4)?, - archived: row.get::<_, i32>(5)? != 0, - }) - })? - .collect::>>()?; - - Ok(projects) - } + Ok(conn.last_insert_rowid()) +} + +pub fn update_project_progress(id: i64, percentage: i32) -> Result<()> { + let conn = open_connection()?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + + conn.execute( + "UPDATE projects SET percentage_completed = ?1, last_modified_time = ?2 WHERE id = ?3", + (percentage, now, id), + )?; + + Ok(()) +} + +pub fn archive_project(id: i64) -> Result<()> { + let conn = open_connection()?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + + conn.execute( + "UPDATE projects SET archived = 1, last_modified_time = ?1 WHERE id = ?2", + (now, id), + )?; + + Ok(()) +} + +pub fn unarchive_project(id: i64) -> Result<()> { + let conn = open_connection()?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + + conn.execute( + "UPDATE projects SET archived = 0, last_modified_time = ?1 WHERE id = ?2", + (now, id), + )?; + + Ok(()) +} + +pub fn list_all_projects() -> Result> { + let conn = open_connection()?; + let mut stmt = conn.prepare( + "SELECT id, title, created_time, last_modified_time, percentage_completed, archived + FROM projects + ORDER BY last_modified_time DESC", + )?; + + let projects = stmt + .query_map([], |row| { + Ok(Project { + id: row.get(0)?, + title: row.get(1)?, + created_time: row.get(2)?, + last_modified_time: row.get(3)?, + percentage_completed: row.get(4)?, + archived: row.get::<_, i32>(5)? != 0, + }) + })? + .collect::>>()?; + + Ok(projects) } diff --git a/src/main.rs b/src/main.rs index d6f2686..840510a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use maud::{DOCTYPE, html}; use rouille::router; mod db; @@ -16,6 +17,35 @@ fn main() { let js = include_bytes!("project-card.js"); rouille::Response::from_data("application/javascript", js.as_ref()) }, + (GET) ["/projects"] => { + let projects = db::list_all_projects().unwrap_or_default(); + + let markup = html! { + (DOCTYPE) + html { + head { + meta charset="utf-8"; + meta name="viewport" content="width=device-width, initial-scale=1"; + link rel="stylesheet" href="/main.css"; + script src="/project-card.js" {} + } + body { + main { + section { + @for project in &projects { + project-card + title=(project.title) + percentage=(project.percentage_completed) + archived=(project.archived) {} + } + } + } + } + } + }; + + rouille::Response::html(markup.into_string()) + }, _ => rouille::Response::empty_404() ) }); diff --git a/todo.md b/todo.md index cdcdad0..557fae8 100644 --- a/todo.md +++ b/todo.md @@ -7,6 +7,6 @@ [x] Create a `main.css` file in src. Use mvp.css as a starting point. [x] Create a `project-card.js` file in src that creates a web component for displaying a project. [x] Using a Rouille router, create `GET /main.css` and `GET /project-card.js` endpoints that returns the relevant files. Use the include_bytes! macro. -[ ] Create a `GET /projects` endpoint. Using Maud for markup generation, have this endpoint return an HTML page that shows all projects. Each project should be a `project-card` web component. Keep the page simple: no title or any buttons currently. +[x] Create a `GET /projects` endpoint. Using Maud for markup generation, have this endpoint return an HTML page that shows all projects. Each project should be a `project-card` web component. Keep the page simple: no title or any buttons currently. [ ] Create a `POST /projects` endpoint that accepts URL encoded data and creates a new project. [ ] Create a `GET /new-project` endpoint that returns a HTML page with a project creation form. Keep it simple. \ No newline at end of file