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 f070322..27987d7 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 @@ -6,7 +6,9 @@ import scala.scalajs.js import org.scalajs.dom import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* import cz.e_bs.cmi.mdr.pdb.app.components.{Navigation, Layout} +import zio.json.{*, given} import scala.scalajs.js.Date @@ -27,14 +29,34 @@ logo, userProfile.signal, pages.signal, - currentPage.signal, userMenu.signal, appElement - ) + )(using router) ) }(unsafeWindowOwner) } + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = "/mdr" + + val router = Router[Page]( + routes = List( + Route.static(Page.Dashboard, root / "dashboard", basePath = base), + Route.static(Page.Detail, root / "detail", basePath = base) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = _ => Page.Dashboard, + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) + val $time = EventStream.periodic(1000).mapTo(new Date().toTimeString) def appElement: Div = div( 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 f070322..27987d7 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 @@ -6,7 +6,9 @@ import scala.scalajs.js import org.scalajs.dom import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* import cz.e_bs.cmi.mdr.pdb.app.components.{Navigation, Layout} +import zio.json.{*, given} import scala.scalajs.js.Date @@ -27,14 +29,34 @@ logo, userProfile.signal, pages.signal, - currentPage.signal, userMenu.signal, appElement - ) + )(using router) ) }(unsafeWindowOwner) } + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = "/mdr" + + val router = Router[Page]( + routes = List( + Route.static(Page.Dashboard, root / "dashboard", basePath = base), + Route.static(Page.Detail, root / "detail", basePath = base) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = _ => Page.Dashboard, + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) + val $time = EventStream.periodic(1000).mapTo(new Date().toTimeString) def appElement: Div = div( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala index 5322770..48c2724 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala @@ -3,6 +3,7 @@ import com.raquo.domtypes.generic.codecs.StringAsIsCodec import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.{Page, UserProfile} +import com.raquo.waypoint.Router def PageHeader(currentPage: Signal[Page]): HtmlElement = header( @@ -36,19 +37,17 @@ logo: Navigation.Logo, profile: Signal[UserProfile], pages: Signal[List[Page]], - currentPage: Signal[Page], userMenu: Signal[List[Navigation.MenuItem]], content: HtmlElement -): HtmlElement = +)(using router: Router[Page]): HtmlElement = div( cls := "min-h-full", Navigation( logo, profile, pages, - currentPage, userMenu ), - PageHeader(currentPage), + PageHeader(router.$currentPage), MainSection(content) ) 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 f070322..27987d7 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 @@ -6,7 +6,9 @@ import scala.scalajs.js import org.scalajs.dom import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* import cz.e_bs.cmi.mdr.pdb.app.components.{Navigation, Layout} +import zio.json.{*, given} import scala.scalajs.js.Date @@ -27,14 +29,34 @@ logo, userProfile.signal, pages.signal, - currentPage.signal, userMenu.signal, appElement - ) + )(using router) ) }(unsafeWindowOwner) } + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = "/mdr" + + val router = Router[Page]( + routes = List( + Route.static(Page.Dashboard, root / "dashboard", basePath = base), + Route.static(Page.Detail, root / "detail", basePath = base) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = _ => Page.Dashboard, + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) + val $time = EventStream.periodic(1000).mapTo(new Date().toTimeString) def appElement: Div = div( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala index 5322770..48c2724 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala @@ -3,6 +3,7 @@ import com.raquo.domtypes.generic.codecs.StringAsIsCodec import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.{Page, UserProfile} +import com.raquo.waypoint.Router def PageHeader(currentPage: Signal[Page]): HtmlElement = header( @@ -36,19 +37,17 @@ logo: Navigation.Logo, profile: Signal[UserProfile], pages: Signal[List[Page]], - currentPage: Signal[Page], userMenu: Signal[List[Navigation.MenuItem]], content: HtmlElement -): HtmlElement = +)(using router: Router[Page]): HtmlElement = div( cls := "min-h-full", Navigation( logo, profile, pages, - currentPage, userMenu ), - PageHeader(currentPage), + PageHeader(router.$currentPage), MainSection(content) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala index 9f5ffa4..29b0e90 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala @@ -3,6 +3,29 @@ import com.raquo.domtypes.generic.codecs.StringAsIsCodec import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.{Page, UserProfile} +import com.raquo.waypoint.Router +import org.scalajs.dom + +def navigateTo(page: Page)(using router: Router[Page]): Binder[HtmlElement] = + Binder { el => + + val isLinkElement = el.ref.isInstanceOf[dom.html.Anchor] + + if (isLinkElement) { + el.amend(href(router.absoluteUrlForPage(page))) + } + + // If element is a link and user is holding a modifier while clicking: + // - Do nothing, browser will open the URL in new tab / window / etc. depending on the modifier key + // Otherwise: + // - Perform regular pushState transition + (onClick + .filter(ev => + !(isLinkElement && (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) + ) + .preventDefault + --> (_ => router.pushState(page))).bind(el) + } object Navigation: @@ -18,9 +41,8 @@ logo: Logo, profile: Signal[UserProfile], pages: Signal[List[Page]], - activePage: Signal[Page], userMenu: Signal[List[MenuItem]] -): +)(using router: Router[Page]): val mobileMenuOpen = Var(false) // Made a pull request to add aria-current to scala-dom-types, remove after @@ -145,9 +167,12 @@ ) ) - private def pageLink(page: Page, active: Signal[Boolean]): Anchor = + private def pageLink(page: Page, active: Signal[Boolean])(using + router: Router[Page] + ): Anchor = a( - href := "#", + href := router.absoluteUrlForPage(page), + navigateTo(page), cls <-- active.map { case true => "bg-indigo-700" case false => "hover:bg-indigo-500 hover:bg-opacity-75" @@ -168,7 +193,7 @@ ) private def pageLinks(mods: Modifier[HtmlElement]*) = pages.map( - _.map(p => pageLink(p, activePage.map(p == _)).amend(mods)) + _.map(p => pageLink(p, router.$currentPage.map(p == _)).amend(mods)) ) private def mobileMenuButton = button( 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 f070322..27987d7 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 @@ -6,7 +6,9 @@ import scala.scalajs.js import org.scalajs.dom import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* import cz.e_bs.cmi.mdr.pdb.app.components.{Navigation, Layout} +import zio.json.{*, given} import scala.scalajs.js.Date @@ -27,14 +29,34 @@ logo, userProfile.signal, pages.signal, - currentPage.signal, userMenu.signal, appElement - ) + )(using router) ) }(unsafeWindowOwner) } + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = "/mdr" + + val router = Router[Page]( + routes = List( + Route.static(Page.Dashboard, root / "dashboard", basePath = base), + Route.static(Page.Detail, root / "detail", basePath = base) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = _ => Page.Dashboard, + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) + val $time = EventStream.periodic(1000).mapTo(new Date().toTimeString) def appElement: Div = div( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala index 5322770..48c2724 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Layout.scala @@ -3,6 +3,7 @@ import com.raquo.domtypes.generic.codecs.StringAsIsCodec import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.{Page, UserProfile} +import com.raquo.waypoint.Router def PageHeader(currentPage: Signal[Page]): HtmlElement = header( @@ -36,19 +37,17 @@ logo: Navigation.Logo, profile: Signal[UserProfile], pages: Signal[List[Page]], - currentPage: Signal[Page], userMenu: Signal[List[Navigation.MenuItem]], content: HtmlElement -): HtmlElement = +)(using router: Router[Page]): HtmlElement = div( cls := "min-h-full", Navigation( logo, profile, pages, - currentPage, userMenu ), - PageHeader(currentPage), + PageHeader(router.$currentPage), MainSection(content) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala index 9f5ffa4..29b0e90 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Navigation.scala @@ -3,6 +3,29 @@ import com.raquo.domtypes.generic.codecs.StringAsIsCodec import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.{Page, UserProfile} +import com.raquo.waypoint.Router +import org.scalajs.dom + +def navigateTo(page: Page)(using router: Router[Page]): Binder[HtmlElement] = + Binder { el => + + val isLinkElement = el.ref.isInstanceOf[dom.html.Anchor] + + if (isLinkElement) { + el.amend(href(router.absoluteUrlForPage(page))) + } + + // If element is a link and user is holding a modifier while clicking: + // - Do nothing, browser will open the URL in new tab / window / etc. depending on the modifier key + // Otherwise: + // - Perform regular pushState transition + (onClick + .filter(ev => + !(isLinkElement && (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) + ) + .preventDefault + --> (_ => router.pushState(page))).bind(el) + } object Navigation: @@ -18,9 +41,8 @@ logo: Logo, profile: Signal[UserProfile], pages: Signal[List[Page]], - activePage: Signal[Page], userMenu: Signal[List[MenuItem]] -): +)(using router: Router[Page]): val mobileMenuOpen = Var(false) // Made a pull request to add aria-current to scala-dom-types, remove after @@ -145,9 +167,12 @@ ) ) - private def pageLink(page: Page, active: Signal[Boolean]): Anchor = + private def pageLink(page: Page, active: Signal[Boolean])(using + router: Router[Page] + ): Anchor = a( - href := "#", + href := router.absoluteUrlForPage(page), + navigateTo(page), cls <-- active.map { case true => "bg-indigo-700" case false => "hover:bg-indigo-500 hover:bg-opacity-75" @@ -168,7 +193,7 @@ ) private def pageLinks(mods: Modifier[HtmlElement]*) = pages.map( - _.map(p => pageLink(p, activePage.map(p == _)).amend(mods)) + _.map(p => pageLink(p, router.$currentPage.map(p == _)).amend(mods)) ) private def mobileMenuButton = button( diff --git a/build.sbt b/build.sbt index b80cd92..d116ef3 100644 --- a/build.sbt +++ b/build.sbt @@ -9,6 +9,7 @@ .settings( IWDeps.useZIO(Test), IWDeps.laminar, + IWDeps.zioJson, libraryDependencies ++= Seq( "com.raquo" %%% "waypoint" % "0.5.0", "be.doeraene" %%% "url-dsl" % "0.4.0",