Create a projects board display

This commit is contained in:
Nettika 2026-01-25 14:46:29 -08:00
parent 64e810d360
commit 38dbf10abe
No known key found for this signature in database
6 changed files with 164 additions and 89 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target /target
projects.db

46
Cargo.lock generated
View file

@ -497,10 +497,33 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
name = "makeprogress" name = "makeprogress"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"maud",
"rouille", "rouille",
"rusqlite", "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]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.6" version = "2.7.6"
@ -617,6 +640,29 @@ dependencies = [
"zerocopy", "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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"

View file

@ -6,3 +6,4 @@ edition = "2024"
[dependencies] [dependencies]
rouille = "3.6" rouille = "3.6"
rusqlite = { version = "0.32", features = ["bundled"] } rusqlite = { version = "0.32", features = ["bundled"] }
maud = "0.26"

173
src/db.rs
View file

@ -1,96 +1,93 @@
use crate::project::Project; use crate::project::Project;
use rusqlite::{Connection, Result}; use rusqlite::{Connection, Result};
pub struct Database { fn open_connection() -> Result<Connection> {
conn: Connection, let conn = Connection::open("projects.db")?;
conn.execute_batch(include_str!("schema.sql"))?;
Ok(conn)
} }
impl Database { pub fn create_project(title: String) -> Result<i64> {
pub fn new(path: &str) -> Result<Self> { let conn = open_connection()?;
let conn = Connection::open(path)?; 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 }) Ok(conn.last_insert_rowid())
} }
pub fn create_project(&self, title: String) -> Result<i64> { pub fn update_project_progress(id: i64, percentage: i32) -> Result<()> {
let now = std::time::SystemTime::now() let conn = open_connection()?;
.duration_since(std::time::UNIX_EPOCH) let now = std::time::SystemTime::now()
.unwrap() .duration_since(std::time::UNIX_EPOCH)
.as_secs() as i64; .unwrap()
.as_secs() as i64;
self.conn.execute(
"INSERT INTO projects (title, created_time, last_modified_time, percentage_completed, archived) conn.execute(
VALUES (?1, ?2, ?3, ?4, ?5)", "UPDATE projects SET percentage_completed = ?1, last_modified_time = ?2 WHERE id = ?3",
(&title, now, now, 0, 0), (percentage, now, id),
)?; )?;
Ok(self.conn.last_insert_rowid()) Ok(())
} }
pub fn update_project_progress(&self, id: i64, percentage: i32) -> Result<()> { pub fn archive_project(id: i64) -> Result<()> {
let now = std::time::SystemTime::now() let conn = open_connection()?;
.duration_since(std::time::UNIX_EPOCH) let now = std::time::SystemTime::now()
.unwrap() .duration_since(std::time::UNIX_EPOCH)
.as_secs() as i64; .unwrap()
.as_secs() as i64;
self.conn.execute(
"UPDATE projects SET percentage_completed = ?1, last_modified_time = ?2 WHERE id = ?3", conn.execute(
(percentage, now, id), "UPDATE projects SET archived = 1, last_modified_time = ?1 WHERE id = ?2",
)?; (now, id),
)?;
Ok(())
} Ok(())
}
pub fn archive_project(&self, id: i64) -> Result<()> {
let now = std::time::SystemTime::now() pub fn unarchive_project(id: i64) -> Result<()> {
.duration_since(std::time::UNIX_EPOCH) let conn = open_connection()?;
.unwrap() let now = std::time::SystemTime::now()
.as_secs() as i64; .duration_since(std::time::UNIX_EPOCH)
.unwrap()
self.conn.execute( .as_secs() as i64;
"UPDATE projects SET archived = 1, last_modified_time = ?1 WHERE id = ?2",
(now, id), conn.execute(
)?; "UPDATE projects SET archived = 0, last_modified_time = ?1 WHERE id = ?2",
(now, id),
Ok(()) )?;
}
Ok(())
pub fn unarchive_project(&self, id: i64) -> Result<()> { }
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) pub fn list_all_projects() -> Result<Vec<Project>> {
.unwrap() let conn = open_connection()?;
.as_secs() as i64; let mut stmt = conn.prepare(
"SELECT id, title, created_time, last_modified_time, percentage_completed, archived
self.conn.execute( FROM projects
"UPDATE projects SET archived = 0, last_modified_time = ?1 WHERE id = ?2", ORDER BY last_modified_time DESC",
(now, id), )?;
)?;
let projects = stmt
Ok(()) .query_map([], |row| {
} Ok(Project {
id: row.get(0)?,
pub fn list_all_projects(&self) -> Result<Vec<Project>> { title: row.get(1)?,
let mut stmt = self.conn.prepare( created_time: row.get(2)?,
"SELECT id, title, created_time, last_modified_time, percentage_completed, archived last_modified_time: row.get(3)?,
FROM projects percentage_completed: row.get(4)?,
ORDER BY last_modified_time DESC", archived: row.get::<_, i32>(5)? != 0,
)?; })
})?
let projects = stmt .collect::<Result<Vec<_>>>()?;
.query_map([], |row| {
Ok(Project { Ok(projects)
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::<Result<Vec<_>>>()?;
Ok(projects)
}
} }

View file

@ -1,3 +1,4 @@
use maud::{DOCTYPE, html};
use rouille::router; use rouille::router;
mod db; mod db;
@ -16,6 +17,35 @@ fn main() {
let js = include_bytes!("project-card.js"); let js = include_bytes!("project-card.js");
rouille::Response::from_data("application/javascript", js.as_ref()) 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() _ => rouille::Response::empty_404()
) )
}); });

View file

@ -7,6 +7,6 @@
[x] Create a `main.css` file in src. Use mvp.css as a starting point. [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] 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. [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 `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. [ ] Create a `GET /new-project` endpoint that returns a HTML page with a project creation form. Keep it simple.