Form
This commit is contained in:
parent
80d3a4d4b2
commit
9d5dc91a22
55
Cargo.lock
generated
55
Cargo.lock
generated
@ -134,6 +134,44 @@ dependencies = [
|
|||||||
"syn 2.0.82",
|
"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]]
|
[[package]]
|
||||||
name = "actix-router"
|
name = "actix-router"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@ -1126,6 +1164,7 @@ dependencies = [
|
|||||||
"actix",
|
"actix",
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-identity",
|
"actix-identity",
|
||||||
|
"actix-multipart",
|
||||||
"actix-session",
|
"actix-session",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"askama",
|
"askama",
|
||||||
@ -1350,6 +1389,7 @@ dependencies = [
|
|||||||
"ident_case",
|
"ident_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
"strsim",
|
||||||
"syn 2.0.82",
|
"syn 2.0.82",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2915,6 +2955,12 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@ -4134,6 +4180,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_plain"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -30,6 +30,7 @@ tantivy = "0.22.0"
|
|||||||
tempfile = "3.13.0"
|
tempfile = "3.13.0"
|
||||||
pulldown-cmark = "0.12.2"
|
pulldown-cmark = "0.12.2"
|
||||||
thiserror = "1.0.65"
|
thiserror = "1.0.65"
|
||||||
|
actix-multipart = "0.7.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|
||||||
|
@ -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{
|
.label{
|
||||||
display: flex;
|
display: flex;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
@ -1671,6 +1725,42 @@ details.collapse summary::-webkit-details-marker{
|
|||||||
gap: 1rem;
|
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{
|
.input input{
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));
|
background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));
|
||||||
@ -2299,6 +2389,10 @@ details.collapse summary::-webkit-details-marker{
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-w-xs{
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
.shrink-0{
|
.shrink-0{
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@ -2435,6 +2529,10 @@ details.collapse summary::-webkit-details-marker{
|
|||||||
padding: 3rem;
|
padding: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-2{
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.p-4{
|
.p-4{
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
@ -2906,6 +3004,18 @@ code{
|
|||||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
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{
|
.marker\:text-rose-900 *::marker{
|
||||||
color: rgb(136 19 55 );
|
color: rgb(136 19 55 );
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ use crate::actors::search::{Find, Search};
|
|||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
use crate::{entities, entities::prelude::*, filters};
|
use crate::{entities, entities::prelude::*, filters};
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
|
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
|
||||||
use actix_web::http::header::{CONTENT_LENGTH, CONTENT_TYPE};
|
use actix_web::http::header::{CONTENT_LENGTH, CONTENT_TYPE};
|
||||||
use actix_web::web::{Data, Form, Path};
|
use actix_web::web::{Data, Form, Path};
|
||||||
use actix_web::HttpMessage;
|
use actix_web::HttpMessage;
|
||||||
@ -56,7 +57,12 @@ struct SignInForm {
|
|||||||
page: Page,
|
page: Page,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Template, Deserialize, Clone)]
|
#[derive(Debug, MultipartForm)]
|
||||||
|
struct ImageUpload {
|
||||||
|
image: Option<TempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Template, Deserialize, Clone, PartialEq)]
|
||||||
#[template(path = "recipies/create_form.jinja", ext = "html")]
|
#[template(path = "recipies/create_form.jinja", ext = "html")]
|
||||||
struct RecipeForm {
|
struct RecipeForm {
|
||||||
title: String,
|
title: String,
|
||||||
@ -64,10 +70,14 @@ struct RecipeForm {
|
|||||||
ingeredients: String,
|
ingeredients: String,
|
||||||
steps: String,
|
steps: String,
|
||||||
tags: String,
|
tags: String,
|
||||||
|
#[serde(default)]
|
||||||
|
selected_tags: Vec<i32>,
|
||||||
image_url: String,
|
image_url: String,
|
||||||
time: Option<String>,
|
time: Option<String>,
|
||||||
author: Option<String>,
|
author: Option<String>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
known_tags: Vec<entities::tags::Model>,
|
||||||
session: Option<User>,
|
session: Option<User>,
|
||||||
page: Page,
|
page: Page,
|
||||||
}
|
}
|
||||||
@ -304,17 +314,20 @@ async fn show(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/create")]
|
#[get("/create")]
|
||||||
async fn recipe_form(admin: Identity) -> RecipeForm {
|
async fn recipe_form(admin: Identity, db: Data<DatabaseConnection>) -> RecipeForm {
|
||||||
|
let tags = Tags::find().all(&**db).await.unwrap_or_default();
|
||||||
RecipeForm {
|
RecipeForm {
|
||||||
title: "".into(),
|
title: "".into(),
|
||||||
summary: "".into(),
|
summary: "".into(),
|
||||||
ingeredients: "".into(),
|
ingeredients: "".into(),
|
||||||
steps: "".into(),
|
steps: "".into(),
|
||||||
tags: "".into(),
|
tags: "".into(),
|
||||||
|
selected_tags: Vec::new(),
|
||||||
image_url: "".into(),
|
image_url: "".into(),
|
||||||
author: None,
|
author: None,
|
||||||
time: None,
|
time: None,
|
||||||
error: None,
|
error: None,
|
||||||
|
known_tags: tags,
|
||||||
session: admin.id().ok(),
|
session: admin.id().ok(),
|
||||||
page: Page::Index,
|
page: Page::Index,
|
||||||
}
|
}
|
||||||
@ -338,6 +351,7 @@ async fn create_recipe(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
failure.error = Some(e.to_string());
|
failure.error = Some(e.to_string());
|
||||||
let _ = t.rollback().await;
|
let _ = t.rollback().await;
|
||||||
|
failure.known_tags = Tags::find().all(&**db).await.unwrap_or_default();
|
||||||
Ok(failure.to_response())
|
Ok(failure.to_response())
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
@ -14,6 +14,11 @@
|
|||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-8">
|
||||||
<div class="flex flex-wrap gap-8 justify-center">
|
<div class="flex flex-wrap gap-8 justify-center">
|
||||||
<form action="/create" method="post" class="flex flex-col gap-4 md:w-1/2 md:mb-10">
|
<form action="/create" method="post" class="flex flex-col gap-4 md:w-1/2 md:mb-10">
|
||||||
|
<label for="image" class="flex gap-4">
|
||||||
|
<input type="file" class="file-input w-full max-w-xs" name="image" id="image" onchange="{ const f = new FormData(); f.append('image', this.files[0]); fetch('/upload', { body: f, method: 'post' }).then(res = res.json()).then(res => document.body.querySelector('#image_url').res.image_url); }" />
|
||||||
|
<input name="image_url" id='image_url' class="hidden" />
|
||||||
|
</label>
|
||||||
|
|
||||||
<label for="title" class="flex gap-4">
|
<label for="title" class="flex gap-4">
|
||||||
<span class="w-24 shrink-0">Title:</span>
|
<span class="w-24 shrink-0">Title:</span>
|
||||||
<input id="title" name="title" class="input border border-solid border-gray-200 rounded-lg" type="text" value="{{title}}" />
|
<input id="title" name="title" class="input border border-solid border-gray-200 rounded-lg" type="text" value="{{title}}" />
|
||||||
@ -66,11 +71,16 @@
|
|||||||
|
|
||||||
<label for="tags" class="flex gap-4">
|
<label for="tags" class="flex gap-4">
|
||||||
<span class="w-24 shrink-0">Tags:</span>
|
<span class="w-24 shrink-0">Tags:</span>
|
||||||
<div class="flex gap-2 w-full">
|
<input id="tags" name="tags" class="textarea-with-view" value="{{tags}}" />
|
||||||
<textarea id="tags" name="tags" class="textarea-with-view">{{tags}}</textarea>
|
|
||||||
<div class="content w-1/2"> </div>
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
|
<div class="w-full flex gap-4 flex-wrap">
|
||||||
|
{% for tag in known_tags %}
|
||||||
|
<label class="border rounded-lg p-2 styled-checkbox cursor-pointer">
|
||||||
|
<span>{{ tag.name }}</span>
|
||||||
|
<input type="checkbox" name="selected_tags[{{loop.index0}}]" value="{{tag.id}}" class="hidden" />
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="submit" class="btn w-full" value="Save" />
|
<input type="submit" class="btn w-full" value="Save" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,3 +58,11 @@ code {
|
|||||||
.textarea-without-view {
|
.textarea-without-view {
|
||||||
@apply min-h-40 input border border-solid border-gray-200 rounded-lg w-full;
|
@apply min-h-40 input border border-solid border-gray-200 rounded-lg w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.styled-checkbox:has(input:checked) {
|
||||||
|
@apply border-green-700;
|
||||||
|
@apply text-green-700;
|
||||||
|
}
|
||||||
|
.styled-checkbox:has(input:not(:checked)) {
|
||||||
|
@apply border-gray-400;
|
||||||
|
}
|
||||||
|
@ -19,7 +19,7 @@ impl MigrationTrait for Migration {
|
|||||||
.auto_increment()
|
.auto_increment()
|
||||||
.primary_key(),
|
.primary_key(),
|
||||||
)
|
)
|
||||||
.col(ColumnDef::new(Tag::Name).string().not_null())
|
.col(ColumnDef::new(Tag::Name).string().not_null().unique_key())
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -36,7 +36,12 @@ impl MigrationTrait for Migration {
|
|||||||
.auto_increment()
|
.auto_increment()
|
||||||
.primary_key(),
|
.primary_key(),
|
||||||
)
|
)
|
||||||
.col(ColumnDef::new(Ingredient::Name).string().not_null())
|
.col(
|
||||||
|
ColumnDef::new(Ingredient::Name)
|
||||||
|
.string()
|
||||||
|
.not_null()
|
||||||
|
.unique_key(),
|
||||||
|
)
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -21,12 +21,12 @@ async fn main() {
|
|||||||
|
|
||||||
let mut t = db.begin().await.unwrap();
|
let mut t = db.begin().await.unwrap();
|
||||||
|
|
||||||
let mut tags = Vec::with_capacity(30);
|
let mut tags = Vec::with_capacity(15);
|
||||||
for _ in 0..30 {
|
for name in TAGS {
|
||||||
use entities::tags::ActiveModel;
|
use entities::tags::ActiveModel;
|
||||||
let r = Tags::insert(ActiveModel {
|
let r = Tags::insert(ActiveModel {
|
||||||
id: NotSet,
|
id: NotSet,
|
||||||
name: Set(fake::faker::name::en::FirstName().fake()),
|
name: Set(name.to_string()),
|
||||||
})
|
})
|
||||||
.exec_with_returning(&mut t)
|
.exec_with_returning(&mut t)
|
||||||
.await;
|
.await;
|
||||||
@ -327,3 +327,20 @@ const DISHES: [&'static str; 96] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const UNIT: [&'static str; 6] = ["kg", "g", "łyżka stołowa", "łyżeczka", "szt", "szklanki"];
|
const UNIT: [&'static str; 6] = ["kg", "g", "łyżka stołowa", "łyżeczka", "szt", "szklanki"];
|
||||||
|
|
||||||
|
const TAGS: [&'static str; 14] = [
|
||||||
|
"Main Dishes",
|
||||||
|
"Soups",
|
||||||
|
"Pasta",
|
||||||
|
"Sweet",
|
||||||
|
"Breads",
|
||||||
|
"Dips",
|
||||||
|
"Baking",
|
||||||
|
"Asian",
|
||||||
|
"Beef Recipes",
|
||||||
|
"Chicken",
|
||||||
|
"Lamb Recipes",
|
||||||
|
"Breakfast",
|
||||||
|
"Muffin Recipes",
|
||||||
|
"Egg Recipes",
|
||||||
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user