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",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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 );
|
||||
}
|
||||
|
@ -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<TempFile>,
|
||||
}
|
||||
|
||||
#[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<i32>,
|
||||
image_url: String,
|
||||
time: Option<String>,
|
||||
author: Option<String>,
|
||||
error: Option<String>,
|
||||
#[serde(default)]
|
||||
known_tags: Vec<entities::tags::Model>,
|
||||
session: Option<User>,
|
||||
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<DatabaseConnection>) -> 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(_) => {
|
||||
|
@ -14,6 +14,11 @@
|
||||
<div class="flex flex-col gap-8">
|
||||
<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">
|
||||
<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">
|
||||
<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}}" />
|
||||
@ -66,11 +71,16 @@
|
||||
|
||||
<label for="tags" class="flex gap-4">
|
||||
<span class="w-24 shrink-0">Tags:</span>
|
||||
<div class="flex gap-2 w-full">
|
||||
<textarea id="tags" name="tags" class="textarea-with-view">{{tags}}</textarea>
|
||||
<div class="content w-1/2"> </div>
|
||||
</div>
|
||||
<input id="tags" name="tags" class="textarea-with-view" value="{{tags}}" />
|
||||
</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>
|
||||
<input type="submit" class="btn w-full" value="Save" />
|
||||
</div>
|
||||
|
@ -58,3 +58,11 @@ code {
|
||||
.textarea-without-view {
|
||||
@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()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Tag::Name).string().not_null())
|
||||
.col(ColumnDef::new(Tag::Name).string().not_null().unique_key())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
@ -36,7 +36,12 @@ impl MigrationTrait for Migration {
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Ingredient::Name).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(Ingredient::Name)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
@ -21,12 +21,12 @@ async fn main() {
|
||||
|
||||
let mut t = db.begin().await.unwrap();
|
||||
|
||||
let mut tags = Vec::with_capacity(30);
|
||||
for _ in 0..30 {
|
||||
let mut tags = Vec::with_capacity(15);
|
||||
for name in TAGS {
|
||||
use entities::tags::ActiveModel;
|
||||
let r = Tags::insert(ActiveModel {
|
||||
id: NotSet,
|
||||
name: Set(fake::faker::name::en::FirstName().fake()),
|
||||
name: Set(name.to_string()),
|
||||
})
|
||||
.exec_with_returning(&mut t)
|
||||
.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 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