diff --git a/Cargo.lock b/Cargo.lock index e2ba88a..0f5c795 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,44 @@ dependencies = [ "syn 2.0.82", ] +[[package]] +name = "actix-multipart" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "derive_more 0.99.18", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "rand", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "actix-router" version = "0.5.3" @@ -1126,6 +1164,7 @@ dependencies = [ "actix", "actix-files", "actix-identity", + "actix-multipart", "actix-session", "actix-web", "askama", @@ -1350,6 +1389,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", + "strsim", "syn 2.0.82", ] @@ -2915,6 +2955,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + [[package]] name = "paste" version = "1.0.15" @@ -4134,6 +4180,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/cooked/Cargo.toml b/cooked/Cargo.toml index 05b10aa..35492e4 100644 --- a/cooked/Cargo.toml +++ b/cooked/Cargo.toml @@ -30,6 +30,7 @@ tantivy = "0.22.0" tempfile = "3.13.0" pulldown-cmark = "0.12.2" thiserror = "1.0.65" +actix-multipart = "0.7.2" [build-dependencies] diff --git a/cooked/assets/styles.css b/cooked/assets/styles.css index b9a40e1..4b40f44 100644 --- a/cooked/assets/styles.css +++ b/cooked/assets/styles.css @@ -1180,6 +1180,60 @@ html{ } } +.file-input{ + height: 3rem; + flex-shrink: 1; + padding-inline-end: 1rem; + font-size: 1rem; + line-height: 2; + line-height: 1.5rem; + overflow: hidden; + border-radius: var(--rounded-btn, 0.5rem); + border-width: 1px; + border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); + --tw-border-opacity: 0; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + +.file-input::file-selector-button{ + margin-inline-end: 1rem; + display: inline-flex; + height: 100%; + flex-shrink: 0; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-wrap: wrap; + align-items: center; + justify-content: center; + padding-left: 1rem; + padding-right: 1rem; + text-align: center; + font-size: 0.875rem; + line-height: 1.25rem; + line-height: 1em; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 200ms; + border-style: solid; + --tw-border-opacity: 1; + border-color: var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + font-weight: 600; + text-transform: uppercase; + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); + text-decoration-line: none; + border-width: var(--border-btn, 1px); + animation: button-pop var(--animation-btn, 0.25s) ease-out; +} + .label{ display: flex; -webkit-user-select: none; @@ -1671,6 +1725,42 @@ details.collapse summary::-webkit-details-marker{ gap: 1rem; } +.file-input:focus{ + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.file-input-disabled, + .file-input[disabled]{ + cursor: not-allowed; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + --tw-text-opacity: 0.2; +} + +.file-input-disabled::-moz-placeholder, .file-input[disabled]::-moz-placeholder{ + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.file-input-disabled::placeholder, + .file-input[disabled]::placeholder{ + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.file-input-disabled::file-selector-button, .file-input[disabled]::file-selector-button{ + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.2; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; +} + .input input{ --tw-bg-opacity: 1; background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); @@ -2299,6 +2389,10 @@ details.collapse summary::-webkit-details-marker{ width: 100%; } +.max-w-xs{ + max-width: 20rem; +} + .shrink-0{ flex-shrink: 0; } @@ -2435,6 +2529,10 @@ details.collapse summary::-webkit-details-marker{ padding: 3rem; } +.p-2{ + padding: 0.5rem; +} + .p-4{ padding: 1rem; } @@ -2906,6 +3004,18 @@ code{ border-color: rgb(229 231 235 / var(--tw-border-opacity)); } +.styled-checkbox:has(input:checked){ + --tw-border-opacity: 1; + border-color: rgb(21 128 61 / var(--tw-border-opacity)); + --tw-text-opacity: 1; + color: rgb(21 128 61 / var(--tw-text-opacity)); +} + +.styled-checkbox:has(input:not(:checked)){ + --tw-border-opacity: 1; + border-color: rgb(156 163 175 / var(--tw-border-opacity)); +} + .marker\:text-rose-900 *::marker{ color: rgb(136 19 55 ); } diff --git a/cooked/src/routes.rs b/cooked/src/routes.rs index 01339c2..79c3767 100644 --- a/cooked/src/routes.rs +++ b/cooked/src/routes.rs @@ -2,6 +2,7 @@ use crate::actors::search::{Find, Search}; use crate::types::*; use crate::{entities, entities::prelude::*, filters}; use actix_identity::Identity; +use actix_multipart::form::{tempfile::TempFile, MultipartForm}; use actix_web::http::header::{CONTENT_LENGTH, CONTENT_TYPE}; use actix_web::web::{Data, Form, Path}; use actix_web::HttpMessage; @@ -56,7 +57,12 @@ struct SignInForm { page: Page, } -#[derive(Debug, Template, Deserialize, Clone)] +#[derive(Debug, MultipartForm)] +struct ImageUpload { + image: Option, +} + +#[derive(Debug, Template, Deserialize, Clone, PartialEq)] #[template(path = "recipies/create_form.jinja", ext = "html")] struct RecipeForm { title: String, @@ -64,10 +70,14 @@ struct RecipeForm { ingeredients: String, steps: String, tags: String, + #[serde(default)] + selected_tags: Vec, image_url: String, time: Option, author: Option, error: Option, + #[serde(default)] + known_tags: Vec, session: Option, page: Page, } @@ -304,17 +314,20 @@ async fn show( } #[get("/create")] -async fn recipe_form(admin: Identity) -> RecipeForm { +async fn recipe_form(admin: Identity, db: Data) -> RecipeForm { + let tags = Tags::find().all(&**db).await.unwrap_or_default(); RecipeForm { title: "".into(), summary: "".into(), ingeredients: "".into(), steps: "".into(), tags: "".into(), + selected_tags: Vec::new(), image_url: "".into(), author: None, time: None, error: None, + known_tags: tags, session: admin.id().ok(), page: Page::Index, } @@ -338,6 +351,7 @@ async fn create_recipe( Err(e) => { failure.error = Some(e.to_string()); let _ = t.rollback().await; + failure.known_tags = Tags::find().all(&**db).await.unwrap_or_default(); Ok(failure.to_response()) } Ok(_) => { diff --git a/cooked/templates/recipies/create_form.jinja b/cooked/templates/recipies/create_form.jinja index e11d97d..de8d3e6 100644 --- a/cooked/templates/recipies/create_form.jinja +++ b/cooked/templates/recipies/create_form.jinja @@ -14,6 +14,11 @@
+ +