New layout & new seed
This commit is contained in:
parent
b4e307496e
commit
fead4e970a
47
Cargo.lock
generated
47
Cargo.lock
generated
@ -87,6 +87,7 @@ dependencies = [
|
|||||||
"bytestring",
|
"bytestring",
|
||||||
"derive_more 0.99.18",
|
"derive_more 0.99.18",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
"flate2",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
@ -104,6 +105,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1039,7 +1041,7 @@ version = "0.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "482aa5695bca086022be453c700a40c02893f1ba7098a2c88351de55341ae894"
|
checksum = "482aa5695bca086022be453c700a40c02893f1ba7098a2c88351de55341ae894"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"entities",
|
"entities 1.0.1",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
@ -1130,6 +1132,7 @@ dependencies = [
|
|||||||
"askama_actix",
|
"askama_actix",
|
||||||
"chrono",
|
"chrono",
|
||||||
"derive_more 1.0.0",
|
"derive_more 1.0.0",
|
||||||
|
"entities 0.1.0",
|
||||||
"humantime",
|
"humantime",
|
||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
"migration",
|
"migration",
|
||||||
@ -1519,6 +1522,14 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "entities"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"sea-orm",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "entities"
|
name = "entities"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -1591,6 +1602,16 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fake"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38bdb1a5a77008bd1208d3fccd90611d7bee8f6d3296175f69fb056747a220fb"
|
||||||
|
dependencies = [
|
||||||
|
"deunicode",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastdivide"
|
name = "fastdivide"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@ -1624,6 +1645,16 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.0.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@ -4026,6 +4057,20 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "seed"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"actix-web",
|
||||||
|
"entities 0.1.0",
|
||||||
|
"fake",
|
||||||
|
"migration",
|
||||||
|
"rand",
|
||||||
|
"sea-orm",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
|
38
Cargo.toml
38
Cargo.toml
@ -1,36 +1,2 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "cooked"
|
members = ["cooked", "seed", "entities"]
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
actix = "0.13.5"
|
|
||||||
actix-files = { version = "0.6.6", features = ["experimental-io-uring"] }
|
|
||||||
actix-web = { version = "4.9.0", features = ["compress-brotli", "cookies", "experimental-io-uring", "macros", "rustls", "secure-cookies", "unicode"], default-features = false }
|
|
||||||
askama = { version = "0.12.1", features = ["with-actix-web", "serde_json", "mime_guess", "markdown", "comrak", "mime"] }
|
|
||||||
askama_actix = "0.14.0"
|
|
||||||
redis = { version = "0.27.5", features = ["tokio", "json", "uuid", "tokio-comp", "connection-manager"] }
|
|
||||||
sea-orm = { version = "1.1.0", default-features = false, features = ["chrono", "macros", "runtime-actix-rustls", "serde_json", "sqlx-postgres", "with-uuid"] }
|
|
||||||
tracing = "0.1.40"
|
|
||||||
tracing-subscriber = "0.3.18"
|
|
||||||
serde = "1.0.210"
|
|
||||||
serde_json = "1.0.132"
|
|
||||||
uuid = { version = "1.11.0", features = ["serde", "v4", "v8"] }
|
|
||||||
migration = { path = "./migration" }
|
|
||||||
rswind = "0.0.1-alpha.1"
|
|
||||||
rswind_cli = "0.0.1-alpha.1"
|
|
||||||
derive_more = { version = "1.0.0", features = ["deref"] }
|
|
||||||
chrono = "0.4.38"
|
|
||||||
humantime = "2.1.0"
|
|
||||||
humantime-serde = "1.1.1"
|
|
||||||
actix-session = { version = "0.10.1", features = ["redis-session-rustls"] }
|
|
||||||
actix-identity = "0.8.0"
|
|
||||||
tantivy = "0.22.0"
|
|
||||||
tempfile = "3.13.0"
|
|
||||||
pulldown-cmark = "0.12.2"
|
|
||||||
thiserror = "1.0.65"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tracing-test = "0.2.5"
|
|
||||||
|
37
cooked/Cargo.toml
Normal file
37
cooked/Cargo.toml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
[package]
|
||||||
|
name = "cooked"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix = "0.13.5"
|
||||||
|
actix-files = { version = "0.6.6", features = ["experimental-io-uring"] }
|
||||||
|
actix-web = { version = "4.9.0", features = ["compress-brotli", "cookies", "experimental-io-uring", "macros", "rustls", "secure-cookies", "unicode"], default-features = false }
|
||||||
|
askama = { version = "0.12.1", features = ["with-actix-web", "serde_json", "mime_guess", "markdown", "comrak", "mime"] }
|
||||||
|
askama_actix = "0.14.0"
|
||||||
|
redis = { version = "0.27.5", features = ["tokio", "json", "uuid", "tokio-comp", "connection-manager"] }
|
||||||
|
sea-orm = { version = "1.1.0", default-features = false, features = ["chrono", "macros", "runtime-actix-rustls", "serde_json", "sqlx-postgres", "with-uuid"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = "0.3.18"
|
||||||
|
serde = "1.0.210"
|
||||||
|
serde_json = "1.0.132"
|
||||||
|
uuid = { version = "1.11.0", features = ["serde", "v4", "v8"] }
|
||||||
|
migration = { path = "../migration" }
|
||||||
|
entities = { path = "../entities" }
|
||||||
|
rswind = "0.0.1-alpha.1"
|
||||||
|
rswind_cli = "0.0.1-alpha.1"
|
||||||
|
derive_more = { version = "1.0.0", features = ["deref"] }
|
||||||
|
chrono = "0.4.38"
|
||||||
|
humantime = "2.1.0"
|
||||||
|
humantime-serde = "1.1.1"
|
||||||
|
actix-session = { version = "0.10.1", features = ["redis-session-rustls"] }
|
||||||
|
actix-identity = "0.8.0"
|
||||||
|
tantivy = "0.22.0"
|
||||||
|
tempfile = "3.13.0"
|
||||||
|
pulldown-cmark = "0.12.2"
|
||||||
|
thiserror = "1.0.65"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tracing-test = "0.2.5"
|
@ -11,7 +11,7 @@ use std::str::FromStr;
|
|||||||
use types::Admins;
|
use types::Admins;
|
||||||
|
|
||||||
pub mod actors;
|
pub mod actors;
|
||||||
pub mod entities;
|
pub use entities;
|
||||||
pub mod filters;
|
pub mod filters;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod types;
|
pub mod types;
|
@ -1,6 +1,6 @@
|
|||||||
use crate::actors::search::{Find, Search};
|
use crate::actors::search::{Find, Search};
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
use crate::{entities, filters};
|
use crate::{entities, entities::prelude::*, filters};
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
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};
|
||||||
@ -8,8 +8,9 @@ use actix_web::HttpMessage;
|
|||||||
use actix_web::{get, post, HttpRequest, HttpResponse, Responder};
|
use actix_web::{get, post, HttpRequest, HttpResponse, Responder};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_actix::TemplateToResponse;
|
use askama_actix::TemplateToResponse;
|
||||||
use sea_orm::{prelude::*, QuerySelect};
|
use sea_orm::{prelude::*, DatabaseTransaction, QuerySelect, TransactionTrait};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
#[derive(Debug, Template, derive_more::Deref)]
|
#[derive(Debug, Template, derive_more::Deref)]
|
||||||
#[template(path = "recipe_card.jinja")]
|
#[template(path = "recipe_card.jinja")]
|
||||||
@ -122,13 +123,6 @@ async fn sign_in(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
struct CreateRecipe {
|
|
||||||
// #[serde(default)]
|
|
||||||
// #[serde(with = "humantime_serde")]
|
|
||||||
// time: Option<chrono::Duration>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
struct SignIn {
|
struct SignIn {
|
||||||
email: String,
|
email: String,
|
||||||
@ -229,9 +223,9 @@ async fn index_html(
|
|||||||
#[template(path = "recipies/show.jinja", ext = "html")]
|
#[template(path = "recipies/show.jinja", ext = "html")]
|
||||||
struct RecipeDetailTemplate {
|
struct RecipeDetailTemplate {
|
||||||
recipe: entities::recipies::Model,
|
recipe: entities::recipies::Model,
|
||||||
tags: Vec<entities::recipe_tags::Model>,
|
tags: Vec<entities::tags::Model>,
|
||||||
steps: Vec<entities::recipe_steps::Model>,
|
steps: Vec<entities::recipe_steps::Model>,
|
||||||
ingeredients: Vec<entities::recipe_ingeredients::Model>,
|
ingredients: Vec<Ingredient>,
|
||||||
session: Option<User>,
|
session: Option<User>,
|
||||||
page: Page,
|
page: Page,
|
||||||
}
|
}
|
||||||
@ -253,7 +247,8 @@ async fn show(
|
|||||||
.append_header(("location", "/"))
|
.append_header(("location", "/"))
|
||||||
.finish();
|
.finish();
|
||||||
};
|
};
|
||||||
let tags = entities::prelude::RecipeTags::find()
|
let tags = entities::prelude::Tags::find()
|
||||||
|
.left_join(entities::prelude::RecipeTags)
|
||||||
.filter(entities::recipe_tags::Column::RecipeId.eq(id))
|
.filter(entities::recipe_tags::Column::RecipeId.eq(id))
|
||||||
.all(db)
|
.all(db)
|
||||||
.await
|
.await
|
||||||
@ -263,18 +258,43 @@ async fn show(
|
|||||||
.all(db)
|
.all(db)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let ingeredients = entities::prelude::RecipeIngeredients::find()
|
let recipe_ingredients = entities::prelude::RecipeIngredients::find()
|
||||||
.filter(entities::recipe_ingeredients::Column::RecipeId.eq(id))
|
.filter(entities::recipe_ingredients::Column::RecipeId.eq(recipe.id))
|
||||||
.all(db)
|
.all(db)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let ingredients = entities::prelude::Ingredients::find()
|
||||||
|
.filter(
|
||||||
|
entities::ingredients::Column::Id.is_in(
|
||||||
|
recipe_ingredients
|
||||||
|
.iter()
|
||||||
|
.map(|i| i.ingredient_id)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.all(db)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|ingredient| (ingredient.id, ingredient.name))
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
let ingredients = recipe_ingredients
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|ri| {
|
||||||
|
Some(Ingredient {
|
||||||
|
name: ingredients.get(&ri.ingredient_id)?.to_owned(),
|
||||||
|
qty: ri.qty,
|
||||||
|
unit: ri.unit,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
HttpResponse::Ok().body(
|
HttpResponse::Ok().body(
|
||||||
RecipeDetailTemplate {
|
RecipeDetailTemplate {
|
||||||
steps,
|
steps,
|
||||||
tags,
|
tags,
|
||||||
recipe,
|
recipe,
|
||||||
ingeredients,
|
ingredients,
|
||||||
session: admin.and_then(|s| s.id().ok()),
|
session: admin.and_then(|s| s.id().ok()),
|
||||||
page: Page::Recipe,
|
page: Page::Recipe,
|
||||||
}
|
}
|
||||||
@ -305,22 +325,31 @@ async fn create_recipe(
|
|||||||
_admin: Identity,
|
_admin: Identity,
|
||||||
form: Form<RecipeForm>,
|
form: Form<RecipeForm>,
|
||||||
db: Data<DatabaseConnection>,
|
db: Data<DatabaseConnection>,
|
||||||
) -> HttpResponse {
|
) -> Result<HttpResponse, Error> {
|
||||||
let form = form.into_inner();
|
let form = form.into_inner();
|
||||||
let mut failure = form.clone();
|
let mut failure = form.clone();
|
||||||
|
|
||||||
match save_recipe(form, db.into_inner()).await {
|
let mut t = db
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| tracing::error!("Create recipe transaction: {e}"))
|
||||||
|
.map_err(|_| Error::DatabaseError)?;
|
||||||
|
match save_recipe(form, &mut t).await {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
failure.error = Some(e.to_string());
|
failure.error = Some(e.to_string());
|
||||||
failure.to_response()
|
let _ = t.rollback().await;
|
||||||
|
Ok(failure.to_response())
|
||||||
}
|
}
|
||||||
Ok(_) => HttpResponse::TemporaryRedirect()
|
Ok(_) => {
|
||||||
|
let _ = t.commit().await;
|
||||||
|
Ok(HttpResponse::TemporaryRedirect()
|
||||||
.append_header(("location", "/"))
|
.append_header(("location", "/"))
|
||||||
.finish(),
|
.finish())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_recipe(form: RecipeForm, db: DatabaseConnection) -> Result<(), Error> {
|
async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result<(), Error> {
|
||||||
use crate::entities::recipies::ActiveModel as RAM;
|
use crate::entities::recipies::ActiveModel as RAM;
|
||||||
use sea_orm::ActiveValue::*;
|
use sea_orm::ActiveValue::*;
|
||||||
|
|
||||||
@ -328,17 +357,107 @@ async fn save_recipe(form: RecipeForm, db: DatabaseConnection) -> Result<(), Err
|
|||||||
let parser = pulldown_cmark::Parser::new(&form.summary);
|
let parser = pulldown_cmark::Parser::new(&form.summary);
|
||||||
pulldown_cmark::html::push_html(&mut summary, parser);
|
pulldown_cmark::html::push_html(&mut summary, parser);
|
||||||
|
|
||||||
let model = RAM {
|
let time = match form.time {
|
||||||
title: Set(form.title),
|
Some(s) => Some(
|
||||||
image_url: Set(form.image_url),
|
humantime::parse_duration(&s)
|
||||||
author: Set(None),
|
.inspect_err(|e| tracing::warn!("Invalid duration format: {e}"))
|
||||||
time: Set(None),
|
.map_err(|_| Error::InvalidTime)?
|
||||||
summary: Set(Some(summary)),
|
.as_secs() as i32,
|
||||||
..Default::default()
|
),
|
||||||
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let steps = crate::utils::parse_steps(&form.steps, 0)?;
|
let recipe_id = Recipies::insert(RAM {
|
||||||
let ingeredients = crate::utils::parse_steps(&form.ingeredients, 0)?;
|
title: Set(form.title),
|
||||||
|
image_url: Set(form.image_url),
|
||||||
|
author: Set(form.author),
|
||||||
|
time: Set(time),
|
||||||
|
summary: Set(Some(summary)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&mut *t)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| tracing::error!("Failed to save Recipe: {e}"))
|
||||||
|
.map_err(|_| Error::SaveRecipe)?
|
||||||
|
.last_insert_id;
|
||||||
|
|
||||||
|
let steps = crate::utils::parse_steps(&form.steps)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(body, hint)| entities::recipe_steps::ActiveModel {
|
||||||
|
body: Set(body),
|
||||||
|
hint: Set(hint),
|
||||||
|
recipe_id: Set(recipe_id),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let _steps = RecipeSteps::insert_many(steps)
|
||||||
|
.exec(&mut *t)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| tracing::error!("Save steps: {e}"))
|
||||||
|
.map_err(|_| Error::SaveRecipeStep)?;
|
||||||
|
let ingredients = crate::utils::parse_ingenedients(&form.ingeredients)?;
|
||||||
|
let known = Ingredients::find()
|
||||||
|
.filter(
|
||||||
|
entities::ingredients::Column::Name.is_in(
|
||||||
|
ingredients
|
||||||
|
.iter()
|
||||||
|
.map(|(name, ..)| name.to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.all(&mut *t)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| tracing::warn!("Failed to find ingredients: {e}"))
|
||||||
|
.map_err(|_| Error::SaveRecipeIngeredient)?;
|
||||||
|
let missing = {
|
||||||
|
let known = known
|
||||||
|
.iter()
|
||||||
|
.map(|model| &model.name)
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
let missing = ingredients
|
||||||
|
.iter()
|
||||||
|
.filter(|(name, ..)| !known.contains(name))
|
||||||
|
.map(|(name, ..)| entities::ingredients::ActiveModel {
|
||||||
|
name: Set(name.to_owned()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut v = Vec::with_capacity(missing.len());
|
||||||
|
for missing in missing {
|
||||||
|
v.push(
|
||||||
|
Ingredients::insert(missing)
|
||||||
|
.exec_with_returning(&mut *t)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| tracing::error!("Failed to create ingredients: {e}"))
|
||||||
|
.map_err(|_| Error::DatabaseError)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
v
|
||||||
|
};
|
||||||
|
let map = known
|
||||||
|
.into_iter()
|
||||||
|
.chain(missing.into_iter())
|
||||||
|
.map(|row| (row.name, row.id))
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
|
RecipeIngredients::insert_many(
|
||||||
|
ingredients
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(name, unit, qty)| {
|
||||||
|
Some(entities::recipe_ingredients::ActiveModel {
|
||||||
|
ingredient_id: Set(*map.get(&name)?),
|
||||||
|
qty: Set(qty),
|
||||||
|
unit: Set(unit.unwrap_or_default()),
|
||||||
|
recipe_id: Set(recipe_id),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.exec(&mut *t)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| tracing::error!("Save ingeredients: {e}"))
|
||||||
|
.map_err(|_| Error::SaveRecipeIngeredient)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
87
cooked/src/types.rs
Normal file
87
cooked/src/types.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
pub type User = String;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Ingredient {
|
||||||
|
pub qty: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub unit: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Invalid step list")]
|
||||||
|
InvalidStepList,
|
||||||
|
#[error("Invalid ingeredients list")]
|
||||||
|
InvalidIngeredientList,
|
||||||
|
#[error("Time has invalid format")]
|
||||||
|
InvalidTime,
|
||||||
|
#[error("Internal server error")]
|
||||||
|
SaveRecipe,
|
||||||
|
#[error("Internal server error")]
|
||||||
|
SaveRecipeStep,
|
||||||
|
#[error("Internal server error")]
|
||||||
|
SaveRecipeIngeredient,
|
||||||
|
#[error("Internal server error")]
|
||||||
|
SaveRecipeTag,
|
||||||
|
#[error("Internal server error")]
|
||||||
|
DatabaseError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix_web::error::ResponseError for Error {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
Self::InvalidStepList => StatusCode::BAD_REQUEST,
|
||||||
|
Self::InvalidIngeredientList => StatusCode::BAD_REQUEST,
|
||||||
|
Self::InvalidTime => StatusCode::BAD_REQUEST,
|
||||||
|
Self::SaveRecipe => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::SaveRecipeStep => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::SaveRecipeIngeredient => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::SaveRecipeTag => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Self::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
|
||||||
|
pub enum Page {
|
||||||
|
Index,
|
||||||
|
Recipe,
|
||||||
|
Search,
|
||||||
|
SignIn,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Admin {
|
||||||
|
pub email: String,
|
||||||
|
pub pass: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Admin {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut it = s.split(':');
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
email: it.next().expect("Admin login is required").into(),
|
||||||
|
pass: it.next().expect("Admin password is required").into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, derive_more::Deref)]
|
||||||
|
pub struct Admins(Vec<Admin>);
|
||||||
|
|
||||||
|
impl FromStr for Admins {
|
||||||
|
type Err = ();
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self(
|
||||||
|
s.trim().split(',').filter_map(|s| s.parse().ok()).collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,7 @@
|
|||||||
use crate::entities::recipe_ingeredients::ActiveModel as RIAM;
|
|
||||||
use crate::entities::recipe_steps::ActiveModel as RSAM;
|
|
||||||
use crate::types::Error;
|
use crate::types::Error;
|
||||||
use crate::types::Error::*;
|
use crate::types::Error::*;
|
||||||
use sea_orm::prelude::*;
|
|
||||||
use sea_orm::ActiveValue::*;
|
|
||||||
|
|
||||||
pub fn parse_steps(s: &str, recipe_id: i32) -> Result<Vec<RSAM>, Error> {
|
pub fn parse_steps(s: &str) -> Result<Vec<(String, Option<String>)>, Error> {
|
||||||
s.lines()
|
s.lines()
|
||||||
.filter(|s| !s.trim().is_empty())
|
.filter(|s| !s.trim().is_empty())
|
||||||
.try_fold(Vec::new(), |mut v, line| {
|
.try_fold(Vec::new(), |mut v, line| {
|
||||||
@ -13,15 +9,11 @@ pub fn parse_steps(s: &str, recipe_id: i32) -> Result<Vec<RSAM>, Error> {
|
|||||||
|
|
||||||
match line.chars().next() {
|
match line.chars().next() {
|
||||||
Some('*') => {
|
Some('*') => {
|
||||||
v.push(RSAM {
|
v.push((line.replacen("*", "", 1).trim().to_string(), None));
|
||||||
body: Set(line.replacen("*", "", 1).trim().to_string()),
|
|
||||||
recipe_id: Set(recipe_id),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Some('>') => {
|
Some('>') => {
|
||||||
v.last_mut().ok_or(InvalidStepList)?.hint =
|
v.last_mut().ok_or(InvalidStepList)?.1 =
|
||||||
Set(Some(line.replacen(">", "", 1).trim().to_string()));
|
Some(line.replacen(">", "", 1).trim().to_string());
|
||||||
}
|
}
|
||||||
_ => return Err(InvalidStepList),
|
_ => return Err(InvalidStepList),
|
||||||
};
|
};
|
||||||
@ -30,7 +22,7 @@ pub fn parse_steps(s: &str, recipe_id: i32) -> Result<Vec<RSAM>, Error> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_ingenedients(s: &str, recipe_id: i32) -> Result<Vec<RIAM>, Error> {
|
pub fn parse_ingenedients(s: &str) -> Result<Vec<(String, Option<String>, i32)>, Error> {
|
||||||
s.lines()
|
s.lines()
|
||||||
.map(|s| s.trim())
|
.map(|s| s.trim())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
@ -47,12 +39,7 @@ pub fn parse_ingenedients(s: &str, recipe_id: i32) -> Result<Vec<RIAM>, Error> {
|
|||||||
.remove(0)
|
.remove(0)
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| InvalidIngeredientList)?;
|
.map_err(|_| InvalidIngeredientList)?;
|
||||||
Ok(RIAM {
|
Ok((pieces.join(" "), None, qty))
|
||||||
recipe_id: Set(recipe_id),
|
|
||||||
name: Set(pieces.join(" ")),
|
|
||||||
qty: Set(qty),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let qty = pieces
|
let qty = pieces
|
||||||
@ -60,15 +47,13 @@ pub fn parse_ingenedients(s: &str, recipe_id: i32) -> Result<Vec<RIAM>, Error> {
|
|||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| InvalidIngeredientList)?;
|
.map_err(|_| InvalidIngeredientList)?;
|
||||||
let unit = pieces.remove(0);
|
let unit = pieces.remove(0);
|
||||||
Ok(RIAM {
|
Ok((pieces.join(" "), Some(unit.to_string()), qty))
|
||||||
recipe_id: Set(recipe_id),
|
|
||||||
name: Set(pieces.join(" ")),
|
|
||||||
unit: Set(unit.to_string()),
|
|
||||||
qty: Set(qty),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.try_collect()
|
.try_collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_tags(s: &str) -> Result<Vec<String>, Error> {
|
||||||
|
s.lines().map(|line| Ok(line.to_string())).try_collect()
|
||||||
|
}
|
8
entities/Cargo.toml
Normal file
8
entities/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "entities"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sea-orm = { version = "1.1.0", default-features = false, features = ["chrono", "macros", "runtime-actix-rustls", "serde_json", "sqlx-postgres", "with-uuid"] }
|
||||||
|
serde = "1.0.210"
|
26
entities/src/ingredients.rs
Normal file
26
entities/src/ingredients.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "ingredients")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::recipe_ingredients::Entity")]
|
||||||
|
RecipeIngredients,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::recipe_ingredients::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::RecipeIngredients.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
pub mod recipe_ingeredients;
|
pub mod ingredients;
|
||||||
|
pub mod recipe_ingredients;
|
||||||
pub mod recipe_steps;
|
pub mod recipe_steps;
|
||||||
pub mod recipe_tags;
|
pub mod recipe_tags;
|
||||||
pub mod recipies;
|
pub mod recipies;
|
||||||
|
pub mod tags;
|
@ -1,6 +1,8 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
pub use super::recipe_ingeredients::Entity as RecipeIngeredients;
|
pub use super::ingredients::Entity as Ingredients;
|
||||||
|
pub use super::recipe_ingredients::Entity as RecipeIngredients;
|
||||||
pub use super::recipe_steps::Entity as RecipeSteps;
|
pub use super::recipe_steps::Entity as RecipeSteps;
|
||||||
pub use super::recipe_tags::Entity as RecipeTags;
|
pub use super::recipe_tags::Entity as RecipeTags;
|
||||||
pub use super::recipies::Entity as Recipies;
|
pub use super::recipies::Entity as Recipies;
|
||||||
|
pub use super::tags::Entity as Tags;
|
@ -4,11 +4,11 @@ use sea_orm::entity::prelude::*;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "recipe_ingeredients")]
|
#[sea_orm(table_name = "recipe_ingredients")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub ingredient_id: i32,
|
||||||
pub qty: i32,
|
pub qty: i32,
|
||||||
pub unit: String,
|
pub unit: String,
|
||||||
pub recipe_id: i32,
|
pub recipe_id: i32,
|
||||||
@ -16,6 +16,14 @@ pub struct Model {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::ingredients::Entity",
|
||||||
|
from = "Column::IngredientId",
|
||||||
|
to = "super::ingredients::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "NoAction"
|
||||||
|
)]
|
||||||
|
Ingredients,
|
||||||
#[sea_orm(
|
#[sea_orm(
|
||||||
belongs_to = "super::recipies::Entity",
|
belongs_to = "super::recipies::Entity",
|
||||||
from = "Column::RecipeId",
|
from = "Column::RecipeId",
|
||||||
@ -26,6 +34,12 @@ pub enum Relation {
|
|||||||
Recipies,
|
Recipies,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::ingredients::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Ingredients.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Related<super::recipies::Entity> for Entity {
|
impl Related<super::recipies::Entity> for Entity {
|
||||||
fn to() -> RelationDef {
|
fn to() -> RelationDef {
|
||||||
Relation::Recipies.def()
|
Relation::Recipies.def()
|
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub tag_id: i32,
|
||||||
pub recipe_id: i32,
|
pub recipe_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,6 +22,14 @@ pub enum Relation {
|
|||||||
on_delete = "NoAction"
|
on_delete = "NoAction"
|
||||||
)]
|
)]
|
||||||
Recipies,
|
Recipies,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::tags::Entity",
|
||||||
|
from = "Column::TagId",
|
||||||
|
to = "super::tags::Column::Id",
|
||||||
|
on_update = "NoAction",
|
||||||
|
on_delete = "NoAction"
|
||||||
|
)]
|
||||||
|
Tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::recipies::Entity> for Entity {
|
impl Related<super::recipies::Entity> for Entity {
|
||||||
@ -30,4 +38,10 @@ impl Related<super::recipies::Entity> for Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Related<super::tags::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Tags.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
@ -17,17 +17,17 @@ pub struct Model {
|
|||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
#[sea_orm(has_many = "super::recipe_ingeredients::Entity")]
|
#[sea_orm(has_many = "super::recipe_ingredients::Entity")]
|
||||||
RecipeIngeredients,
|
RecipeIngredients,
|
||||||
#[sea_orm(has_many = "super::recipe_steps::Entity")]
|
#[sea_orm(has_many = "super::recipe_steps::Entity")]
|
||||||
RecipeSteps,
|
RecipeSteps,
|
||||||
#[sea_orm(has_many = "super::recipe_tags::Entity")]
|
#[sea_orm(has_many = "super::recipe_tags::Entity")]
|
||||||
RecipeTags,
|
RecipeTags,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::recipe_ingeredients::Entity> for Entity {
|
impl Related<super::recipe_ingredients::Entity> for Entity {
|
||||||
fn to() -> RelationDef {
|
fn to() -> RelationDef {
|
||||||
Relation::RecipeIngeredients.def()
|
Relation::RecipeIngredients.def()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
26
entities/src/tags.rs
Normal file
26
entities/src/tags.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "tags")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key)]
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(has_many = "super::recipe_tags::Entity")]
|
||||||
|
RecipeTags,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::recipe_tags::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::RecipeTags.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
@ -6,6 +6,40 @@ pub struct Migration;
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigrationTrait for Migration {
|
impl MigrationTrait for Migration {
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Tag::Tags)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Tag::Id)
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.unique_key()
|
||||||
|
.auto_increment()
|
||||||
|
.primary_key(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Tag::Name).string().not_null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Ingredient::Ingredients)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Ingredient::Id)
|
||||||
|
.integer()
|
||||||
|
.not_null()
|
||||||
|
.unique_key()
|
||||||
|
.auto_increment()
|
||||||
|
.primary_key(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Ingredient::Name).string().not_null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
Table::create()
|
Table::create()
|
||||||
@ -56,7 +90,7 @@ impl MigrationTrait for Migration {
|
|||||||
manager
|
manager
|
||||||
.create_table(
|
.create_table(
|
||||||
Table::create()
|
Table::create()
|
||||||
.table(RecipeIngredient::RecipeIngeredients)
|
.table(RecipeIngredient::RecipeIngredients)
|
||||||
.if_not_exists()
|
.if_not_exists()
|
||||||
.col(
|
.col(
|
||||||
ColumnDef::new(RecipeIngredient::Id)
|
ColumnDef::new(RecipeIngredient::Id)
|
||||||
@ -66,7 +100,11 @@ impl MigrationTrait for Migration {
|
|||||||
.auto_increment()
|
.auto_increment()
|
||||||
.primary_key(),
|
.primary_key(),
|
||||||
)
|
)
|
||||||
.col(ColumnDef::new(RecipeIngredient::Name).string().not_null())
|
.col(
|
||||||
|
ColumnDef::new(RecipeIngredient::IngredientId)
|
||||||
|
.integer()
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
.col(ColumnDef::new(RecipeIngredient::Qty).integer().not_null())
|
.col(ColumnDef::new(RecipeIngredient::Qty).integer().not_null())
|
||||||
.col(ColumnDef::new(RecipeIngredient::Unit).string().not_null())
|
.col(ColumnDef::new(RecipeIngredient::Unit).string().not_null())
|
||||||
.col(
|
.col(
|
||||||
@ -78,9 +116,16 @@ impl MigrationTrait for Migration {
|
|||||||
&mut ForeignKeyCreateStatement::new()
|
&mut ForeignKeyCreateStatement::new()
|
||||||
.to_tbl(Recipe::Recipies)
|
.to_tbl(Recipe::Recipies)
|
||||||
.to_col(Recipe::Id)
|
.to_col(Recipe::Id)
|
||||||
.from_tbl(RecipeIngredient::RecipeIngeredients)
|
.from_tbl(RecipeIngredient::RecipeIngredients)
|
||||||
.from_col(RecipeIngredient::RecipeId),
|
.from_col(RecipeIngredient::RecipeId),
|
||||||
)
|
)
|
||||||
|
.foreign_key(
|
||||||
|
&mut ForeignKeyCreateStatement::new()
|
||||||
|
.to_tbl(Ingredient::Ingredients)
|
||||||
|
.to_col(Recipe::Id)
|
||||||
|
.from_tbl(RecipeIngredient::RecipeIngredients)
|
||||||
|
.from_col(RecipeIngredient::IngredientId),
|
||||||
|
)
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -97,7 +142,7 @@ impl MigrationTrait for Migration {
|
|||||||
.auto_increment()
|
.auto_increment()
|
||||||
.primary_key(),
|
.primary_key(),
|
||||||
)
|
)
|
||||||
.col(ColumnDef::new(RecipeTag::Name).string().not_null())
|
.col(ColumnDef::new(RecipeTag::TagId).integer().not_null())
|
||||||
.col(ColumnDef::new(RecipeTag::RecipeId).integer().not_null())
|
.col(ColumnDef::new(RecipeTag::RecipeId).integer().not_null())
|
||||||
.foreign_key(
|
.foreign_key(
|
||||||
&mut ForeignKeyCreateStatement::new()
|
&mut ForeignKeyCreateStatement::new()
|
||||||
@ -106,6 +151,13 @@ impl MigrationTrait for Migration {
|
|||||||
.from_tbl(RecipeTag::RecipeTags)
|
.from_tbl(RecipeTag::RecipeTags)
|
||||||
.from_col(RecipeTag::RecipeId),
|
.from_col(RecipeTag::RecipeId),
|
||||||
)
|
)
|
||||||
|
.foreign_key(
|
||||||
|
&mut ForeignKeyCreateStatement::new()
|
||||||
|
.to_tbl(Tag::Tags)
|
||||||
|
.to_col(Tag::Id)
|
||||||
|
.from_tbl(RecipeTag::RecipeTags)
|
||||||
|
.from_col(RecipeTag::TagId),
|
||||||
|
)
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -116,7 +168,7 @@ impl MigrationTrait for Migration {
|
|||||||
manager
|
manager
|
||||||
.drop_table(
|
.drop_table(
|
||||||
Table::drop()
|
Table::drop()
|
||||||
.table(RecipeIngredient::RecipeIngeredients)
|
.table(RecipeIngredient::RecipeIngredients)
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -129,6 +181,12 @@ impl MigrationTrait for Migration {
|
|||||||
manager
|
manager
|
||||||
.drop_table(Table::drop().table(Recipe::Recipies).to_owned())
|
.drop_table(Table::drop().table(Recipe::Recipies).to_owned())
|
||||||
.await?;
|
.await?;
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Tag::Tags).to_owned())
|
||||||
|
.await?;
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Ingredient::Ingredients).to_owned())
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,19 +212,33 @@ enum RecipeStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(DeriveIden)]
|
#[derive(DeriveIden)]
|
||||||
enum RecipeIngredient {
|
enum Ingredient {
|
||||||
RecipeIngeredients,
|
Ingredients,
|
||||||
Id,
|
Id,
|
||||||
Name,
|
Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum RecipeIngredient {
|
||||||
|
RecipeIngredients,
|
||||||
|
Id,
|
||||||
|
IngredientId,
|
||||||
Qty,
|
Qty,
|
||||||
Unit,
|
Unit,
|
||||||
RecipeId,
|
RecipeId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum Tag {
|
||||||
|
Tags,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(DeriveIden)]
|
#[derive(DeriveIden)]
|
||||||
enum RecipeTag {
|
enum RecipeTag {
|
||||||
RecipeTags,
|
RecipeTags,
|
||||||
Id,
|
Id,
|
||||||
Name,
|
TagId,
|
||||||
RecipeId,
|
RecipeId,
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
mkdir -p src/entities
|
mkdir -p src/entities
|
||||||
sea-orm-cli generate -v entity --database-url postgres://postgres@localhost/cooked --output-dir ./src/entities --with-serde=both
|
sea-orm-cli generate -v entity --lib --database-url postgres://postgres@localhost/cooked --output-dir ./entities/src --with-serde=both
|
||||||
|
7
scripts/migrate
Executable file
7
scripts/migrate
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
export DATABASE_URL=postgres://postgres@localhost/cooked
|
||||||
|
|
||||||
|
cargo build -p migration
|
||||||
|
|
||||||
|
./target/debug/migration
|
||||||
|
|
||||||
|
./scripts/seed.sh
|
@ -1,4 +1,4 @@
|
|||||||
psql cooked postgres -h localhost < ./seed/recipies.sql
|
psql -b cooked postgres -h localhost < ./seed/recipies.sql &&
|
||||||
psql cooked postgres -h localhost < ./seed/recipe_tags.sql
|
psql -b cooked postgres -h localhost < ./seed/recipe_tags.sql &&
|
||||||
psql cooked postgres -h localhost < ./seed/recipe_ingeredients.sql
|
psql -b cooked postgres -h localhost < ./seed/recipe_ingeredients.sql &&
|
||||||
psql cooked postgres -h localhost < ./seed/recipe_steps.sql
|
psql -b cooked postgres -h localhost < ./seed/recipe_steps.sql
|
||||||
|
2733
seed/Cargo.lock
generated
Normal file
2733
seed/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
seed/Cargo.toml
Normal file
15
seed/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "seed"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = "*"
|
||||||
|
sea-orm = { version = "1.1.0", default-features = false, features = ["chrono", "macros", "runtime-actix-rustls", "serde_json", "sqlx-postgres", "with-uuid"] }
|
||||||
|
serde = "1.0.210"
|
||||||
|
actix-web = "*"
|
||||||
|
|
||||||
|
entities = { path = "../entities" }
|
||||||
|
migration = { path = "../migration" }
|
||||||
|
fake = "3.0.0"
|
||||||
|
rand = "0.8.5"
|
265
seed/src/main.rs
Normal file
265
seed/src/main.rs
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
use entities::prelude::*;
|
||||||
|
use fake::Fake;
|
||||||
|
use migration::*;
|
||||||
|
use rand::Rng;
|
||||||
|
use sea_orm::Set;
|
||||||
|
use sea_orm::*;
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
let psql =
|
||||||
|
std::env::var("PSQL").expect("PSQL is required. Please provide postgresql connection url");
|
||||||
|
let db = Database::connect(&psql)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to postgresql");
|
||||||
|
{
|
||||||
|
use migration::*;
|
||||||
|
|
||||||
|
Migrator::up(&db, None).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tags = Vec::with_capacity(30);
|
||||||
|
for _ in 0..30 {
|
||||||
|
use entities::tags::ActiveModel;
|
||||||
|
tags.push(
|
||||||
|
Tags::insert(ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
name: Set(fake::faker::name::en::FirstName().fake()),
|
||||||
|
})
|
||||||
|
.exec_with_returning(&db)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut ingredients = Vec::with_capacity(INGREDIENTS.len());
|
||||||
|
for name in INGREDIENTS {
|
||||||
|
use entities::tags::ActiveModel;
|
||||||
|
tags.push(
|
||||||
|
Tags::insert(ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
name: Set(name.to_string()),
|
||||||
|
})
|
||||||
|
.exec_with_returning(&db)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut recipies = Vec::with_capacity(1_000);
|
||||||
|
for name in DISHES {
|
||||||
|
use entities::recipies::ActiveModel;
|
||||||
|
recipies.push(
|
||||||
|
Recipies::insert(ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
title: Set(name.to_string()),
|
||||||
|
image_url: Set(format!(
|
||||||
|
"https://picsum.photos/{w}/{h}",
|
||||||
|
w = fake::rand::random::<i32>() + 200,
|
||||||
|
h = fake::rand::random::<i32>() + 200
|
||||||
|
)),
|
||||||
|
author: Set(Some(fake::faker::name::en::FirstName().fake())),
|
||||||
|
time: Set(Some(fake::rand::random::<i32>() * 15)),
|
||||||
|
summary: Set(fake::faker::lorem::en::Paragraph(2..4).fake()),
|
||||||
|
})
|
||||||
|
.exec_with_returning(&db)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
{
|
||||||
|
use entities::recipe_ingredients::ActiveModel;
|
||||||
|
RecipeIngredients::insert_many(
|
||||||
|
recipies
|
||||||
|
.iter()
|
||||||
|
.flat_map(|recipe| {
|
||||||
|
(0..rand::thread_rng().gen_range(3..6))
|
||||||
|
.map(|_| ingredients.choose(&mut rand::thread_rng()))
|
||||||
|
.map(|igredient| ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
ingredient_id: Set(ingredient.id),
|
||||||
|
qty: Set(rand::thread_rng().get_range(4..8)),
|
||||||
|
unit: Set(Some(UNIT.choose(&mut rand::thread_rng()))),
|
||||||
|
recipe_id: Set(recipe.id),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.exec(&db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const INGREDIENTS: [&'static str; 69] = [
|
||||||
|
"Almond Meal",
|
||||||
|
"Almonds",
|
||||||
|
"Amaranth",
|
||||||
|
"Apples",
|
||||||
|
"Apricots",
|
||||||
|
"Avocados",
|
||||||
|
"Bananas",
|
||||||
|
"Barley",
|
||||||
|
"Beef",
|
||||||
|
"Beef Chuck",
|
||||||
|
"Beef Ribs",
|
||||||
|
"Beef Tenderloin",
|
||||||
|
"Brisket",
|
||||||
|
"Brown Rice",
|
||||||
|
"Buckwheat",
|
||||||
|
"Bulgur",
|
||||||
|
"Cheese",
|
||||||
|
"Cherries",
|
||||||
|
"Chia Seeds",
|
||||||
|
"Chicken",
|
||||||
|
"Chicken Breasts",
|
||||||
|
"Chicken Legs",
|
||||||
|
"Chicken Thighs",
|
||||||
|
"Chicken Wings",
|
||||||
|
"Chocolate",
|
||||||
|
"Coconut",
|
||||||
|
"Corn Flour",
|
||||||
|
"Cornish Hens",
|
||||||
|
"Cornmeal",
|
||||||
|
"Duck",
|
||||||
|
"Fish",
|
||||||
|
"Flax Seeds",
|
||||||
|
"Goat",
|
||||||
|
"Ground Beef",
|
||||||
|
"Ground Chicken",
|
||||||
|
"Ground Pork",
|
||||||
|
"Ground Turkey",
|
||||||
|
"Lamb",
|
||||||
|
"Mangos",
|
||||||
|
"Millet",
|
||||||
|
"Mushroom",
|
||||||
|
"Nectarines",
|
||||||
|
"Oat Flour",
|
||||||
|
"Oats",
|
||||||
|
"Peaches",
|
||||||
|
"Peanuts",
|
||||||
|
"Pears",
|
||||||
|
"Pineapples",
|
||||||
|
"Plums",
|
||||||
|
"Pomegranates",
|
||||||
|
"Pork",
|
||||||
|
"Pork Ribs",
|
||||||
|
"Pork Shoulder",
|
||||||
|
"Pork Tenderloin",
|
||||||
|
"Prime Rib",
|
||||||
|
"Quinoa",
|
||||||
|
"Sausage",
|
||||||
|
"Seafood",
|
||||||
|
"Shellfish",
|
||||||
|
"Sirloin",
|
||||||
|
"Spelt",
|
||||||
|
"Steak",
|
||||||
|
"Tapioca Flour",
|
||||||
|
"Turkey",
|
||||||
|
"Veal",
|
||||||
|
"Venison",
|
||||||
|
"White Rice Flour",
|
||||||
|
"Wild Game",
|
||||||
|
"Wild Rice",
|
||||||
|
];
|
||||||
|
|
||||||
|
const DISHES: [&'static str; 96] = [
|
||||||
|
"Achari baingan",
|
||||||
|
"Aloo gobi",
|
||||||
|
"Aloo tikki",
|
||||||
|
"Aloo tuk",
|
||||||
|
"Aloo matar",
|
||||||
|
"Aloo kulcha",
|
||||||
|
"Aloo methi",
|
||||||
|
"Aloo shimla mirch",
|
||||||
|
"Amriti with rabdi",
|
||||||
|
"Talit Macchi
|
||||||
|
(Indian fish fry)",
|
||||||
|
"Baati",
|
||||||
|
"Bhatura",
|
||||||
|
"Bhindi masala",
|
||||||
|
"Biryani",
|
||||||
|
"Butter chicken",
|
||||||
|
"Chaat",
|
||||||
|
"Chana masala",
|
||||||
|
"Chapati",
|
||||||
|
"Chicken razala",
|
||||||
|
"Chicken Tikka",
|
||||||
|
"Chicken Tikka masala",
|
||||||
|
"Chole bhature",
|
||||||
|
"Daal baati churma",
|
||||||
|
"Daal puri",
|
||||||
|
"Dal makhani (kali dal)",
|
||||||
|
"Dal fara",
|
||||||
|
"Dal",
|
||||||
|
"Dal fry with tadka",
|
||||||
|
"Dum aloo",
|
||||||
|
"Poha",
|
||||||
|
"Fara",
|
||||||
|
"phirni",
|
||||||
|
"Aloo Phalliyaan",
|
||||||
|
"Gajar Pak[2]",
|
||||||
|
"Gatte ki Sabzi",
|
||||||
|
"Gajar matar aloo",
|
||||||
|
"Gobhi matar",
|
||||||
|
"Imarti",
|
||||||
|
"Hari mutter ka nimona (green peas daal)",
|
||||||
|
"Jalebi",
|
||||||
|
"Jaleba",
|
||||||
|
"Kachori",
|
||||||
|
"Kadai paneer",
|
||||||
|
"Kadhi pakoda",
|
||||||
|
"Karela bharta",
|
||||||
|
"Katha meetha petha / kaddu halwa",
|
||||||
|
"Kheer",
|
||||||
|
"Khichdi",
|
||||||
|
"Kadhi and Khichdi",
|
||||||
|
"Kofta",
|
||||||
|
"Kulfi falooda",
|
||||||
|
"Laapsi",
|
||||||
|
"Lauki ke kofte",
|
||||||
|
"Lauki ki bhaaji",
|
||||||
|
"Litti chokha",
|
||||||
|
"Makhaan ka kheer",
|
||||||
|
"Makki ki roti, sarson ka saag",
|
||||||
|
"Mathura ke pede",
|
||||||
|
"Methi saag, chaulai saag",
|
||||||
|
"Millet Lapsi",
|
||||||
|
"Mirchi Bada",
|
||||||
|
"Missi roti",
|
||||||
|
"Mixed vegetable",
|
||||||
|
"Moong dal ki Lapsi",
|
||||||
|
"Murgh musallam",
|
||||||
|
"Mushroom do pyaza (Kanda Khumb)",
|
||||||
|
"Mushroom matar (Matar Khumb)",
|
||||||
|
"Naan",
|
||||||
|
"Navrattan korma",
|
||||||
|
"Pakhala",
|
||||||
|
"Palak paneer",
|
||||||
|
"Paneer butter masala",
|
||||||
|
"Paneer tikka masala",
|
||||||
|
"Pani puri",
|
||||||
|
"Panjeeri",
|
||||||
|
"Papad",
|
||||||
|
"Paratha",
|
||||||
|
"Pattor",
|
||||||
|
"Phirni",
|
||||||
|
"Pinni",
|
||||||
|
"Rajma chaval",
|
||||||
|
"Rajma",
|
||||||
|
"Ramatori bhaaji",
|
||||||
|
"Lobiya",
|
||||||
|
"Samosa",
|
||||||
|
"Samose",
|
||||||
|
"Sattu ki roti",
|
||||||
|
"Rajwadi Chhena/Paneer[4]",
|
||||||
|
"Shahi tukra",
|
||||||
|
"Singhada Lapsi",
|
||||||
|
"Sooji halwa (Suji Lapsi)",
|
||||||
|
"Sweet pethas / kesar petha / pista petha",
|
||||||
|
"Vegetable jalfrezi",
|
||||||
|
"Tandoori Chicken",
|
||||||
|
"Tamatar Chaat",
|
||||||
|
"Tandoori Fish Tikka",
|
||||||
|
];
|
1
seed/target/.rustc_info.json
Normal file
1
seed/target/.rustc_info.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"rustc_fingerprint":5966236372496131995,"outputs":{"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.84.0-nightly (3ed6e3cc6 2024-10-17)\nbinary: rustc\ncommit-hash: 3ed6e3cc69857129c1d314daec00119ff47986ed\ncommit-date: 2024-10-17\nhost: x86_64-unknown-linux-gnu\nrelease: 1.84.0-nightly\nLLVM version: 19.1.1\n","stderr":""},"14371922958718593042":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/eraden/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\nfmt_debug=\"full\"\noverflow_checks\npanic=\"unwind\"\nproc_macro\nrelocation_model=\"pic\"\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"unknown\"\nub_checks\nunix\n","stderr":""},"15729799797837862367":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/eraden/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\nfmt_debug=\"full\"\noverflow_checks\npanic=\"unwind\"\nproc_macro\nrelocation_model=\"pic\"\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"unknown\"\nub_checks\nunix\n","stderr":""}},"successes":{}}
|
3
seed/target/CACHEDIR.TAG
Normal file
3
seed/target/CACHEDIR.TAG
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Signature: 8a477f597d28d172789f06886806bc55
|
||||||
|
# This file is a cache directory tag created by cargo.
|
||||||
|
# For information about cache directory tags see https://bford.info/cachedir/
|
52
src/types.rs
52
src/types.rs
@ -1,52 +0,0 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
pub type User = String;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Invalid step list")]
|
|
||||||
InvalidStepList,
|
|
||||||
#[error("Invalid ingeredients list")]
|
|
||||||
InvalidIngeredientList,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
|
|
||||||
pub enum Page {
|
|
||||||
Index,
|
|
||||||
Recipe,
|
|
||||||
Search,
|
|
||||||
SignIn,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct Admin {
|
|
||||||
pub email: String,
|
|
||||||
pub pass: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Admin {
|
|
||||||
type Err = ();
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut it = s.split(':');
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
email: it.next().expect("Admin login is required").into(),
|
|
||||||
pass: it.next().expect("Admin password is required").into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Deref)]
|
|
||||||
pub struct Admins(Vec<Admin>);
|
|
||||||
|
|
||||||
impl FromStr for Admins {
|
|
||||||
type Err = ();
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Ok(Self(
|
|
||||||
s.trim().split(',').filter_map(|s| s.parse().ok()).collect(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,9 +12,13 @@
|
|||||||
<h1 class="font-young-serif text-desktop-heading-l text-stone-700 sm:text-stone-800 text-center md:text-left">
|
<h1 class="font-young-serif text-desktop-heading-l text-stone-700 sm:text-stone-800 text-center md:text-left">
|
||||||
{{ recipe.title }}
|
{{ recipe.title }}
|
||||||
</h1>
|
</h1>
|
||||||
|
{% match recipe.summary %}
|
||||||
|
{% when Some with (summary) %}
|
||||||
<p class="font-outfit-regular text-stone-500 sm:text-base">
|
<p class="font-outfit-regular text-stone-500 sm:text-base">
|
||||||
{{ recipe.summary.clone().unwrap_or_default() }}
|
{{ summary }}
|
||||||
</p>
|
</p>
|
||||||
|
{% when None %}
|
||||||
|
{% endmatch %}
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="flex flex-col gap-7">
|
<div class="flex flex-col gap-7">
|
||||||
@ -22,16 +26,16 @@
|
|||||||
Ingredients
|
Ingredients
|
||||||
</p>
|
</p>
|
||||||
<ul class="list-disc marker:text-rose-900 list-inside flex flex-col gap-3">
|
<ul class="list-disc marker:text-rose-900 list-inside flex flex-col gap-3">
|
||||||
{% for ingeredient in ingeredients %}
|
{% for ingredient in ingredients %}
|
||||||
<li class="paragraph">
|
<li class="paragraph">
|
||||||
<span class="mr-1">
|
<span class="mr-1">
|
||||||
{{ ingeredient.qty }}
|
{{ ingredient.qty }}
|
||||||
</span>
|
</span>
|
||||||
<span class="mr-4">
|
<span class="mr-4">
|
||||||
{{ ingeredient.unit }}
|
{{ ingredient.unit }}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{{ ingeredient.name }}
|
{{ ingredient.name }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -44,7 +48,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<ol class="list-decimal list-inside flex flex-col gap-3">
|
<ol class="list-decimal list-inside flex flex-col gap-3">
|
||||||
{% for step in steps %}
|
{% for step in steps %}
|
||||||
<li class="font-outfit-regular text-stone-500 text-base text-base">
|
<li class="font-outfit-regular text-stone-500 text-base">
|
||||||
{{ step.body }}
|
{{ step.body }}
|
||||||
<div>{{ step.hint.clone().unwrap_or_default() }}</div>
|
<div>{{ step.hint.clone().unwrap_or_default() }}</div>
|
||||||
</li>
|
</li>
|
||||||
@ -56,7 +60,7 @@
|
|||||||
<p class="text-desktop-heading-m">
|
<p class="text-desktop-heading-m">
|
||||||
Tags
|
Tags
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-4 gap-7">
|
<div class="flex gap-7">
|
||||||
{% for tag in tags %}
|
{% for tag in tags %}
|
||||||
<a class="bg-gray-300 dark:bg-gray-700 text-base text-gray-700 dark:text-white py-2 px-4 rounded-full font-bold hover:bg-gray-400 dark:hover:bg-gray-600 flex justify-center items-center">
|
<a class="bg-gray-300 dark:bg-gray-700 text-base text-gray-700 dark:text-white py-2 px-4 rounded-full font-bold hover:bg-gray-400 dark:hover:bg-gray-600 flex justify-center items-center">
|
||||||
{{ tag.name }}
|
{{ tag.name }}
|
||||||
@ -69,10 +73,10 @@
|
|||||||
<p class="text-desktop-heading-m">
|
<p class="text-desktop-heading-m">
|
||||||
Ingredients
|
Ingredients
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-4 gap-7">
|
<div class="flex gap-7">
|
||||||
{% for ingeredient in ingeredients %}
|
{% for ingredient in ingredients %}
|
||||||
<a class="bg-gray-300 dark:bg-gray-700 text-base text-gray-700 dark:text-white py-2 px-4 rounded-full font-bold hover:bg-gray-400 dark:hover:bg-gray-600 flex justify-center items-center">
|
<a class="bg-gray-300 dark:bg-gray-700 text-base text-gray-700 dark:text-white py-2 px-4 rounded-full font-bold hover:bg-gray-400 dark:hover:bg-gray-600 flex justify-center items-center">
|
||||||
{{ ingeredient.name }}
|
{{ ingredient.name }}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user