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" ) ) ) }