diff --git a/Cargo.lock b/Cargo.lock index a396c2a..281d8db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" +dependencies = [ + "axum", + "axum-core", + "bytes", + "form_urlencoded", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde_core", + "serde_html_form", + "serde_path_to_error", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base32" version = "0.5.1" @@ -934,6 +959,7 @@ version = "0.1.0" dependencies = [ "argon2", "axum", + "axum-extra", "base32", "base64", "chrono", @@ -1289,6 +1315,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde_core", +] + [[package]] name = "serde_json" version = "1.0.149" diff --git a/Cargo.toml b/Cargo.toml index f721615..27a0eb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] argon2 = "0.5.3" axum = "0.8.8" +axum-extra = { version = "0.12.5", features = ["form"] } base32 = "0.5.1" base64 = "0.22.1" chrono = { version = "0.4.43", features = ["serde"] } diff --git a/src/web/pages/mod.rs b/src/web/pages/mod.rs index d947723..1586a27 100644 --- a/src/web/pages/mod.rs +++ b/src/web/pages/mod.rs @@ -31,8 +31,10 @@ pub fn pages() -> Router { .route("/persons", get(persons::page)) .route("/persons/create", post(persons::create)) .route("/logs", get(logs::page)) + // .route("/quotes", get(quotes::page)) .route("/quotes/add", get(quotes::add::page)) + .route("/quotes/add-form", post(quotes::add::form)) } pub fn base(title: &str, inner: Markup) -> Markup { diff --git a/src/web/pages/quotes/add.rs b/src/web/pages/quotes/add.rs index 04ad6d4..0a34cd9 100644 --- a/src/web/pages/quotes/add.rs +++ b/src/web/pages/quotes/add.rs @@ -1,14 +1,26 @@ use axum::{ + Json, extract::Request, + http::HeaderMap, response::{IntoResponse, Response}, }; +use axum_extra::extract::Form; +use chrono::{DateTime, Utc}; +use chrono_tz::Europe::Warsaw; use maud::{PreEscaped, html}; +use serde::Deserialize; +use uuid::Uuid; use crate::{ database, error::CompositeError, + logs::{LogAction, LogEntry}, persons::Name, - users::{User, auth::UserAuthenticate}, + quotes::Quote, + users::{ + User, + auth::{UserAuthRequired, UserAuthenticate}, + }, web::{components::nav::nav, icons, pages::base}, }; @@ -29,7 +41,8 @@ pub async fn page(req: Request) -> Result { span class="text-2xl font-semibold font-lora" {"Quote Maker"} } } - div class="border border-neutral-200/25 bg-neutral-200/5 rounded-md p-4 flex flex-col" { + form method="post" action="/quotes/add-form" + class="border border-neutral-200/25 bg-neutral-200/5 rounded-md p-4 flex flex-col" { @for i in 1..=2 { div class="flex justify-between gap-4" { div class="flex flex-col flex-1" { @@ -38,10 +51,6 @@ pub async fn page(req: Request) -> Result { input type="text" name="quoteline" placeholder="They said..." autocomplete="off" class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25"; } - // label for=(format!("line-{i}")) class="mb-1" {(format!("Quote Line #{i}"))} - // input type="text" id=(format!("line-{i}")) name=(format!("line-{i}")) - // placeholder=(format!("They said...")) autocomplete="off" - // class="px-2 py-1 mb-2 bg-neutral-950/50 rounded border border-neutral-200/25"; } div class="flex flex-col" { label { @@ -50,18 +59,10 @@ pub async fn page(req: Request) -> Result { class="px-2 py-1.5 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25"{ option {"--"} @for name in &names { - option {(name.name)} + option value=(name.id.to_string()) {(name.name)} } } } - // label for=(format!("who-{i}")) class="mb-1" {(format!("Quote Author #{i}"))} - // select id=(format!("line-{i}")) name=(format!("line-{i}")) autocomplete="off" - // class="px-2 py-1.5 mb-2 bg-neutral-950/50 rounded border border-neutral-200/25" { - // option {"--"} - // @for name in &names { - // option {(name.name)} - // } - // } } } } @@ -77,7 +78,9 @@ pub async fn page(req: Request) -> Result { div class="flex flex-col flex-1" { label class="w-full" { p class="mb-1" {"Time of utterance"} - input type="text" name="time" autocomplete="off" placeholder="2026-04-05T01:14:05+02:00" + input type="hidden" name="time" id="time_hidden"; + input type="datetime-local" autocomplete="off" + onchange="document.getElementById('time_hidden').value = new Date(this.value).toISOString()" class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25"; } } @@ -100,3 +103,44 @@ pub async fn page(req: Request) -> Result { ) .into_response()) } + +#[derive(Deserialize, Debug)] +pub struct IncomingQuote { + #[serde(rename = "quoteline")] + lines: Vec, + #[serde(rename = "quoteauthor")] + authors: Vec, + location: String, + time: String, + context: String, +} +pub async fn form( + headers: HeaderMap, + Form(form): Form, +) -> Result { + let u = User::authenticate(&headers)?.required()?; + let mut conn = database::conn()?; + let tx = conn.transaction()?; + + let authors = form + .authors + .into_iter() + .map(|nid| Name::get_by_id(&tx, nid).unwrap()); + let lines = form.lines.into_iter().zip(authors).collect(); + let timestamp = DateTime::parse_from_rfc3339(&form.time) + .unwrap_or_else(|_| Utc::now().with_timezone(&Warsaw).fixed_offset()); + let context = match form.context.trim() { + "" => None, + s => Some(s.to_string()), + }; + let location = match form.location.trim() { + "" => None, + s => Some(s.to_string()), + }; + + let q = Quote::create(&tx, lines, timestamp, context, location, u.id, false)?; + LogEntry::new(&tx, u, LogAction::CreateQuote { id: q.id })?; + tx.commit()?; + + Ok(Json(q).into_response()) +}