app,server: Allow to edit the rating
This commit is contained in:
parent
5ef000dec0
commit
d614029931
4 changed files with 167 additions and 15 deletions
|
|
@ -129,3 +129,8 @@ pub struct AddRecipeIngredientRequest {
|
||||||
pub struct RecipeEditStepsRequest {
|
pub struct RecipeEditStepsRequest {
|
||||||
pub steps: String
|
pub steps: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct RecipeEditRating {
|
||||||
|
pub rating: u8,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use api::{
|
use api::{
|
||||||
AddRecipeIngredientRequest, IngredientInfo, RecipeEditStepsRequest, RecipeInfo,
|
AddRecipeIngredientRequest, IngredientInfo, RecipeEditRating, RecipeEditStepsRequest,
|
||||||
RecipeIngredientEditRequest, RecipeRenameRequest,
|
RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -19,6 +19,21 @@ use crate::{
|
||||||
RegaladeGlobalState, Route,
|
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(
|
async fn get_all_recipes(
|
||||||
token: String,
|
token: String,
|
||||||
household: Uuid,
|
household: Uuid,
|
||||||
|
|
@ -50,7 +65,7 @@ fn RecipeListInner(props: &RecipeListProps) -> HtmlResult {
|
||||||
Ok(l) => html! {
|
Ok(l) => html! {
|
||||||
<div class="container text-center">
|
<div class="container text-center">
|
||||||
<div class="row row-cols-2 row-cols-sm-2 row-cols-md-4 g-2 mb-2">
|
<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="col" key={*id}>
|
||||||
<div class="p-3 border rounded border-light-subtle h-100">
|
<div class="p-3 border rounded border-light-subtle h-100">
|
||||||
<Link<Route>
|
<Link<Route>
|
||||||
|
|
@ -64,6 +79,7 @@ fn RecipeListInner(props: &RecipeListProps) -> HtmlResult {
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Link<Route>>
|
</Link<Route>>
|
||||||
|
<RecipeRating {rating} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
})}
|
})}
|
||||||
|
|
@ -623,19 +639,119 @@ fn EditSteps(props: &EditStepsProps) -> Html {
|
||||||
</>}
|
</>}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
async fn do_edit_rating(
|
||||||
struct RecipeRatingProps {
|
token: String,
|
||||||
|
household: Uuid,
|
||||||
|
recipe: i64,
|
||||||
rating: u8,
|
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]
|
#[function_component]
|
||||||
fn RecipeRating(props: &RecipeRatingProps) -> Html {
|
fn EditRating(props: &EditRatingProps) -> Html {
|
||||||
let rating = (props.rating + 1).min(3);
|
let rating = use_state(|| props.rating);
|
||||||
html! {
|
|
||||||
<div aria-label={format!("Rating: {rating}")}>
|
let error = use_state(|| None::<String>);
|
||||||
{ for (0..rating).map(|_| html!{<i aria-hidden="true" class="bi-star-fill" />}) }
|
|
||||||
</div>
|
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]
|
#[function_component]
|
||||||
|
|
@ -686,8 +802,19 @@ fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
|
||||||
name={r.name.clone()}
|
name={r.name.clone()}
|
||||||
update={update.clone()}
|
update={update.clone()}
|
||||||
/>
|
/>
|
||||||
<div class="mt-2">
|
<div class="container row mt-2">
|
||||||
<RecipeRating rating={r.rating}/>
|
<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>
|
</div>
|
||||||
if let Some(e) = &*error {
|
if let Some(e) = &*error {
|
||||||
<div class={classes!("alert", "alert-danger")} role="alert">
|
<div class={classes!("alert", "alert-danger")} role="alert">
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,10 @@ pub(crate) fn router(api_allowed: Option<HeaderValue>) -> Router<AppState> {
|
||||||
"/household/:house_id/recipe/:recipe_id/steps",
|
"/household/:house_id/recipe/:recipe_id/steps",
|
||||||
patch(recipe::edit_step).layer(mk_service(vec![Method::PATCH])),
|
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(
|
.route(
|
||||||
"/household/:house_id/recipe/:recipe_id/ingredients/:iid",
|
"/household/:house_id/recipe/:recipe_id/ingredients/:iid",
|
||||||
patch(recipe::edit_ig_amount)
|
patch(recipe::edit_ig_amount)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use api::{
|
use api::{
|
||||||
AddRecipeIngredientRequest, CreateRecipeRequest, CreateRecipeResponse, EmptyResponse,
|
AddRecipeIngredientRequest, CreateRecipeRequest, CreateRecipeResponse, EmptyResponse,
|
||||||
IngredientInfo, ListRecipesResponse, RecipeEditStepsRequest, RecipeInfo,
|
IngredientInfo, ListRecipesResponse, RecipeEditRating, RecipeEditStepsRequest, RecipeInfo,
|
||||||
RecipeIngredientEditRequest, RecipeRenameRequest,
|
RecipeIngredientEditRequest, RecipeRenameRequest,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
|
|
@ -227,3 +227,19 @@ pub(super) async fn edit_step(
|
||||||
|
|
||||||
Ok(EmptyResponse {}.into())
|
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())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue