diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala index 1c3675f..8699fbe 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala @@ -1,12 +1,14 @@ package cz.e_bs.cmi.mdr.pdb.app +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + import java.time.LocalDate object ExampleData: object persons: val jmeistrova = Osoba( - "1031", + OsobniCislo("60308"), "Ing. Jana Meistrová", "jmeistrova@cmi.cz", "+420222866180", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala index 1c3675f..8699fbe 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala @@ -1,12 +1,14 @@ package cz.e_bs.cmi.mdr.pdb.app +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + import java.time.LocalDate object ExampleData: object persons: val jmeistrova = Osoba( - "1031", + OsobniCislo("60308"), "Ing. Jana Meistrová", "jmeistrova@cmi.cz", "+420222866180", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 7737b2f..c612f6f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -25,37 +25,37 @@ @JSExportTopLevel("app") object Main: - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - @JSExport - def main(args: Array[String]): Unit = { - documentEvents.onDomContentLoaded.foreach { _ => + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() val appContainer = dom.document.querySelector("#app") - given router: Router[Page] = Routes.router + val _ = + render( + appContainer, + renderPage(MockAppState(using unsafeWindowOwner, router)) + ) + } - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) ) ) + ) - val _ = render( - appContainer, - renderPage - ) - }(unsafeWindowOwner) - } - - def renderPage(using router: Router[Page]): HtmlElement = + def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( pages - .DetailPage(osc => - EventStream.fromValue(ExampleData.persons.jmeistrova).delay(1000) - )(_) + .DetailPage(state.details, state.actionBus, _) .render ) .collectStatic(Page.Dashboard)(pages.DashboardPage().render) @@ -72,20 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage(() => - EventStream - .fromValue( - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(o => JSON.stringify(o).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - ) - ) + .DirectoryPage(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala index 1c3675f..8699fbe 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala @@ -1,12 +1,14 @@ package cz.e_bs.cmi.mdr.pdb.app +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + import java.time.LocalDate object ExampleData: object persons: val jmeistrova = Osoba( - "1031", + OsobniCislo("60308"), "Ing. Jana Meistrová", "jmeistrova@cmi.cz", "+420222866180", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 7737b2f..c612f6f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -25,37 +25,37 @@ @JSExportTopLevel("app") object Main: - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - @JSExport - def main(args: Array[String]): Unit = { - documentEvents.onDomContentLoaded.foreach { _ => + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() val appContainer = dom.document.querySelector("#app") - given router: Router[Page] = Routes.router + val _ = + render( + appContainer, + renderPage(MockAppState(using unsafeWindowOwner, router)) + ) + } - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) ) ) + ) - val _ = render( - appContainer, - renderPage - ) - }(unsafeWindowOwner) - } - - def renderPage(using router: Router[Page]): HtmlElement = + def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( pages - .DetailPage(osc => - EventStream.fromValue(ExampleData.persons.jmeistrova).delay(1000) - )(_) + .DetailPage(state.details, state.actionBus, _) .render ) .collectStatic(Page.Dashboard)(pages.DashboardPage().render) @@ -72,20 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage(() => - EventStream - .fromValue( - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(o => JSON.stringify(o).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - ) - ) + .DirectoryPage(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 17901a2..24702b8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -2,6 +2,7 @@ import java.time.LocalDate import java.time.Instant +import cz.e_bs.cmi.mdr.pdb.OsobniCislo case class Potvrzeni( uzivatel: String, @@ -39,7 +40,7 @@ ) case class Osoba( - osobniCislo: String, + osobniCislo: OsobniCislo, jmeno: String, email: String, telefon: String, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala index 1c3675f..8699fbe 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala @@ -1,12 +1,14 @@ package cz.e_bs.cmi.mdr.pdb.app +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + import java.time.LocalDate object ExampleData: object persons: val jmeistrova = Osoba( - "1031", + OsobniCislo("60308"), "Ing. Jana Meistrová", "jmeistrova@cmi.cz", "+420222866180", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 7737b2f..c612f6f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -25,37 +25,37 @@ @JSExportTopLevel("app") object Main: - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - @JSExport - def main(args: Array[String]): Unit = { - documentEvents.onDomContentLoaded.foreach { _ => + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() val appContainer = dom.document.querySelector("#app") - given router: Router[Page] = Routes.router + val _ = + render( + appContainer, + renderPage(MockAppState(using unsafeWindowOwner, router)) + ) + } - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) ) ) + ) - val _ = render( - appContainer, - renderPage - ) - }(unsafeWindowOwner) - } - - def renderPage(using router: Router[Page]): HtmlElement = + def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( pages - .DetailPage(osc => - EventStream.fromValue(ExampleData.persons.jmeistrova).delay(1000) - )(_) + .DetailPage(state.details, state.actionBus, _) .render ) .collectStatic(Page.Dashboard)(pages.DashboardPage().render) @@ -72,20 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage(() => - EventStream - .fromValue( - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(o => JSON.stringify(o).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - ) - ) + .DirectoryPage(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 17901a2..24702b8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -2,6 +2,7 @@ import java.time.LocalDate import java.time.Instant +import cz.e_bs.cmi.mdr.pdb.OsobniCislo case class Potvrzeni( uzivatel: String, @@ -39,7 +40,7 @@ ) case class Osoba( - osobniCislo: String, + osobniCislo: OsobniCislo, jmeno: String, email: String, telefon: String, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 636d058..55cb1f0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.* import org.scalajs.dom import zio.json.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js @@ -16,7 +17,7 @@ case object Dashboard extends Page("Dashboard", Some(Directory)) - case class Detail(osobniCislo: String, jmenoOsoby: Option[String] = None) + case class Detail(osobniCislo: OsobniCislo, jmenoOsoby: Option[String] = None) extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { @@ -24,7 +25,7 @@ } case class DetailParametru( - osobniCislo: String, + osobniCislo: OsobniCislo, idParametru: String, jmenoOsoby: Option[String] = None, nazevParametru: Option[String] = None @@ -46,6 +47,8 @@ ) extends Page("Unexpected error", Some(Directory)) object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] @@ -56,7 +59,7 @@ val homePage: Page = Page.Directory - val router = Router[Page]( + given router: Router[Page] = Router[Page]( routes = List( Route.static(homePage, root / endOfSegments, basePath = base), Route.static( @@ -65,14 +68,14 @@ basePath = base ), Route[Page.Detail, String]( - encode = _.osobniCislo, - decode = Page.Detail(_), + encode = _.osobniCislo.toString, + decode = osc => Page.Detail(OsobniCislo(osc)), root / "osoba" / segment[String] / endOfSegments, basePath = base ), Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo, p.idParametru), - decode = p => Page.DetailParametru(p._1, p._2), + encode = p => (p.osobniCislo.toString, p.idParametru), + decode = p => Page.DetailParametru(OsobniCislo(p._1), p._2), root / "osoba" / segment[String] / "parametr" / segment[ String ] / endOfSegments, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala index 1c3675f..8699fbe 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala @@ -1,12 +1,14 @@ package cz.e_bs.cmi.mdr.pdb.app +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + import java.time.LocalDate object ExampleData: object persons: val jmeistrova = Osoba( - "1031", + OsobniCislo("60308"), "Ing. Jana Meistrová", "jmeistrova@cmi.cz", "+420222866180", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 7737b2f..c612f6f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -25,37 +25,37 @@ @JSExportTopLevel("app") object Main: - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - @JSExport - def main(args: Array[String]): Unit = { - documentEvents.onDomContentLoaded.foreach { _ => + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() val appContainer = dom.document.querySelector("#app") - given router: Router[Page] = Routes.router + val _ = + render( + appContainer, + renderPage(MockAppState(using unsafeWindowOwner, router)) + ) + } - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) ) ) + ) - val _ = render( - appContainer, - renderPage - ) - }(unsafeWindowOwner) - } - - def renderPage(using router: Router[Page]): HtmlElement = + def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( pages - .DetailPage(osc => - EventStream.fromValue(ExampleData.persons.jmeistrova).delay(1000) - )(_) + .DetailPage(state.details, state.actionBus, _) .render ) .collectStatic(Page.Dashboard)(pages.DashboardPage().render) @@ -72,20 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage(() => - EventStream - .fromValue( - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(o => JSON.stringify(o).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - ) - ) + .DirectoryPage(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 17901a2..24702b8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -2,6 +2,7 @@ import java.time.LocalDate import java.time.Instant +import cz.e_bs.cmi.mdr.pdb.OsobniCislo case class Potvrzeni( uzivatel: String, @@ -39,7 +40,7 @@ ) case class Osoba( - osobniCislo: String, + osobniCislo: OsobniCislo, jmeno: String, email: String, telefon: String, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 636d058..55cb1f0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.* import org.scalajs.dom import zio.json.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js @@ -16,7 +17,7 @@ case object Dashboard extends Page("Dashboard", Some(Directory)) - case class Detail(osobniCislo: String, jmenoOsoby: Option[String] = None) + case class Detail(osobniCislo: OsobniCislo, jmenoOsoby: Option[String] = None) extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { @@ -24,7 +25,7 @@ } case class DetailParametru( - osobniCislo: String, + osobniCislo: OsobniCislo, idParametru: String, jmenoOsoby: Option[String] = None, nazevParametru: Option[String] = None @@ -46,6 +47,8 @@ ) extends Page("Unexpected error", Some(Directory)) object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] @@ -56,7 +59,7 @@ val homePage: Page = Page.Directory - val router = Router[Page]( + given router: Router[Page] = Router[Page]( routes = List( Route.static(homePage, root / endOfSegments, basePath = base), Route.static( @@ -65,14 +68,14 @@ basePath = base ), Route[Page.Detail, String]( - encode = _.osobniCislo, - decode = Page.Detail(_), + encode = _.osobniCislo.toString, + decode = osc => Page.Detail(OsobniCislo(osc)), root / "osoba" / segment[String] / endOfSegments, basePath = base ), Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo, p.idParametru), - decode = p => Page.DetailParametru(p._1, p._2), + encode = p => (p.osobniCislo.toString, p.idParametru), + decode = p => Page.DetailParametru(OsobniCislo(p._1), p._2), root / "osoba" / segment[String] / "parametr" / segment[ String ] / endOfSegments, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d80418d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -0,0 +1,9 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala index 1c3675f..8699fbe 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala @@ -1,12 +1,14 @@ package cz.e_bs.cmi.mdr.pdb.app +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + import java.time.LocalDate object ExampleData: object persons: val jmeistrova = Osoba( - "1031", + OsobniCislo("60308"), "Ing. Jana Meistrová", "jmeistrova@cmi.cz", "+420222866180", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 7737b2f..c612f6f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -25,37 +25,37 @@ @JSExportTopLevel("app") object Main: - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - @JSExport - def main(args: Array[String]): Unit = { - documentEvents.onDomContentLoaded.foreach { _ => + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() val appContainer = dom.document.querySelector("#app") - given router: Router[Page] = Routes.router + val _ = + render( + appContainer, + renderPage(MockAppState(using unsafeWindowOwner, router)) + ) + } - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) ) ) + ) - val _ = render( - appContainer, - renderPage - ) - }(unsafeWindowOwner) - } - - def renderPage(using router: Router[Page]): HtmlElement = + def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( pages - .DetailPage(osc => - EventStream.fromValue(ExampleData.persons.jmeistrova).delay(1000) - )(_) + .DetailPage(state.details, state.actionBus, _) .render ) .collectStatic(Page.Dashboard)(pages.DashboardPage().render) @@ -72,20 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage(() => - EventStream - .fromValue( - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(o => JSON.stringify(o).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - ) - ) + .DirectoryPage(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 17901a2..24702b8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -2,6 +2,7 @@ import java.time.LocalDate import java.time.Instant +import cz.e_bs.cmi.mdr.pdb.OsobniCislo case class Potvrzeni( uzivatel: String, @@ -39,7 +40,7 @@ ) case class Osoba( - osobniCislo: String, + osobniCislo: OsobniCislo, jmeno: String, email: String, telefon: String, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 636d058..55cb1f0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.* import org.scalajs.dom import zio.json.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js @@ -16,7 +17,7 @@ case object Dashboard extends Page("Dashboard", Some(Directory)) - case class Detail(osobniCislo: String, jmenoOsoby: Option[String] = None) + case class Detail(osobniCislo: OsobniCislo, jmenoOsoby: Option[String] = None) extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { @@ -24,7 +25,7 @@ } case class DetailParametru( - osobniCislo: String, + osobniCislo: OsobniCislo, idParametru: String, jmenoOsoby: Option[String] = None, nazevParametru: Option[String] = None @@ -46,6 +47,8 @@ ) extends Page("Unexpected error", Some(Directory)) object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] @@ -56,7 +59,7 @@ val homePage: Page = Page.Directory - val router = Router[Page]( + given router: Router[Page] = Router[Page]( routes = List( Route.static(homePage, root / endOfSegments, basePath = base), Route.static( @@ -65,14 +68,14 @@ basePath = base ), Route[Page.Detail, String]( - encode = _.osobniCislo, - decode = Page.Detail(_), + encode = _.osobniCislo.toString, + decode = osc => Page.Detail(OsobniCislo(osc)), root / "osoba" / segment[String] / endOfSegments, basePath = base ), Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo, p.idParametru), - decode = p => Page.DetailParametru(p._1, p._2), + encode = p => (p.osobniCislo.toString, p.idParametru), + decode = p => Page.DetailParametru(OsobniCislo(p._1), p._2), root / "osoba" / segment[String] / "parametr" / segment[ String ] / endOfSegments, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d80418d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -0,0 +1,9 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala index 43f16c9..ab68c35 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo} +import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator trait AppPage @@ -34,7 +34,7 @@ UserProfile( "tom", UserInfo( - "1031", + OsobniCislo("1031"), "tom", "Tom", "Cook", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala index 1c3675f..8699fbe 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala @@ -1,12 +1,14 @@ package cz.e_bs.cmi.mdr.pdb.app +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + import java.time.LocalDate object ExampleData: object persons: val jmeistrova = Osoba( - "1031", + OsobniCislo("60308"), "Ing. Jana Meistrová", "jmeistrova@cmi.cz", "+420222866180", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 7737b2f..c612f6f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -25,37 +25,37 @@ @JSExportTopLevel("app") object Main: - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - @JSExport - def main(args: Array[String]): Unit = { - documentEvents.onDomContentLoaded.foreach { _ => + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() val appContainer = dom.document.querySelector("#app") - given router: Router[Page] = Routes.router + val _ = + render( + appContainer, + renderPage(MockAppState(using unsafeWindowOwner, router)) + ) + } - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) ) ) + ) - val _ = render( - appContainer, - renderPage - ) - }(unsafeWindowOwner) - } - - def renderPage(using router: Router[Page]): HtmlElement = + def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( pages - .DetailPage(osc => - EventStream.fromValue(ExampleData.persons.jmeistrova).delay(1000) - )(_) + .DetailPage(state.details, state.actionBus, _) .render ) .collectStatic(Page.Dashboard)(pages.DashboardPage().render) @@ -72,20 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage(() => - EventStream - .fromValue( - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(o => JSON.stringify(o).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - ) - ) + .DirectoryPage(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 17901a2..24702b8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -2,6 +2,7 @@ import java.time.LocalDate import java.time.Instant +import cz.e_bs.cmi.mdr.pdb.OsobniCislo case class Potvrzeni( uzivatel: String, @@ -39,7 +40,7 @@ ) case class Osoba( - osobniCislo: String, + osobniCislo: OsobniCislo, jmeno: String, email: String, telefon: String, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 636d058..55cb1f0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.* import org.scalajs.dom import zio.json.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js @@ -16,7 +17,7 @@ case object Dashboard extends Page("Dashboard", Some(Directory)) - case class Detail(osobniCislo: String, jmenoOsoby: Option[String] = None) + case class Detail(osobniCislo: OsobniCislo, jmenoOsoby: Option[String] = None) extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { @@ -24,7 +25,7 @@ } case class DetailParametru( - osobniCislo: String, + osobniCislo: OsobniCislo, idParametru: String, jmenoOsoby: Option[String] = None, nazevParametru: Option[String] = None @@ -46,6 +47,8 @@ ) extends Page("Unexpected error", Some(Directory)) object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] @@ -56,7 +59,7 @@ val homePage: Page = Page.Directory - val router = Router[Page]( + given router: Router[Page] = Router[Page]( routes = List( Route.static(homePage, root / endOfSegments, basePath = base), Route.static( @@ -65,14 +68,14 @@ basePath = base ), Route[Page.Detail, String]( - encode = _.osobniCislo, - decode = Page.Detail(_), + encode = _.osobniCislo.toString, + decode = osc => Page.Detail(OsobniCislo(osc)), root / "osoba" / segment[String] / endOfSegments, basePath = base ), Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo, p.idParametru), - decode = p => Page.DetailParametru(p._1, p._2), + encode = p => (p.osobniCislo.toString, p.idParametru), + decode = p => Page.DetailParametru(OsobniCislo(p._1), p._2), root / "osoba" / segment[String] / "parametr" / segment[ String ] / endOfSegments, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d80418d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -0,0 +1,9 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala index 43f16c9..ab68c35 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo} +import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator trait AppPage @@ -34,7 +34,7 @@ UserProfile( "tom", UserInfo( - "1031", + OsobniCislo("1031"), "tom", "Tom", "Cook", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala index 69bc8c6..0f54b7a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala @@ -16,22 +16,30 @@ import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails -case class DetailPage(fetch: String => EventStream[Osoba])( +case class DetailPage( + $input: EventStream[Osoba], + actionBus: Observer[Action], $page: Signal[Page.Detail] )(using router: Router[Page]) extends AppPage: override def pageContent: HtmlElement = - val data = Var[Option[Osoba]](None) + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone val $maybeOsoba = - data.signal.split(_ => ())((_, _, s) => renderView(s)) - val $fetchedData = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - .flatMap(fetch) - .debugLog() + $data.split(_ => ())((_, _, s) => renderView(s)) + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $fetchedData --> data.writer.contramapSome, - $fetchedData --> (o => router.replaceState(Page.Detail(o))), + $pageChangeSignal --> actionBus, + // $fetchedData --> (o => router.replaceState(Page.Detail(o))), child <-- $maybeOsoba.map(_.getOrElse(Loading)) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala index 1c3675f..8699fbe 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala @@ -1,12 +1,14 @@ package cz.e_bs.cmi.mdr.pdb.app +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + import java.time.LocalDate object ExampleData: object persons: val jmeistrova = Osoba( - "1031", + OsobniCislo("60308"), "Ing. Jana Meistrová", "jmeistrova@cmi.cz", "+420222866180", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 7737b2f..c612f6f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -25,37 +25,37 @@ @JSExportTopLevel("app") object Main: - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - @JSExport - def main(args: Array[String]): Unit = { - documentEvents.onDomContentLoaded.foreach { _ => + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() val appContainer = dom.document.querySelector("#app") - given router: Router[Page] = Routes.router + val _ = + render( + appContainer, + renderPage(MockAppState(using unsafeWindowOwner, router)) + ) + } - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) ) ) + ) - val _ = render( - appContainer, - renderPage - ) - }(unsafeWindowOwner) - } - - def renderPage(using router: Router[Page]): HtmlElement = + def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( pages - .DetailPage(osc => - EventStream.fromValue(ExampleData.persons.jmeistrova).delay(1000) - )(_) + .DetailPage(state.details, state.actionBus, _) .render ) .collectStatic(Page.Dashboard)(pages.DashboardPage().render) @@ -72,20 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage(() => - EventStream - .fromValue( - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(o => JSON.stringify(o).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - ) - ) + .DirectoryPage(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 17901a2..24702b8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -2,6 +2,7 @@ import java.time.LocalDate import java.time.Instant +import cz.e_bs.cmi.mdr.pdb.OsobniCislo case class Potvrzeni( uzivatel: String, @@ -39,7 +40,7 @@ ) case class Osoba( - osobniCislo: String, + osobniCislo: OsobniCislo, jmeno: String, email: String, telefon: String, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 636d058..55cb1f0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.* import org.scalajs.dom import zio.json.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js @@ -16,7 +17,7 @@ case object Dashboard extends Page("Dashboard", Some(Directory)) - case class Detail(osobniCislo: String, jmenoOsoby: Option[String] = None) + case class Detail(osobniCislo: OsobniCislo, jmenoOsoby: Option[String] = None) extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { @@ -24,7 +25,7 @@ } case class DetailParametru( - osobniCislo: String, + osobniCislo: OsobniCislo, idParametru: String, jmenoOsoby: Option[String] = None, nazevParametru: Option[String] = None @@ -46,6 +47,8 @@ ) extends Page("Unexpected error", Some(Directory)) object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] @@ -56,7 +59,7 @@ val homePage: Page = Page.Directory - val router = Router[Page]( + given router: Router[Page] = Router[Page]( routes = List( Route.static(homePage, root / endOfSegments, basePath = base), Route.static( @@ -65,14 +68,14 @@ basePath = base ), Route[Page.Detail, String]( - encode = _.osobniCislo, - decode = Page.Detail(_), + encode = _.osobniCislo.toString, + decode = osc => Page.Detail(OsobniCislo(osc)), root / "osoba" / segment[String] / endOfSegments, basePath = base ), Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo, p.idParametru), - decode = p => Page.DetailParametru(p._1, p._2), + encode = p => (p.osobniCislo.toString, p.idParametru), + decode = p => Page.DetailParametru(OsobniCislo(p._1), p._2), root / "osoba" / segment[String] / "parametr" / segment[ String ] / endOfSegments, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d80418d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -0,0 +1,9 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala index 43f16c9..ab68c35 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo} +import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator trait AppPage @@ -34,7 +34,7 @@ UserProfile( "tom", UserInfo( - "1031", + OsobniCislo("1031"), "tom", "Tom", "Cook", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala index 69bc8c6..0f54b7a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala @@ -16,22 +16,30 @@ import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails -case class DetailPage(fetch: String => EventStream[Osoba])( +case class DetailPage( + $input: EventStream[Osoba], + actionBus: Observer[Action], $page: Signal[Page.Detail] )(using router: Router[Page]) extends AppPage: override def pageContent: HtmlElement = - val data = Var[Option[Osoba]](None) + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone val $maybeOsoba = - data.signal.split(_ => ())((_, _, s) => renderView(s)) - val $fetchedData = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - .flatMap(fetch) - .debugLog() + $data.split(_ => ())((_, _, s) => renderView(s)) + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $fetchedData --> data.writer.contramapSome, - $fetchedData --> (o => router.replaceState(Page.Detail(o))), + $pageChangeSignal --> actionBus, + // $fetchedData --> (o => router.replaceState(Page.Detail(o))), child <-- $maybeOsoba.map(_.getOrElse(Loading)) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala index 66067e3..fc3fe2d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala @@ -7,20 +7,27 @@ import cz.e_bs.cmi.mdr.pdb.app.components.{AppPage, Loading} import cz.e_bs.cmi.mdr.pdb.UserInfo import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory -case class DirectoryPage(fetch: () => EventStream[List[UserInfo]])(using +case class DirectoryPage( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page] ) extends AppPage: override def pageContent: HtmlElement = - val data = Var[Option[List[UserInfo]]](None) + val data = $input.startWithNone val $maybeDirectory = data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) + val $actionSignal = EventStream.fromValue(FetchDirectory) div( cls := "max-w-7xl mx-auto", //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", searchForm, - fetch().delay(1000) --> data.writer.contramapSome, + $actionSignal --> actionBus, + // fetch().delay(1000) --> data.writer.contramapSome, child <-- $maybeDirectory.map(_.getOrElse(Loading)) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala index 1c3675f..8699fbe 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala @@ -1,12 +1,14 @@ package cz.e_bs.cmi.mdr.pdb.app +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + import java.time.LocalDate object ExampleData: object persons: val jmeistrova = Osoba( - "1031", + OsobniCislo("60308"), "Ing. Jana Meistrová", "jmeistrova@cmi.cz", "+420222866180", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 7737b2f..c612f6f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -25,37 +25,37 @@ @JSExportTopLevel("app") object Main: - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - @JSExport - def main(args: Array[String]): Unit = { - documentEvents.onDomContentLoaded.foreach { _ => + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() val appContainer = dom.document.querySelector("#app") - given router: Router[Page] = Routes.router + val _ = + render( + appContainer, + renderPage(MockAppState(using unsafeWindowOwner, router)) + ) + } - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) ) ) + ) - val _ = render( - appContainer, - renderPage - ) - }(unsafeWindowOwner) - } - - def renderPage(using router: Router[Page]): HtmlElement = + def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( pages - .DetailPage(osc => - EventStream.fromValue(ExampleData.persons.jmeistrova).delay(1000) - )(_) + .DetailPage(state.details, state.actionBus, _) .render ) .collectStatic(Page.Dashboard)(pages.DashboardPage().render) @@ -72,20 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage(() => - EventStream - .fromValue( - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(o => JSON.stringify(o).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - ) - ) + .DirectoryPage(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 17901a2..24702b8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -2,6 +2,7 @@ import java.time.LocalDate import java.time.Instant +import cz.e_bs.cmi.mdr.pdb.OsobniCislo case class Potvrzeni( uzivatel: String, @@ -39,7 +40,7 @@ ) case class Osoba( - osobniCislo: String, + osobniCislo: OsobniCislo, jmeno: String, email: String, telefon: String, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 636d058..55cb1f0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.* import org.scalajs.dom import zio.json.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js @@ -16,7 +17,7 @@ case object Dashboard extends Page("Dashboard", Some(Directory)) - case class Detail(osobniCislo: String, jmenoOsoby: Option[String] = None) + case class Detail(osobniCislo: OsobniCislo, jmenoOsoby: Option[String] = None) extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { @@ -24,7 +25,7 @@ } case class DetailParametru( - osobniCislo: String, + osobniCislo: OsobniCislo, idParametru: String, jmenoOsoby: Option[String] = None, nazevParametru: Option[String] = None @@ -46,6 +47,8 @@ ) extends Page("Unexpected error", Some(Directory)) object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] @@ -56,7 +59,7 @@ val homePage: Page = Page.Directory - val router = Router[Page]( + given router: Router[Page] = Router[Page]( routes = List( Route.static(homePage, root / endOfSegments, basePath = base), Route.static( @@ -65,14 +68,14 @@ basePath = base ), Route[Page.Detail, String]( - encode = _.osobniCislo, - decode = Page.Detail(_), + encode = _.osobniCislo.toString, + decode = osc => Page.Detail(OsobniCislo(osc)), root / "osoba" / segment[String] / endOfSegments, basePath = base ), Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo, p.idParametru), - decode = p => Page.DetailParametru(p._1, p._2), + encode = p => (p.osobniCislo.toString, p.idParametru), + decode = p => Page.DetailParametru(OsobniCislo(p._1), p._2), root / "osoba" / segment[String] / "parametr" / segment[ String ] / endOfSegments, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d80418d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -0,0 +1,9 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala index 43f16c9..ab68c35 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo} +import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator trait AppPage @@ -34,7 +34,7 @@ UserProfile( "tom", UserInfo( - "1031", + OsobniCislo("1031"), "tom", "Tom", "Cook", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala index 69bc8c6..0f54b7a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala @@ -16,22 +16,30 @@ import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails -case class DetailPage(fetch: String => EventStream[Osoba])( +case class DetailPage( + $input: EventStream[Osoba], + actionBus: Observer[Action], $page: Signal[Page.Detail] )(using router: Router[Page]) extends AppPage: override def pageContent: HtmlElement = - val data = Var[Option[Osoba]](None) + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone val $maybeOsoba = - data.signal.split(_ => ())((_, _, s) => renderView(s)) - val $fetchedData = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - .flatMap(fetch) - .debugLog() + $data.split(_ => ())((_, _, s) => renderView(s)) + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $fetchedData --> data.writer.contramapSome, - $fetchedData --> (o => router.replaceState(Page.Detail(o))), + $pageChangeSignal --> actionBus, + // $fetchedData --> (o => router.replaceState(Page.Detail(o))), child <-- $maybeOsoba.map(_.getOrElse(Loading)) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala index 66067e3..fc3fe2d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala @@ -7,20 +7,27 @@ import cz.e_bs.cmi.mdr.pdb.app.components.{AppPage, Loading} import cz.e_bs.cmi.mdr.pdb.UserInfo import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory -case class DirectoryPage(fetch: () => EventStream[List[UserInfo]])(using +case class DirectoryPage( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page] ) extends AppPage: override def pageContent: HtmlElement = - val data = Var[Option[List[UserInfo]]](None) + val data = $input.startWithNone val $maybeDirectory = data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) + val $actionSignal = EventStream.fromValue(FetchDirectory) div( cls := "max-w-7xl mx-auto", //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", searchForm, - fetch().delay(1000) --> data.writer.contramapSome, + $actionSignal --> actionBus, + // fetch().delay(1000) --> data.writer.contramapSome, child <-- $maybeDirectory.map(_.getOrElse(Loading)) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/waypoint/components/Navigator.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/waypoint/components/Navigator.scala index 1eed887..2fc3d8e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/waypoint/components/Navigator.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/waypoint/components/Navigator.scala @@ -7,6 +7,7 @@ trait Navigator[P](using router: Router[P]): def navigateTo(page: P): Binder[HtmlElement] = Navigator.navigateTo[P](page) +// TODO: replace router NavigateTo action object Navigator { def navigateTo[P](page: P)(using router: Router[P]): Binder[HtmlElement] = Binder { el => diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala new file mode 100644 index 0000000..582cff9 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -0,0 +1,54 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import com.raquo.airstream.core.EventStream +import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router + +trait AppState: + def users: EventStream[List[UserInfo]] + def details: EventStream[Osoba] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + + private val mockData = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map(o => JSON.stringify(o).fromJson[UserInfo]) + .collect { case Right(u) => + u + } + .toList + + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) + pushDetails(o) + router.replaceState(Page.Detail(o)) + case NavigateTo(page) => router.pushState(page) + } + + override def users = usersStream.debugWithName("users") + + override def details = detailsStream.debugWithName("details") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala index 1c3675f..8699fbe 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala @@ -1,12 +1,14 @@ package cz.e_bs.cmi.mdr.pdb.app +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + import java.time.LocalDate object ExampleData: object persons: val jmeistrova = Osoba( - "1031", + OsobniCislo("60308"), "Ing. Jana Meistrová", "jmeistrova@cmi.cz", "+420222866180", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala index 7737b2f..c612f6f 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala @@ -25,37 +25,37 @@ @JSExportTopLevel("app") object Main: - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - @JSExport - def main(args: Array[String]): Unit = { - documentEvents.onDomContentLoaded.foreach { _ => + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() val appContainer = dom.document.querySelector("#app") - given router: Router[Page] = Routes.router + val _ = + render( + appContainer, + renderPage(MockAppState(using unsafeWindowOwner, router)) + ) + } - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) ) ) + ) - val _ = render( - appContainer, - renderPage - ) - }(unsafeWindowOwner) - } - - def renderPage(using router: Router[Page]): HtmlElement = + def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( pages - .DetailPage(osc => - EventStream.fromValue(ExampleData.persons.jmeistrova).delay(1000) - )(_) + .DetailPage(state.details, state.actionBus, _) .render ) .collectStatic(Page.Dashboard)(pages.DashboardPage().render) @@ -72,20 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage(() => - EventStream - .fromValue( - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(o => JSON.stringify(o).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - ) - ) + .DirectoryPage(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 17901a2..24702b8 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -2,6 +2,7 @@ import java.time.LocalDate import java.time.Instant +import cz.e_bs.cmi.mdr.pdb.OsobniCislo case class Potvrzeni( uzivatel: String, @@ -39,7 +40,7 @@ ) case class Osoba( - osobniCislo: String, + osobniCislo: OsobniCislo, jmeno: String, email: String, telefon: String, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 636d058..55cb1f0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -4,6 +4,7 @@ import com.raquo.waypoint.* import org.scalajs.dom import zio.json.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js @@ -16,7 +17,7 @@ case object Dashboard extends Page("Dashboard", Some(Directory)) - case class Detail(osobniCislo: String, jmenoOsoby: Option[String] = None) + case class Detail(osobniCislo: OsobniCislo, jmenoOsoby: Option[String] = None) extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { @@ -24,7 +25,7 @@ } case class DetailParametru( - osobniCislo: String, + osobniCislo: OsobniCislo, idParametru: String, jmenoOsoby: Option[String] = None, nazevParametru: Option[String] = None @@ -46,6 +47,8 @@ ) extends Page("Unexpected error", Some(Directory)) object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] @@ -56,7 +59,7 @@ val homePage: Page = Page.Directory - val router = Router[Page]( + given router: Router[Page] = Router[Page]( routes = List( Route.static(homePage, root / endOfSegments, basePath = base), Route.static( @@ -65,14 +68,14 @@ basePath = base ), Route[Page.Detail, String]( - encode = _.osobniCislo, - decode = Page.Detail(_), + encode = _.osobniCislo.toString, + decode = osc => Page.Detail(OsobniCislo(osc)), root / "osoba" / segment[String] / endOfSegments, basePath = base ), Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo, p.idParametru), - decode = p => Page.DetailParametru(p._1, p._2), + encode = p => (p.osobniCislo.toString, p.idParametru), + decode = p => Page.DetailParametru(OsobniCislo(p._1), p._2), root / "osoba" / segment[String] / "parametr" / segment[ String ] / endOfSegments, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d80418d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala @@ -0,0 +1,9 @@ +package cz.e_bs.cmi.mdr.pdb.app + +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala index 43f16c9..ab68c35 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo} +import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator trait AppPage @@ -34,7 +34,7 @@ UserProfile( "tom", UserInfo( - "1031", + OsobniCislo("1031"), "tom", "Tom", "Cook", diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala index 69bc8c6..0f54b7a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala @@ -16,22 +16,30 @@ import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails -case class DetailPage(fetch: String => EventStream[Osoba])( +case class DetailPage( + $input: EventStream[Osoba], + actionBus: Observer[Action], $page: Signal[Page.Detail] )(using router: Router[Page]) extends AppPage: override def pageContent: HtmlElement = - val data = Var[Option[Osoba]](None) + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone val $maybeOsoba = - data.signal.split(_ => ())((_, _, s) => renderView(s)) - val $fetchedData = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - .flatMap(fetch) - .debugLog() + $data.split(_ => ())((_, _, s) => renderView(s)) + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $fetchedData --> data.writer.contramapSome, - $fetchedData --> (o => router.replaceState(Page.Detail(o))), + $pageChangeSignal --> actionBus, + // $fetchedData --> (o => router.replaceState(Page.Detail(o))), child <-- $maybeOsoba.map(_.getOrElse(Loading)) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala index 66067e3..fc3fe2d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala @@ -7,20 +7,27 @@ import cz.e_bs.cmi.mdr.pdb.app.components.{AppPage, Loading} import cz.e_bs.cmi.mdr.pdb.UserInfo import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory -case class DirectoryPage(fetch: () => EventStream[List[UserInfo]])(using +case class DirectoryPage( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page] ) extends AppPage: override def pageContent: HtmlElement = - val data = Var[Option[List[UserInfo]]](None) + val data = $input.startWithNone val $maybeDirectory = data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) + val $actionSignal = EventStream.fromValue(FetchDirectory) div( cls := "max-w-7xl mx-auto", //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", searchForm, - fetch().delay(1000) --> data.writer.contramapSome, + $actionSignal --> actionBus, + // fetch().delay(1000) --> data.writer.contramapSome, child <-- $maybeDirectory.map(_.getOrElse(Loading)) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/waypoint/components/Navigator.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/waypoint/components/Navigator.scala index 1eed887..2fc3d8e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/waypoint/components/Navigator.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/waypoint/components/Navigator.scala @@ -7,6 +7,7 @@ trait Navigator[P](using router: Router[P]): def navigateTo(page: P): Binder[HtmlElement] = Navigator.navigateTo[P](page) +// TODO: replace router NavigateTo action object Navigator { def navigateTo[P](page: P)(using router: Router[P]): Binder[HtmlElement] = Binder { el => diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala index d21571d..1bdfd5d 100644 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala +++ b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala @@ -1,7 +1,15 @@ package cz.e_bs.cmi.mdr.pdb +opaque type OsobniCislo = String + +object OsobniCislo: + // TODO: validation + def apply(osc: String): OsobniCislo = osc + +extension (osc: OsobniCislo) def toString: String = osc + case class UserInfo( - personalNumber: String, + personalNumber: OsobniCislo, username: String, givenName: String, surname: String,