app,server: Allow to edit the rating

This commit is contained in:
traxys 2023-06-26 12:47:06 +02:00
parent 5ef000dec0
commit d614029931
4 changed files with 167 additions and 15 deletions

View file

@ -129,3 +129,8 @@ pub struct AddRecipeIngredientRequest {
pub struct RecipeEditStepsRequest {
pub steps: String
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RecipeEditRating {
pub rating: u8,
}

View file

@ -1,6 +1,6 @@
use api::{
AddRecipeIngredientRequest, IngredientInfo, RecipeEditStepsRequest, RecipeInfo,
RecipeIngredientEditRequest, RecipeRenameRequest,
AddRecipeIngredientRequest, IngredientInfo, RecipeEditRating, RecipeEditStepsRequest,
RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest,
};
use itertools::Itertools;
use uuid::Uuid;
@ -19,6 +19,21 @@ use crate::{
RegaladeGlobalState, Route,
};
#[derive(Debug, Clone, PartialEq, Properties)]
struct RecipeRatingProps {
rating: u8,
}
#[function_component]
fn RecipeRating(props: &RecipeRatingProps) -> Html {
let rating = (props.rating + 1).min(3);
html! {
<div aria-label={format!("Rating: {rating}")}>
{ for (0..rating).map(|_| html!{<i aria-hidden="true" class="bi-star-fill" />}) }
</div>
}
}
async fn get_all_recipes(
token: String,
household: Uuid,
@ -50,7 +65,7 @@ fn RecipeListInner(props: &RecipeListProps) -> HtmlResult {
Ok(l) => html! {
<div class="container text-center">
<div class="row row-cols-2 row-cols-sm-2 row-cols-md-4 g-2 mb-2">
{for l.recipes.iter().sorted_by_key(|(_, name, _)| name).map(|(id, name, _)| html!{
{for l.recipes.iter().sorted_by_key(|(_, name, _)| name).map(|(id, name, rating)| html!{
<div class="col" key={*id}>
<div class="p-3 border rounded border-light-subtle h-100">
<Link<Route>
@ -64,6 +79,7 @@ fn RecipeListInner(props: &RecipeListProps) -> HtmlResult {
>
{name}
</Link<Route>>
<RecipeRating {rating} />
</div>
</div>
})}
@ -623,19 +639,119 @@ fn EditSteps(props: &EditStepsProps) -> Html {
</>}
}
#[derive(Debug, Clone, PartialEq, Properties)]
struct RecipeRatingProps {
async fn do_edit_rating(
token: String,
household: Uuid,
recipe: i64,
rating: u8,
) -> anyhow::Result<()> {
let rsp = gloo_net::http::Request::patch(api!("household/{household}/recipe/{recipe}/rating"))
.json(&RecipeEditRating { rating })?
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!("Could not edit rating (code={}): {body}", rsp.status());
}
Ok(())
}
#[derive(Properties, PartialEq, Clone)]
struct EditRatingProps {
token: String,
household: Uuid,
recipe: i64,
rating: u8,
update: Callback<()>,
}
#[function_component]
fn RecipeRating(props: &RecipeRatingProps) -> Html {
let rating = (props.rating + 1).min(3);
html! {
<div aria-label={format!("Rating: {rating}")}>
{ for (0..rating).map(|_| html!{<i aria-hidden="true" class="bi-star-fill" />}) }
</div>
}
fn EditRating(props: &EditRatingProps) -> Html {
let rating = use_state(|| props.rating);
let error = use_state(|| None::<String>);
let onchange = {
let rating = rating.clone();
Callback::from(move |e: Event| {
let Some(target) = e.target() else {
return;
};
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
return;
};
if !target.report_validity() {
return;
}
rating.set(target.value().parse().expect("invalid number"));
})
};
let on_submit = {
let rating = rating.clone();
let token = props.token.clone();
let household = props.household;
let recipe = props.recipe;
let error = error.clone();
let update = props.update.clone();
Callback::from(move |_| {
let token = token.clone();
let rating = rating.clone();
let error = error.clone();
let update = update.clone();
wasm_bindgen_futures::spawn_local(async move {
match do_edit_rating(token.clone(), household, recipe, *rating - 1).await {
Ok(_) => {
let modal = bs::Modal::get_instance("#rcpEditRating");
modal.hide();
error.set(None);
update.emit(());
}
Err(e) => {
error.set(Some(format!("Could not edit rating: {e}")));
}
}
});
})
};
html! {<>
<ModalToggleButton modal_id="rcpEditRating" classes={classes!("btn", "btn-secondary")}>
{"Edit Rating"}
</ModalToggleButton>
<FormModal
id="rcpEditRating"
fade=true
centered=true
submit_label="Edit"
title="Edit rating"
{on_submit}
>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<div class="form-floating">
<input
type="number"
max="3"
min="1"
class="form-control"
id="rcpEditRatingInp"
value={(*rating).to_string()}
{onchange}
/>
<label for="rcpEditRatingInp">{"Rating"}</label>
</div>
</FormModal>
</>}
}
#[function_component]
@ -686,8 +802,19 @@ fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
name={r.name.clone()}
update={update.clone()}
/>
<div class="mt-2">
<RecipeRating rating={r.rating}/>
<div class="container row mt-2">
<div class="col-8">
<RecipeRating rating={r.rating} />
</div>
<div class="col-4">
<EditRating
token={props.token.clone()}
recipe={props.id}
household={props.household}
rating={r.rating + 1}
update={update.clone()}
/>
</div>
</div>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">

View file

@ -232,6 +232,10 @@ pub(crate) fn router(api_allowed: Option<HeaderValue>) -> Router<AppState> {
"/household/:house_id/recipe/:recipe_id/steps",
patch(recipe::edit_step).layer(mk_service(vec![Method::PATCH])),
)
.route(
"/household/:house_id/recipe/:recipe_id/rating",
patch(recipe::edit_rating).layer(mk_service(vec![Method::PATCH])),
)
.route(
"/household/:house_id/recipe/:recipe_id/ingredients/:iid",
patch(recipe::edit_ig_amount)

View file

@ -1,6 +1,6 @@
use api::{
AddRecipeIngredientRequest, CreateRecipeRequest, CreateRecipeResponse, EmptyResponse,
IngredientInfo, ListRecipesResponse, RecipeEditStepsRequest, RecipeInfo,
IngredientInfo, ListRecipesResponse, RecipeEditRating, RecipeEditStepsRequest, RecipeInfo,
RecipeIngredientEditRequest, RecipeRenameRequest,
};
use axum::{
@ -227,3 +227,19 @@ pub(super) async fn edit_step(
Ok(EmptyResponse {}.into())
}
pub(super) async fn edit_rating(
State(state): State<AppState>,
RecipeExtractor(recipe): RecipeExtractor,
Json(req): Json<RecipeEditRating>,
) -> JsonResult<EmptyResponse> {
let model = recipe::ActiveModel {
id: ActiveValue::Set(recipe.id),
ranking: ActiveValue::Set(req.rating as _),
..Default::default()
};
model.update(&state.db).await?;
Ok(EmptyResponse {}.into())
}