Render recipes
This commit is contained in:
parent
55dd953f43
commit
1aecaf9286
4 changed files with 217 additions and 31 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -2953,6 +2953,7 @@ dependencies = [
|
||||||
name = "regalade"
|
name = "regalade"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ammonia",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"api",
|
"api",
|
||||||
"axum",
|
"axum",
|
||||||
|
|
@ -2963,6 +2964,7 @@ dependencies = [
|
||||||
"migration",
|
"migration",
|
||||||
"openidconnect",
|
"openidconnect",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"pulldown-cmark",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"sea-query",
|
"sea-query",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ time = "0.3.31"
|
||||||
maud = { git = "https://github.com/lambda-fairy/maud", version = "0.25.0", features = [
|
maud = { git = "https://github.com/lambda-fairy/maud", version = "0.25.0", features = [
|
||||||
"axum",
|
"axum",
|
||||||
] }
|
] }
|
||||||
|
pulldown-cmark = "0.9.3"
|
||||||
|
ammonia = "3.3.0"
|
||||||
|
|
||||||
[dependencies.sea-orm]
|
[dependencies.sea-orm]
|
||||||
version = "0.12"
|
version = "0.12"
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,8 @@ enum RouteError {
|
||||||
Session(#[from] session::Error),
|
Session(#[from] session::Error),
|
||||||
#[error("Could not extract session")]
|
#[error("Could not extract session")]
|
||||||
SessionExtract,
|
SessionExtract,
|
||||||
|
#[error("Unexpected internal error")]
|
||||||
|
Internal(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DbErr> for Box<RouteError> {
|
impl From<DbErr> for Box<RouteError> {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,29 @@
|
||||||
use axum::{extract::State, routing::get, Router};
|
use std::borrow::Cow;
|
||||||
use maud::{html, Markup};
|
|
||||||
use sea_orm::prelude::*;
|
|
||||||
|
|
||||||
use crate::entity::prelude::*;
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use pulldown_cmark::Parser;
|
||||||
|
use sea_orm::{prelude::*, QueryOrder};
|
||||||
|
|
||||||
|
use crate::entity::{prelude::*, recipe};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
base_page,
|
||||||
household::CurrentHousehold,
|
household::CurrentHousehold,
|
||||||
sidebar::{sidebar, SidebarLocation},
|
sidebar::{sidebar, SidebarLocation},
|
||||||
AppState, AuthenticatedUser, RouteError,
|
AppState, AuthenticatedUser, RouteError,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) fn routes() -> Router<AppState> {
|
pub(super) fn routes() -> Router<AppState> {
|
||||||
Router::new().route("/", get(list_recipes))
|
Router::new()
|
||||||
|
.route("/", get(list_recipes))
|
||||||
|
.route("/:id", get(view_recipe))
|
||||||
|
.route("/public/:hs/:id", get(view_public_recipe))
|
||||||
|
.route("/public/:hs", get(list_public_recipe))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recipe_rating(rating: i32) -> Markup {
|
fn recipe_rating(rating: i32) -> Markup {
|
||||||
|
|
@ -24,41 +36,209 @@ fn recipe_rating(rating: i32) -> Markup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If household is None then the function acts on the private household
|
||||||
|
fn recipe_list(recipes: &[recipe::Model], household: Option<Uuid>) -> Markup {
|
||||||
|
let destination = match household {
|
||||||
|
Some(id) => Cow::from(format!("/recipe/public/{id}/")),
|
||||||
|
None => Cow::from("/recipe/"),
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
.d-flex.align-items-center.justify-content-center."w-100" {
|
||||||
|
.container.text-center.rounded.border."pt-2"."m-2" {
|
||||||
|
h2 { "Recipes" }
|
||||||
|
.container.text-center {
|
||||||
|
.row."row-cols-2"."row-cols-sm-2"."row-cols-md-4"."g-2"."mb-2" {
|
||||||
|
@for r in recipes {
|
||||||
|
.col {
|
||||||
|
."p-3".border.rounded.border-light-subtle."h-100" {
|
||||||
|
a .link-light."link-offset-2"."link-underline-opacity-25"."link-underline-opacity-100-hover"
|
||||||
|
href={(destination) (r.id)} {
|
||||||
|
(r.name) @if household.is_none() { (recipe_rating(r.ranking)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_recipes(
|
async fn list_recipes(
|
||||||
state: State<AppState>,
|
state: State<AppState>,
|
||||||
user: AuthenticatedUser,
|
user: AuthenticatedUser,
|
||||||
household: CurrentHousehold,
|
household: CurrentHousehold,
|
||||||
) -> Result<Markup, RouteError> {
|
) -> Result<Markup, RouteError> {
|
||||||
let mut recipes = household.0.find_related(Recipe).all(&state.db).await?;
|
let recipes = household
|
||||||
recipes.sort_unstable_by(|m1, m2| m1.name.cmp(&m2.name));
|
.0
|
||||||
|
.find_related(Recipe)
|
||||||
let content = html! {
|
.order_by_asc(recipe::Column::Name)
|
||||||
.d-flex.align-items-center.justify-content-center."w-100" {
|
.all(&state.db)
|
||||||
.container.text-center.rounded.border."pt-2"."m-2" {
|
.await?;
|
||||||
h2 { "Recipes" }
|
|
||||||
.container.text-center {
|
|
||||||
.row."row-cols-2"."row-cols-sm-2"."row-cols-md-4"."g-2"."mb-2" {
|
|
||||||
@for r in recipes {
|
|
||||||
.col {
|
|
||||||
."p-3".border.rounded.border-light-subtle."h-100" {
|
|
||||||
a .link-light."link-offset-2"."link-underline-opacity-25"."link-underline-opacity-100-hover"
|
|
||||||
href={"/recipe/" (r.id)}
|
|
||||||
{
|
|
||||||
(r.name) (recipe_rating(r.ranking))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(sidebar(
|
Ok(sidebar(
|
||||||
SidebarLocation::RecipeList,
|
SidebarLocation::RecipeList,
|
||||||
&household,
|
&household,
|
||||||
&user,
|
&user,
|
||||||
content,
|
recipe_list(&recipes, None),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_public_recipe(
|
||||||
|
state: State<AppState>,
|
||||||
|
household: Path<Uuid>,
|
||||||
|
) -> Result<Markup, RouteError> {
|
||||||
|
let household = Household::find_by_id(household.0)
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.ok_or(RouteError::RessourceNotFound)?;
|
||||||
|
|
||||||
|
let recipes = household
|
||||||
|
.find_related(Recipe)
|
||||||
|
.order_by_asc(recipe::Column::Name)
|
||||||
|
.all(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(base_page(recipe_list(&recipes, Some(household.id))))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recipe_view(
|
||||||
|
r: &recipe::Model,
|
||||||
|
private: bool,
|
||||||
|
db: &DatabaseConnection,
|
||||||
|
) -> Result<Markup, RouteError> {
|
||||||
|
let base_ingredients = r.find_related(Ingredient).all(db).await?;
|
||||||
|
|
||||||
|
let mut ingredients = Vec::with_capacity(base_ingredients.len());
|
||||||
|
for ig in base_ingredients {
|
||||||
|
ingredients.push((
|
||||||
|
RecipeIngredients::find_by_id((r.id, ig.id))
|
||||||
|
.one(db)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
RouteError::Internal(format!(
|
||||||
|
"No recipe ingredient found for rcp={}/ig={}",
|
||||||
|
r.id, ig.id
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
.amount,
|
||||||
|
ig,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut script = "const base_amount = [".to_string();
|
||||||
|
for (amount, ig) in &ingredients {
|
||||||
|
script += &format!(
|
||||||
|
r#"{{amount: {}, elem: document.querySelector('#rcpIg{} > span > .ig-amount')}},"#,
|
||||||
|
amount, ig.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
script += r#"];
|
||||||
|
const chg_person = document.querySelector('#rcpPersonCount');
|
||||||
|
chg_person.addEventListener("change", (event) => {
|
||||||
|
base_amount.forEach(({amount, elem}) => {
|
||||||
|
elem.textContent = amount * event.target.value;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let steps = {
|
||||||
|
let parser = Parser::new(&r.steps);
|
||||||
|
|
||||||
|
let mut html_output = String::new();
|
||||||
|
pulldown_cmark::html::push_html(&mut html_output, parser);
|
||||||
|
|
||||||
|
ammonia::clean(&html_output)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(html! {
|
||||||
|
.d-flex.align-items-center.justify-content-center."w-100" {
|
||||||
|
.container.text-center.rounded.border."pt-2"."m-2" {
|
||||||
|
h1 { (r.name) @if private { (recipe_rating(r.ranking)) } }
|
||||||
|
@if private { ."mt-2" { "TODO: edit" } }
|
||||||
|
."mt-2".container.text-start {
|
||||||
|
.row {
|
||||||
|
."col-8"[private] {
|
||||||
|
.input-group {
|
||||||
|
input .form-control type="number" #rcpPersonCount min="1" value=(r.person_count) {}
|
||||||
|
span .input-group-text { "people" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if private { .col {"TODO: edit"} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hr {}
|
||||||
|
.text-start {
|
||||||
|
h2 { "Ingredients" }
|
||||||
|
ul .list-group."mb-2" {
|
||||||
|
@for (amount, ig) in &ingredients {
|
||||||
|
li .list-group-item.d-flex.justify-content-between.align-items-center #{"rcpIg" (ig.id)} {
|
||||||
|
span {
|
||||||
|
span .ig-amount { ((*amount * r.person_count as f64).round()) }
|
||||||
|
@if let Some(u) = &ig.unit { (u) } " " (ig.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if private { "TODO: add ingredient" }
|
||||||
|
}
|
||||||
|
hr {}
|
||||||
|
.text-start {
|
||||||
|
h2 {"Steps"}
|
||||||
|
div { (PreEscaped(steps)) }
|
||||||
|
@ if private { "TODO: edit steps" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
script { (PreEscaped(script)) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn view_recipe(
|
||||||
|
state: State<AppState>,
|
||||||
|
user: AuthenticatedUser,
|
||||||
|
household: CurrentHousehold,
|
||||||
|
id: Path<i32>,
|
||||||
|
) -> Result<Markup, RouteError> {
|
||||||
|
let recipe = household
|
||||||
|
.0
|
||||||
|
.find_related(Recipe)
|
||||||
|
.filter(recipe::Column::Id.eq(id.0))
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.ok_or(RouteError::RessourceNotFound)?;
|
||||||
|
|
||||||
|
Ok(sidebar(
|
||||||
|
SidebarLocation::RecipeList,
|
||||||
|
&household,
|
||||||
|
&user,
|
||||||
|
recipe_view(&recipe, true, &state.db).await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct PublicQuery {
|
||||||
|
hs: Uuid,
|
||||||
|
id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn view_public_recipe(
|
||||||
|
state: State<AppState>,
|
||||||
|
info: Path<PublicQuery>,
|
||||||
|
) -> Result<Markup, RouteError> {
|
||||||
|
let household = Household::find_by_id(info.0.hs)
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.ok_or(RouteError::RessourceNotFound)?;
|
||||||
|
|
||||||
|
let recipe = household
|
||||||
|
.find_related(Recipe)
|
||||||
|
.filter(recipe::Column::Id.eq(info.0.id))
|
||||||
|
.one(&state.db)
|
||||||
|
.await?
|
||||||
|
.ok_or(RouteError::RessourceNotFound)?;
|
||||||
|
|
||||||
|
Ok(base_page(recipe_view(&recipe, false, &state.db).await?))
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue