diff --git a/backend/src/braid/Main.scala b/backend/src/braid/Main.scala index a75cdfd..9e9b198 100644 --- a/backend/src/braid/Main.scala +++ b/backend/src/braid/Main.scala @@ -20,12 +20,19 @@ object Main header = "An amazing web application built with Krop" ) { - val habitRoutes = + // TODO: need idea how to store model(s) on the server side: json? + val habitRoute = Route( Request.post(Path / "habit" / Param.int), Response.ok(Entity.text) ).handle(habitId => s"Response==>HabitId: $habitId") + val habitDeleteRoute = + Route( + Request.post(Path / "habit" / "delete" / Param.int), + Response.ok(Entity.text) + ).handle(habitId => s"Response==>HabitId: $habitId") + val home = Routes.home.handle(() => html.base(name, html.home(name, BuildInfo.kropVersion)).toString @@ -45,7 +52,8 @@ object Main val application = home - .orElse(habitRoutes) + .orElse(habitRoute) + .orElse(habitDeleteRoute) .orElse(javascript) .orElse(assets) .orElse(Application.notFound) diff --git a/build.sbt b/build.sbt index d21a62b..7c2363c 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,7 @@ val kropVersion = "0.9.4" val laminarVersion = "17.2.1" val logbackVersion = "1.5.18" val munitVersion = "1.1.1" +val Borer = "1.14.0" // Run this command (build) to do everything involved in building the project commands += Command.command("build") { state => @@ -93,7 +94,13 @@ lazy val frontend = project .settings( name := """braid-frontend""", commonSettings, - libraryDependencies += "com.raquo" %%% "laminar" % laminarVersion, + libraryDependencies ++= Seq( + "com.raquo" %%% "laminar" % laminarVersion, + "io.bullet" %%% "borer-core" % Borer, + "io.bullet" %%% "borer-derivation" % Borer + ), + // JSON codec + scalaJSUseMainModuleInitializer := true ) .enablePlugins(ScalaJSPlugin, KropLayout) diff --git a/date/js/src/braid/date/Date.scala b/date/js/src/braid/date/Date.scala index 570fcf1..8afeb15 100644 --- a/date/js/src/braid/date/Date.scala +++ b/date/js/src/braid/date/Date.scala @@ -75,4 +75,5 @@ object Date { def today(): Date = { fromJsDate(new js.Date(js.Date.now())) } + } diff --git a/frontend/Controller.scala b/frontend/Controller.scala index 99e785d..9681034 100644 --- a/frontend/Controller.scala +++ b/frontend/Controller.scala @@ -4,8 +4,8 @@ import braid.Habit import braid.date.Date import com.raquo.laminar.api.L.{_, given} -class Controller(model: Var[Map[Int, Habit]]) { - def toggleCompletionDate(id: Int, date: Date): Unit = +class Controller(model: Var[Map[String, Habit]]) { + def toggleCompletionDate(id: String, date: Date): Unit = model.update(habits => habits.get(id) match { case Some(habit) => @@ -16,4 +16,11 @@ class Controller(model: Var[Map[Int, Habit]]) { case None => habits } ) + def dropSingleRow(index: String): Unit = { + model.update(habits => habits.removed(index)) + } + def sortByStreak(): Unit = { + // TODO: implement reverse sorting | Use:: inContext + model.update(habits => habits.toSeq.sortWith(_._1 > _._1).toMap) + } } diff --git a/frontend/src/braid/Braid.scala b/frontend/src/braid/Braid.scala index f911e50..0fc4b5f 100644 --- a/frontend/src/braid/Braid.scala +++ b/frontend/src/braid/Braid.scala @@ -9,14 +9,7 @@ import scala.scalajs.js object Braid { def run(mount: dom.Element): Unit = { - val model = - Var( - Map( - 1 -> Habit(1, "Work on Braid", 0, Seq()), - 2 -> Habit(2, "Exercise", 5, Seq()), - 3 -> Habit(3, "Read", 3, Seq(Date.today())) - ) - ) + val model = Habit.habitsVar val controller = Controller(model) val view = View(controller) diff --git a/frontend/src/braid/View.scala b/frontend/src/braid/View.scala index 0cc97d6..4b5cd2a 100644 --- a/frontend/src/braid/View.scala +++ b/frontend/src/braid/View.scala @@ -1,14 +1,52 @@ package braid import braid.date.Date +import braid.date.Day import braid.model.Habit +import com.raquo.airstream.web.WebStorageVar import com.raquo.laminar.api.L.{_, given} import com.raquo.laminar.api.features.unitArrows import com.raquo.laminar.nodes.ReactiveHtmlElement +import io.bullet.borer.Codec +import io.bullet.borer.Json +import io.bullet.borer.derivation.MapBasedCodecs._ +import java.nio.charset.StandardCharsets import scala.scalajs.js +import scala.util.Try -final case class Habit(id: Int, name: String, streak: Int, dates: Seq[Date]) +final case class Habit(id: String, name: String, streak: Int, dates: Seq[Date]) + +object Habit { + + val defaultValues = Map( + "1" -> Habit("1", "Work on Braid", 0, Seq()), + "2" -> Habit("2", "Exercise", 5, Seq()), + "3" -> Habit("3", "Read", 3, Seq(Date.today())) + ) + + given habitCodec: Codec[Habit] = deriveCodec + given dateCodec: Codec[Date] = deriveCodec + given dayCodec: Codec[braid.date.Day] = deriveCodec + given monthCodec: Codec[braid.date.Month] = deriveCodec + + // We can use session storage instead:: + // https://github.com/raquo/Airstream/?tab=readme-ov-file#sessionstorage + val habitsVar = WebStorageVar + .localStorage( + key = "habits", + syncOwner = None + ) + .withCodec[Map[String, Habit]]( + encode = hs => Json.encode(hs).toUtf8String, + decode = jsonStr => + Json + .decode(jsonStr.getBytes(StandardCharsets.UTF_8)) + .to[Map[String, Habit]] + .valueTry, + default = Try(defaultValues) + ) +} class View(controller: Controller) { def last7Days: Seq[Date] = { @@ -35,7 +73,7 @@ class View(controller: Controller) { ) } - def view(habits: Signal[Map[Int, Habit]]): Div = + def view(habits: Signal[Map[String, Habit]]): Div = div( className := "bg-white rounded-lg shadow-lg overflow-hidden", div( @@ -51,7 +89,12 @@ class View(controller: Controller) { ), th( className := "px-4 py-4 text-center text-sm font-semibold text-gray-700", - "Streak" + "Streak", + onClick --> { + // TODO: sort by streak + println("Sorting ...") + controller.sortByStreak() + } ), last7DaysTableHeadings, th(className := "px-4 py-4") @@ -105,8 +148,11 @@ class View(controller: Controller) { className := "px-4 py-4 text-center", button( "Delete", - onClick.flatMap(_ => FetchStream.post("/habit/" + habit.id)) --> { - responseText => println(responseText) + onClick.flatMap(_ => + FetchStream.post("/habit/delete/" + habit.id) + ) --> { responseText => + // println(responseText) || assume its a success response + controller.dropSingleRow(habit.id) }, className := "text-red-500 hover:text-red-700 transition" )