| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- package com.weEat.view
- import com.raquo.laminar.api.L._
- import io.laminext.syntax.core._
- import com.raquo.airstream.ownership.ManualOwner
- import com.raquo.waypoint._
- import play.api.libs.json.{JsValue,Json}
- import com.weEat.controllers.{FoodController,USDAController}
- import com.weEat.modules._
- import com.weEat.shared.models.UnitType._
- import com.weEat.shared.models._
- import org.scalajs.dom.{Event,KeyboardEvent,HTMLInputElement,HTMLElement}
- import com.tflucke.sortable.{Sortable, SortableOptions, SortableEvent}
- import com.tflucke.typeahead._
- import com.tflucke.typeahead.Dataset.Templates
- import org.scalajs.dom.document
- import gov.usda.nal.fdc.models.DataType._
- import scala.concurrent.Future
- import scala.util.{Success,Failure}
- // TODO: prevent user from not having any of discreet/mass/volume input
- // TODO: Save recipe node in cookie until ready to use
- object RecipeEdit extends View[Option[String]] {
- import com.weEat.Main.headers
- implicit val ec = com.weEat.shared.ctx
- val navName = "New Recipe"
- val tag = "editRecipe"
- case class ViewPage(val id: Option[String] = None) extends P {
- val title = "Edit Recipe"
- def jsonValue = Json.toJson(id)
- }
- def parseJson(jsVal: JsValue) = ViewPage(jsVal.asOpt[String])
- def route = Route.onlyQuery(
- encode = (page: ViewPage) => page.id,
- decode = (id: Option[String]) => ViewPage(id = id),
- pattern = (root / tag / endOfSegments) ? param[String]("id").?
- )
- def defaultPage = ViewPage()
- val ENTER_KEY_CODE = 13
- override def permissions = Set("user")
- private val _nameIn = input(idAttr := "name",
- cls := "form-control",
- required := true
- )
- private val _name: Signal[String] = _nameIn.value
- private val internalFoodDS = Dataset(
- { _ => Nil},
- Some({ (str: String) => FoodController.query(str.toLowerCase)() }),
- display = { (food: FoodNodeId) => food.name }
- )
- private val usdaFoodDS = Dataset(
- { _ => Nil},
- Some({ str: String =>
- USDAController.getFoodsSearch(str, Seq(
- Foundation, Survey, SRLegacy
- ).map(_.toString))().map(_.foods.map(USDANodeNoId.fromSearchResult))
- }),
- templates = Some(Templates({(x: USDANodeNoId) => x.name}).copy(
- header = Some({ (_: String, _: Seq[USDANodeNoId], _: String) =>
- val html = document.createElement("div").asInstanceOf[HTMLElement]
- html.innerText = "Unoffical Foods"
- html
- })
- )),
- display = {(x: USDANodeNoId) => x.name}
- )
- private val _ingredientSearch = input(typ := "text",
- cls := "form-control input-sm",
- onMountCallback({(ctx) =>
- val elm = ctx.thisNode
- TypeaheadElement[FoodNode](
- elm.ref.asInstanceOf[HTMLInputElement],
- minLength = 3
- )(Seq(internalFoodDS, usdaFoodDS))
- elm.amend(
- TypeaheadRx.onSelected[FoodNode] --> { (e: CursorEvent[FoodNode]) =>
- e.selectable.map(_.data).foreach({ node =>
- // TODO: default unit
- _editIngredient(Ingredient.fromFoodNode(node, 0, Gram)) { in =>
- _ingredients.update(_ :+ in)
- elm.ref.value = ""
- Future.successful(())
- }
- })
- }
- )
- })
- )
- private val _stepIn = input(idAttr := "step",
- cls := "form-control",
- onKeyPress --> { (e: KeyboardEvent) => if (e.keyCode == ENTER_KEY_CODE) {
- val elm = e.target.asInstanceOf[HTMLInputElement]
- _steps.update(_ :+ elm.value)
- elm.value = ""
- } }
- )
- private val _ingredients = Var[Seq[Ingredient]](Nil)
- private val _steps = Var[Seq[String]](Nil)
- private val _servingsIn = input(idAttr := "serv",
- typ := "number",
- cls := "form-control",
- defaultValue := "4",
- stepAttr := "0.1",
- required := true
- )
- private val _servings = _servingsIn.value
- private val _discreetIn = input(idAttr := "discreet",
- typ := "checkbox",
- cls := "form-check-input",
- defaultChecked := true
- )
- private val _discreet = _discreetIn.checked
- // TODO: Try autopopulate ingredients.sum(_.massPerUnit)
- private val _massPIn = input(idAttr := "massP",
- cls := "form-control",
- typ := "number",
- minAttr := "1",
- stepAttr := "0.1",
- value := "100"
- )
- private val _massP = _massPIn.value
- // TODO: Try autopopulate ingredients.averge(_.density)
- private val _volPIn = input(idAttr := "volP",
- cls := "form-control",
- typ := "number",
- minAttr := "0",
- stepAttr := "0.1"
- )
- private val _volP = _volPIn.value
- private val _volPUnit = Var[MeasureUnit](Milliliter)
- private val _volPUnitIn = select(cls := "custom-select",
- MeasureUnit.units.zipWithIndex
- .filter({ case (u, _) => u.typ == VOLUME })
- .map({ case (unit, idx) =>
- // TODO: default selected dynamic
- option(
- value := idx.toString,
- selected := unit.abr == Milliliter.abr,
- unit.abr
- )
- }),
- onChange.mapToValue.map(_.toInt).map(MeasureUnit.units(_)) --> _volPUnit
- )
- val recipieNode = _ingredients.signal.combineWithFn(
- _steps,
- _name,
- _discreet,
- _servings,
- _volP,
- _volPUnit,
- _massP
- ) { case (ingredients, steps, name, discreet, servings, volP, volPUnit,
- massP) =>
- val gPServ = massP.toFloatOption
- val mLPServ = volP.toFloatOption
- .map( _ * volPUnit.conversionRatio )
- val (servFactor, servUnit) = gPServ.zip(Some(Gram))
- .getOrElse(
- if (discreet) (1.0f, Count)
- else mLPServ.zip(Some(Milliliter)).getOrElse(???)
- ).asInstanceOf[(Float, MeasureUnit)]
- val numServings = servings.toFloatOption.getOrElse(1.0f)
- val stdQtiesPServing = servFactor / servUnit.typ.standardQuanity
- val stdQties = numServings * stdQtiesPServing
- RecipeNodeNoId(
- name,
- stdQties,
- stdQtiesPServing,
- servUnit.typ,
- ingredients,
- steps,
- gPServ.zip(mLPServ).map({ case (m, v) => (m/v).toFloat }),
- gPServ
- )
- }
- private def _editIngredient(ing: Ingredient)(callback: (Ingredient => Future[_])) = {
- val amount = Var[Float](ing.amount)
- val unit = Var[MeasureUnit](Gram)
- Overlay.confirmFuture(ing.food.map({ (food) =>
- val amountIn = input(typ := "number",
- cls := "form-control input-sm col-9",
- minAttr := "0",
- defaultValue := ing.amount.toString,
- onInput.mapToValue.map(_.toFloat) --> amount
- )
- val unitIn = select(cls := "col-3 custom-select",
- MeasureUnit.units.zipWithIndex.map({ case (unit, idx) =>
- // TODO: default selected dynamic
- option(value := idx.toString,
- selected := unit.abr == Gram.abr,
- unit.name
- )
- }),
- onChange.mapToValue.map(_.toInt).map(MeasureUnit.units(_)) --> unit
- )
- div(cls := "row", amountIn, unitIn)
- }),
- amount.signal.recoverToTry.map(_.isSuccess) &&
- unit.signal.recoverToTry.map(_.isSuccess)
- ) { () =>
- callback(ing.copy(
- amount = amount.now(),
- unit = unit.now()
- ))
- }
- }
- private def _removeFromList[T](s: Seq[T], idx: Int) =
- s.take(idx) ++ s.drop(idx + 1)
- private val _stepList = ol(
- listStyleType := "none",
- paddingLeft := "0",
- children <-- _steps.signal.map(_.zipWithIndex.map((presentStep _).tupled)),
- onMountCallback({(ctx) =>
- Sortable.create(ctx.thisNode.ref, SortableOptions.onUpdate({
- (event: SortableEvent) =>
- _steps.update({ steps =>
- (event.oldIndex.toOption, event.newIndex.toOption) match {
- case (Some(old), Some(ne)) if (old < ne) =>
- moveBackwards(steps, old, ne)
- case (Some(old), Some(ne)) =>
- moveBackwards(
- steps.reverse,
- steps.size - old - 1,
- steps.size - ne - 1
- ).reverse
- case (None, Some(ne)) =>
- val (first, second) = steps.splitAt(ne)
- (first :+ event.item.innerText) ++ second
- case (Some(old), None) =>
- _removeFromList(steps, old)
- }
- })
- // If the added element is still in the parent, remove it since the
- // Rx.update will have generated a new one.
- Option(event.item.parentElement).map(_.removeChild(event.item))
- def moveBackwards(steps: Seq[String], old: Int, ne: Int) =
- (steps.take(old) ++
- steps.take(ne+1).drop(old + 1) :+
- steps(old)) ++
- steps.drop(ne+1)
- }))
- })
- )
- def presentFoodNode(idx: Int)(ingredientSig: Signal[Ingredient]) =
- li(children <-- ingredientSig.flatMap { (ingredient: Ingredient) =>
- Signal.fromFuture(ingredient.food).optionMap { (food) =>
- Seq(
- span(cls := "ui-icon ui-icon-pencil",
- onClick --> { (e: Event) =>
- _editIngredient(ingredient) { (in) =>
- _ingredients.update(_.updated(idx, in))
- Future.successful(())
- }
- }
- ),
- span(cls := "ui-icon ui-icon-close",
- onClick --> { (e: Event) =>
- _ingredients.update(_.filterNot(_ == ingredient))
- }
- ),
- span(f"${ingredient.amount}%.02f${ingredient.unit.abr} ${food.name}")
- )
- } withDefault(Nil)
- })
- def presentStep(step: String, idx: Int) = li(
- span(cls := "ui-icon ui-icon-close",
- onClick --> { (e: Event) =>
- _steps.update(_removeFromList(_, idx))
- }
- ),
- step
- )
- def content(page: Signal[ViewPage]) = div(
- h2("Recipe Editor"),
- div(cls := "form-group",
- div(cls := "container",
- div(cls := "row",
- div(cls := "col-md-12",
- label("Name: "),
- _nameIn
- )
- ),
- div(cls := "row",
- div(cls := "col-md-3 form-check",
- div(cls := "form-check",
- _discreetIn,
- label(cls := "form-check-label",
- forId := "discreet",
- "Descrete Servings"
- )
- )
- ),
- div(cls := "col-md-3 input-group",
- _servingsIn,
- div(cls := "input-group-append",
- label(cls := "input-group-text", "Servings")
- )
- ),
- div(cls := "col-md-3 input-group",
- _massPIn,
- div(cls := "input-group-append",
- label(cls := "input-group-text", "g/Serving")
- )
- ),
- div(cls := "col-md-3 input-group",
- _volPIn,
- _volPUnitIn,
- div(cls := "input-group-append",
- label(cls := "input-group-text", "/Serving"),
- ),
- )
- ),
- div(cls := "row",
- div(cls := "col-md-5",
- h2("Ingredients"),
- _ingredientSearch,
- ul(
- listStyleType := "none",
- paddingLeft := "0",
- children <-- _ingredients.signal.splitByIndex {
- case (idx, _, ingredientStream) =>
- presentFoodNode(idx)(ingredientStream)
- }
- )
- ),
- div(cls := "col-md-5",
- h2("Steps"),
- _stepIn,
- _stepList
- ),
- div(cls := "col-md-2",
- NutritionPane(recipieNode).render
- )
- )
- ),
- button(cls := "btn",
- onClick --> { (e: Event) =>
- implicit val owner = new ManualOwner()
- // import play.api.libs.json.Json
- // println(recipieNode.value)
- // println(Json.toJson(recipieNode.value))
- // println(Json.stringify(Json.toJson(recipieNode.value)))
- FoodController.add()(recipieNode.observe.now()).onComplete {
- case Success(_) => println("Success!")
- case Failure(ex) =>
- println("Could not add recipe")
- throw ex
- }
- },
- "Add"
- )
- )
- )
- }
|