From 9521fc4aef0e3ec0381490cbaedafaf3dd8843bc Mon Sep 17 00:00:00 2001 From: lew Date: Thu, 9 Apr 2026 23:00:48 +0100 Subject: [PATCH] feat: initial draft of drawings, still missing some features from my other site --- .env.example | 2 +- README.md | 3 ++- module.nix | 2 +- src/render.rs | 4 ++-- src/telegram.rs | 2 +- src/web.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ templates/default.css | 1 + 7 files changed, 50 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 2f47748..be53f25 100644 --- a/.env.example +++ b/.env.example @@ -59,7 +59,7 @@ # Custom CSS injected into a style tag. # Classes: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, -# .guestbook-textarea, .guestbook-button, .entry-header, .entry-name, +# .guestbook-textarea, .guestbook-button, .entry-header, .entry-date, .entry-name, # .entry-website, .entry-body, .entry-separator # BOOK_STYLE= diff --git a/README.md b/README.md index 9cff78f..bfc15c8 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ Running `guestbook` with no env vars will give you a working guestbook on `local # Custom CSS injected into a style tag. # Classes: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, -# .guestbook-textarea, .guestbook-button, .entry-header, .entry-name, +# .guestbook-textarea, .guestbook-button, .entry-header, .entry-date, .entry-name, # .entry-website, .entry-body, .entry-separator # BOOK_STYLE= @@ -360,6 +360,7 @@ entries /* Entries */ .entry-header {} +.entry-date {} .entry-name {} .entry-website {} .entry-body {} diff --git a/module.nix b/module.nix index e4f8182..6fe86fe 100644 --- a/module.nix +++ b/module.nix @@ -179,7 +179,7 @@ in css = mkOption { type = types.str; default = ""; - description = "Custom CSS injected into a style tag. Use class names: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, .guestbook-textarea, .guestbook-button, .guestbook-canvas, .entry-header, .entry-name, .entry-website, .entry-body, .entry-drawing, .entry-separator"; + description = "Custom CSS injected into a style tag. Use class names: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, .guestbook-textarea, .guestbook-button, .guestbook-canvas, .entry-header, .entry-date, .entry-name, .entry-website, .entry-body, .entry-drawing, .entry-separator"; }; cssFile = mkOption { diff --git a/src/render.rs b/src/render.rs index 07ab1b4..be24b68 100644 --- a/src/render.rs +++ b/src/render.rs @@ -127,7 +127,7 @@ fn render_entry(entry: &Entry, config: &Config) -> String { escape_html(&entry.meta.name) }; let mut header = format!( - "{} - {}", + "{} - {}", &entry.meta.date[..10], name ); if config.enable_website_links && !entry.meta.website.is_empty() { @@ -152,7 +152,7 @@ fn render_entry(entry: &Entry, config: &Config) -> String { String::new() }; format!( - "\n{header}\n\n{body}{drawing_html}\n\n{}\n", + "\n{header}\n{drawing_html}\n{body}\n\n{}\n", config.separator ) } diff --git a/src/telegram.rs b/src/telegram.rs index 5cd7b9c..2001824 100644 --- a/src/telegram.rs +++ b/src/telegram.rs @@ -7,7 +7,7 @@ use crate::entries::{self, Entry, Status}; /// Send a notification to Telegram about a new entry. async fn notify(bot: &Bot, chat_id: ChatId, entry: &Entry) { - let short_id = entry.id.split('-').last().unwrap_or(&entry.id); + let short_id = entry.id.split('_').last().unwrap_or(&entry.id); let text = format!( "New guestbook entry:\n\nName: {}\nWebsite: {}\n\n{}\n\n/allow_{}\n/deny_{}", entry.meta.name, entry.meta.website, entry.body, short_id, short_id diff --git a/src/web.rs b/src/web.rs index 61a15db..c32e0b2 100644 --- a/src/web.rs +++ b/src/web.rs @@ -742,4 +742,46 @@ mod tests { let content = std::fs::read_to_string(entries[0].as_ref().unwrap().path()).unwrap(); assert!(content.contains("drawing = \"\"")); } + + #[tokio::test] + async fn test_drawing_full_roundtrip() { + let dir = tempfile::tempdir().unwrap(); + let mut config = test_config(dir.path()); + config.enable_drawings = true; + config.canvas_width = 400; + config.canvas_height = 200; + let (app, _rx) = test_app(config); + + // Submit with a drawing + let png = fake_png(400, 200); + let drawing_data = base64::engine::general_purpose::STANDARD.encode(&png); + let data_url = format!("data:image/png;base64,{drawing_data}"); + let body = format!( + "name=alice&message=hello&drawing={}", + urlencoding::encode(&data_url) + ); + post_form(&app, &body).await; + + // Approve the entry + let entries_dir = dir.path().join("entries"); + let entry_file = std::fs::read_dir(&entries_dir).unwrap().next().unwrap().unwrap(); + let content = std::fs::read_to_string(entry_file.path()).unwrap(); + let id = entry_file.path().file_stem().unwrap().to_str().unwrap().to_string(); + let mut entry = entries::Entry::parse(&id, &content).unwrap(); + entry.meta.status = entries::Status::Approved; + std::fs::write(entry_file.path(), entry.to_file_contents()).unwrap(); + + let drawing_filename = entry.meta.drawing.clone(); + assert!(!drawing_filename.is_empty(), "entry should have a drawing filename"); + + // Verify index shows the drawing + let html = get_index(&app).await; + assert!(html.contains("entry-drawing")); + assert!(html.contains(&format!("/drawings/{drawing_filename}"))); + + // Verify the drawing file is served + let (status, bytes) = get_path(&app, &format!("/drawings/{drawing_filename}")).await; + assert_eq!(status, StatusCode::OK); + assert_eq!(bytes, png); + } } diff --git a/templates/default.css b/templates/default.css index 91d8ba1..c25963e 100644 --- a/templates/default.css +++ b/templates/default.css @@ -29,6 +29,7 @@ /* Entries */ .entry-header {} +.entry-date {} .entry-name {} .entry-website {} .entry-body {}