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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala index d0836ee..e1b3a56 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -8,7 +8,7 @@ import pages.detail.DetailParametruPage import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink object DetailParametruPageConnector { trait AppState { @@ -40,8 +40,8 @@ } yield (da, pb) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailParametruPage.render(s)), $pageChangeSignal --> state.actionBus @@ -53,10 +53,15 @@ ): DetailParametruPage.ViewModel = DetailParametruPage.ViewModel( o.toDetailOsoby, - p.toParametr, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), p.criteria.map( - _.toKriterium(c => - a(Navigator.navigateTo[Page](Page.DetailKriteria(o, p, c))) - ) + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } ) ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala index d0836ee..e1b3a56 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -8,7 +8,7 @@ import pages.detail.DetailParametruPage import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink object DetailParametruPageConnector { trait AppState { @@ -40,8 +40,8 @@ } yield (da, pb) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailParametruPage.render(s)), $pageChangeSignal --> state.actionBus @@ -53,10 +53,15 @@ ): DetailParametruPage.ViewModel = DetailParametruPage.ViewModel( o.toDetailOsoby, - p.toParametr, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), p.criteria.map( - _.toKriterium(c => - a(Navigator.navigateTo[Page](Page.DetailKriteria(o, p, c))) - ) + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala index 72af2d4..5a66b4e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -4,24 +4,27 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator case class DirectoryPageConnector( $input: EventStream[List[UserInfo]], actionBus: Observer[Action] -)(using Router[Page]): +)(using router: Router[Page]): val $data = $input.startWithNone val $actionSignal = EventStream.fromValue(FetchDirectory) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(actionBus)( $data.split(_ => ())((_, _, s) => pages.directory.DirectoryPage.render( s.map( _.map( _.toUserRow(u => - a(Navigator.navigateTo[Page](Page.Detail(u.personalNumber))) + PageLink.container( + Page.Detail(u.personalNumber), + actionBus + ) ) ) ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala index d0836ee..e1b3a56 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -8,7 +8,7 @@ import pages.detail.DetailParametruPage import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink object DetailParametruPageConnector { trait AppState { @@ -40,8 +40,8 @@ } yield (da, pb) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailParametruPage.render(s)), $pageChangeSignal --> state.actionBus @@ -53,10 +53,15 @@ ): DetailParametruPage.ViewModel = DetailParametruPage.ViewModel( o.toDetailOsoby, - p.toParametr, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), p.criteria.map( - _.toKriterium(c => - a(Navigator.navigateTo[Page](Page.DetailKriteria(o, p, c))) - ) + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala index 72af2d4..5a66b4e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -4,24 +4,27 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator case class DirectoryPageConnector( $input: EventStream[List[UserInfo]], actionBus: Observer[Action] -)(using Router[Page]): +)(using router: Router[Page]): val $data = $input.startWithNone val $actionSignal = EventStream.fromValue(FetchDirectory) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(actionBus)( $data.split(_ => ())((_, _, s) => pages.directory.DirectoryPage.render( s.map( _.map( _.toUserRow(u => - a(Navigator.navigateTo[Page](Page.Detail(u.personalNumber))) + PageLink.container( + Page.Detail(u.personalNumber), + actionBus + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index 041ffc3..cf123e0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -23,18 +23,18 @@ ) extension (param: Parameter) - def toParametr: SeznamParametru.Parametr = + def toParametr(container: Parameter => Anchor): SeznamParametru.Parametr = SeznamParametru.Parametr( id = param.id, nazev = param.name, status = "Nesplněno", - statusColor = Color.red + statusColor = Color.red, + a = container(param) ) extension (crit: ParameterCriteria) def toKriterium( - container: ParameterCriteria => HtmlElement = (_: ParameterCriteria) => - div() + container: ParameterCriteria => Anchor ): SeznamKriterii.Kriterium = SeznamKriterii.Kriterium( nazev = crit.criteriumText, 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala index d0836ee..e1b3a56 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -8,7 +8,7 @@ import pages.detail.DetailParametruPage import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink object DetailParametruPageConnector { trait AppState { @@ -40,8 +40,8 @@ } yield (da, pb) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailParametruPage.render(s)), $pageChangeSignal --> state.actionBus @@ -53,10 +53,15 @@ ): DetailParametruPage.ViewModel = DetailParametruPage.ViewModel( o.toDetailOsoby, - p.toParametr, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), p.criteria.map( - _.toKriterium(c => - a(Navigator.navigateTo[Page](Page.DetailKriteria(o, p, c))) - ) + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala index 72af2d4..5a66b4e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -4,24 +4,27 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator case class DirectoryPageConnector( $input: EventStream[List[UserInfo]], actionBus: Observer[Action] -)(using Router[Page]): +)(using router: Router[Page]): val $data = $input.startWithNone val $actionSignal = EventStream.fromValue(FetchDirectory) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(actionBus)( $data.split(_ => ())((_, _, s) => pages.directory.DirectoryPage.render( s.map( _.map( _.toUserRow(u => - a(Navigator.navigateTo[Page](Page.Detail(u.personalNumber))) + PageLink.container( + Page.Detail(u.personalNumber), + actionBus + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index 041ffc3..cf123e0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -23,18 +23,18 @@ ) extension (param: Parameter) - def toParametr: SeznamParametru.Parametr = + def toParametr(container: Parameter => Anchor): SeznamParametru.Parametr = SeznamParametru.Parametr( id = param.id, nazev = param.name, status = "Nesplněno", - statusColor = Color.red + statusColor = Color.red, + a = container(param) ) extension (crit: ParameterCriteria) def toKriterium( - container: ParameterCriteria => HtmlElement = (_: ParameterCriteria) => - div() + container: ParameterCriteria => Anchor ): SeznamKriterii.Kriterium = SeznamKriterii.Kriterium( nazev = crit.criteriumText, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala index 50afeea..c840fe2 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -5,6 +5,7 @@ import components._ import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object DetailPage: @@ -13,16 +14,12 @@ parametry: SeznamParametru.ViewModel ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", div( cls := "flex flex-col space-y-4", DetailOsoby.render($m.map(_.osoba)), - child <-- $m.map(m => - SeznamParametru.render($m.map(_.parametry))(p => - Page.DetailParametru(m.osoba.osobniCislo, p.id) - ) - ) + SeznamParametru($m.map(_.parametry)) ) ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala index d0836ee..e1b3a56 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -8,7 +8,7 @@ import pages.detail.DetailParametruPage import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink object DetailParametruPageConnector { trait AppState { @@ -40,8 +40,8 @@ } yield (da, pb) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailParametruPage.render(s)), $pageChangeSignal --> state.actionBus @@ -53,10 +53,15 @@ ): DetailParametruPage.ViewModel = DetailParametruPage.ViewModel( o.toDetailOsoby, - p.toParametr, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), p.criteria.map( - _.toKriterium(c => - a(Navigator.navigateTo[Page](Page.DetailKriteria(o, p, c))) - ) + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala index 72af2d4..5a66b4e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -4,24 +4,27 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator case class DirectoryPageConnector( $input: EventStream[List[UserInfo]], actionBus: Observer[Action] -)(using Router[Page]): +)(using router: Router[Page]): val $data = $input.startWithNone val $actionSignal = EventStream.fromValue(FetchDirectory) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(actionBus)( $data.split(_ => ())((_, _, s) => pages.directory.DirectoryPage.render( s.map( _.map( _.toUserRow(u => - a(Navigator.navigateTo[Page](Page.Detail(u.personalNumber))) + PageLink.container( + Page.Detail(u.personalNumber), + actionBus + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index 041ffc3..cf123e0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -23,18 +23,18 @@ ) extension (param: Parameter) - def toParametr: SeznamParametru.Parametr = + def toParametr(container: Parameter => Anchor): SeznamParametru.Parametr = SeznamParametru.Parametr( id = param.id, nazev = param.name, status = "Nesplněno", - statusColor = Color.red + statusColor = Color.red, + a = container(param) ) extension (crit: ParameterCriteria) def toKriterium( - container: ParameterCriteria => HtmlElement = (_: ParameterCriteria) => - div() + container: ParameterCriteria => Anchor ): SeznamKriterii.Kriterium = SeznamKriterii.Kriterium( nazev = crit.criteriumText, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala index 50afeea..c840fe2 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -5,6 +5,7 @@ import components._ import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object DetailPage: @@ -13,16 +14,12 @@ parametry: SeznamParametru.ViewModel ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", div( cls := "flex flex-col space-y-4", DetailOsoby.render($m.map(_.osoba)), - child <-- $m.map(m => - SeznamParametru.render($m.map(_.parametry))(p => - Page.DetailParametru(m.osoba.osobniCislo, p.id) - ) - ) + SeznamParametru($m.map(_.parametry)) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index b035789..e7bce53 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -26,7 +26,7 @@ def render($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList.render($m, _.id) { $i => + kritList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala index d0836ee..e1b3a56 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -8,7 +8,7 @@ import pages.detail.DetailParametruPage import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink object DetailParametruPageConnector { trait AppState { @@ -40,8 +40,8 @@ } yield (da, pb) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailParametruPage.render(s)), $pageChangeSignal --> state.actionBus @@ -53,10 +53,15 @@ ): DetailParametruPage.ViewModel = DetailParametruPage.ViewModel( o.toDetailOsoby, - p.toParametr, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), p.criteria.map( - _.toKriterium(c => - a(Navigator.navigateTo[Page](Page.DetailKriteria(o, p, c))) - ) + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala index 72af2d4..5a66b4e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -4,24 +4,27 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator case class DirectoryPageConnector( $input: EventStream[List[UserInfo]], actionBus: Observer[Action] -)(using Router[Page]): +)(using router: Router[Page]): val $data = $input.startWithNone val $actionSignal = EventStream.fromValue(FetchDirectory) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(actionBus)( $data.split(_ => ())((_, _, s) => pages.directory.DirectoryPage.render( s.map( _.map( _.toUserRow(u => - a(Navigator.navigateTo[Page](Page.Detail(u.personalNumber))) + PageLink.container( + Page.Detail(u.personalNumber), + actionBus + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index 041ffc3..cf123e0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -23,18 +23,18 @@ ) extension (param: Parameter) - def toParametr: SeznamParametru.Parametr = + def toParametr(container: Parameter => Anchor): SeznamParametru.Parametr = SeznamParametru.Parametr( id = param.id, nazev = param.name, status = "Nesplněno", - statusColor = Color.red + statusColor = Color.red, + a = container(param) ) extension (crit: ParameterCriteria) def toKriterium( - container: ParameterCriteria => HtmlElement = (_: ParameterCriteria) => - div() + container: ParameterCriteria => Anchor ): SeznamKriterii.Kriterium = SeznamKriterii.Kriterium( nazev = crit.criteriumText, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala index 50afeea..c840fe2 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -5,6 +5,7 @@ import components._ import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object DetailPage: @@ -13,16 +14,12 @@ parametry: SeznamParametru.ViewModel ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", div( cls := "flex flex-col space-y-4", DetailOsoby.render($m.map(_.osoba)), - child <-- $m.map(m => - SeznamParametru.render($m.map(_.parametry))(p => - Page.DetailParametru(m.osoba.osobniCislo, p.id) - ) - ) + SeznamParametru($m.map(_.parametry)) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index b035789..e7bce53 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -26,7 +26,7 @@ def render($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList.render($m, _.id) { $i => + kritList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index 70b86f4..ad3c67b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -10,30 +10,24 @@ RowNext } import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* object SeznamParametru: - sealed trait Action - case object Selected extends Action - case class Parametr( id: String, nazev: String, status: String, - statusColor: Color + statusColor: Color, + a: Anchor ) type ViewModel = List[Parametr] private val parametrList = new StackedList[Parametr] - def render($m: Signal[ViewModel])(pageF: Parametr => Page)(using - router: Router[Page] - ): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList.render($m, _.id) { $i => + parametrList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, @@ -43,7 +37,7 @@ bottomLeft = emptyNode, bottomRight = emptyNode, farRight = RowNext.render, - containerElement = a(Navigator.navigateTo(pageF(i))) + containerElement = i.a ) } } 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala index d0836ee..e1b3a56 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -8,7 +8,7 @@ import pages.detail.DetailParametruPage import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink object DetailParametruPageConnector { trait AppState { @@ -40,8 +40,8 @@ } yield (da, pb) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailParametruPage.render(s)), $pageChangeSignal --> state.actionBus @@ -53,10 +53,15 @@ ): DetailParametruPage.ViewModel = DetailParametruPage.ViewModel( o.toDetailOsoby, - p.toParametr, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), p.criteria.map( - _.toKriterium(c => - a(Navigator.navigateTo[Page](Page.DetailKriteria(o, p, c))) - ) + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala index 72af2d4..5a66b4e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -4,24 +4,27 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator case class DirectoryPageConnector( $input: EventStream[List[UserInfo]], actionBus: Observer[Action] -)(using Router[Page]): +)(using router: Router[Page]): val $data = $input.startWithNone val $actionSignal = EventStream.fromValue(FetchDirectory) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(actionBus)( $data.split(_ => ())((_, _, s) => pages.directory.DirectoryPage.render( s.map( _.map( _.toUserRow(u => - a(Navigator.navigateTo[Page](Page.Detail(u.personalNumber))) + PageLink.container( + Page.Detail(u.personalNumber), + actionBus + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index 041ffc3..cf123e0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -23,18 +23,18 @@ ) extension (param: Parameter) - def toParametr: SeznamParametru.Parametr = + def toParametr(container: Parameter => Anchor): SeznamParametru.Parametr = SeznamParametru.Parametr( id = param.id, nazev = param.name, status = "Nesplněno", - statusColor = Color.red + statusColor = Color.red, + a = container(param) ) extension (crit: ParameterCriteria) def toKriterium( - container: ParameterCriteria => HtmlElement = (_: ParameterCriteria) => - div() + container: ParameterCriteria => Anchor ): SeznamKriterii.Kriterium = SeznamKriterii.Kriterium( nazev = crit.criteriumText, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala index 50afeea..c840fe2 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -5,6 +5,7 @@ import components._ import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object DetailPage: @@ -13,16 +14,12 @@ parametry: SeznamParametru.ViewModel ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", div( cls := "flex flex-col space-y-4", DetailOsoby.render($m.map(_.osoba)), - child <-- $m.map(m => - SeznamParametru.render($m.map(_.parametry))(p => - Page.DetailParametru(m.osoba.osobniCislo, p.id) - ) - ) + SeznamParametru($m.map(_.parametry)) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index b035789..e7bce53 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -26,7 +26,7 @@ def render($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList.render($m, _.id) { $i => + kritList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index 70b86f4..ad3c67b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -10,30 +10,24 @@ RowNext } import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* object SeznamParametru: - sealed trait Action - case object Selected extends Action - case class Parametr( id: String, nazev: String, status: String, - statusColor: Color + statusColor: Color, + a: Anchor ) type ViewModel = List[Parametr] private val parametrList = new StackedList[Parametr] - def render($m: Signal[ViewModel])(pageF: Parametr => Page)(using - router: Router[Page] - ): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList.render($m, _.id) { $i => + parametrList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, @@ -43,7 +37,7 @@ bottomLeft = emptyNode, bottomRight = emptyNode, farRight = RowNext.render, - containerElement = a(Navigator.navigateTo(pageF(i))) + containerElement = i.a ) } } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala index aa41c05..467c5f5 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -2,18 +2,23 @@ import com.raquo.domtypes.generic.codecs.StringAsIsCodec import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router -case class ErrorPage( - homePage: Page, - errorName: String, - title: String, - subTitle: String -)(using router: Router[Page]) - extends Navigator[Page]: - def render: HtmlElement = +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m div( cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", main( @@ -52,11 +57,12 @@ ), div( cls := "mt-6", - a( - navigateTo(homePage), - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) ) ) ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala index d0836ee..e1b3a56 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -8,7 +8,7 @@ import pages.detail.DetailParametruPage import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink object DetailParametruPageConnector { trait AppState { @@ -40,8 +40,8 @@ } yield (da, pb) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailParametruPage.render(s)), $pageChangeSignal --> state.actionBus @@ -53,10 +53,15 @@ ): DetailParametruPage.ViewModel = DetailParametruPage.ViewModel( o.toDetailOsoby, - p.toParametr, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), p.criteria.map( - _.toKriterium(c => - a(Navigator.navigateTo[Page](Page.DetailKriteria(o, p, c))) - ) + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala index 72af2d4..5a66b4e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -4,24 +4,27 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator case class DirectoryPageConnector( $input: EventStream[List[UserInfo]], actionBus: Observer[Action] -)(using Router[Page]): +)(using router: Router[Page]): val $data = $input.startWithNone val $actionSignal = EventStream.fromValue(FetchDirectory) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(actionBus)( $data.split(_ => ())((_, _, s) => pages.directory.DirectoryPage.render( s.map( _.map( _.toUserRow(u => - a(Navigator.navigateTo[Page](Page.Detail(u.personalNumber))) + PageLink.container( + Page.Detail(u.personalNumber), + actionBus + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index 041ffc3..cf123e0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -23,18 +23,18 @@ ) extension (param: Parameter) - def toParametr: SeznamParametru.Parametr = + def toParametr(container: Parameter => Anchor): SeznamParametru.Parametr = SeznamParametru.Parametr( id = param.id, nazev = param.name, status = "Nesplněno", - statusColor = Color.red + statusColor = Color.red, + a = container(param) ) extension (crit: ParameterCriteria) def toKriterium( - container: ParameterCriteria => HtmlElement = (_: ParameterCriteria) => - div() + container: ParameterCriteria => Anchor ): SeznamKriterii.Kriterium = SeznamKriterii.Kriterium( nazev = crit.criteriumText, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala index 50afeea..c840fe2 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -5,6 +5,7 @@ import components._ import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object DetailPage: @@ -13,16 +14,12 @@ parametry: SeznamParametru.ViewModel ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", div( cls := "flex flex-col space-y-4", DetailOsoby.render($m.map(_.osoba)), - child <-- $m.map(m => - SeznamParametru.render($m.map(_.parametry))(p => - Page.DetailParametru(m.osoba.osobniCislo, p.id) - ) - ) + SeznamParametru($m.map(_.parametry)) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index b035789..e7bce53 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -26,7 +26,7 @@ def render($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList.render($m, _.id) { $i => + kritList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index 70b86f4..ad3c67b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -10,30 +10,24 @@ RowNext } import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* object SeznamParametru: - sealed trait Action - case object Selected extends Action - case class Parametr( id: String, nazev: String, status: String, - statusColor: Color + statusColor: Color, + a: Anchor ) type ViewModel = List[Parametr] private val parametrList = new StackedList[Parametr] - def render($m: Signal[ViewModel])(pageF: Parametr => Page)(using - router: Router[Page] - ): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList.render($m, _.id) { $i => + parametrList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, @@ -43,7 +37,7 @@ bottomLeft = emptyNode, bottomRight = emptyNode, farRight = RowNext.render, - containerElement = a(Navigator.navigateTo(pageF(i))) + containerElement = i.a ) } } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala index aa41c05..467c5f5 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -2,18 +2,23 @@ import com.raquo.domtypes.generic.codecs.StringAsIsCodec import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router -case class ErrorPage( - homePage: Page, - errorName: String, - title: String, - subTitle: String -)(using router: Router[Page]) - extends Navigator[Page]: - def render: HtmlElement = +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m div( cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", main( @@ -52,11 +57,12 @@ ), div( cls := "mt-6", - a( - navigateTo(homePage), - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala index 787a250..bd9baec 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -4,13 +4,18 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action -def NotFoundPage(homePage: Page, url: String)(using - router: Router[Page] -): HtmlElement = - ErrorPage( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ).render +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala index d0836ee..e1b3a56 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -8,7 +8,7 @@ import pages.detail.DetailParametruPage import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink object DetailParametruPageConnector { trait AppState { @@ -40,8 +40,8 @@ } yield (da, pb) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailParametruPage.render(s)), $pageChangeSignal --> state.actionBus @@ -53,10 +53,15 @@ ): DetailParametruPage.ViewModel = DetailParametruPage.ViewModel( o.toDetailOsoby, - p.toParametr, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), p.criteria.map( - _.toKriterium(c => - a(Navigator.navigateTo[Page](Page.DetailKriteria(o, p, c))) - ) + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala index 72af2d4..5a66b4e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -4,24 +4,27 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator case class DirectoryPageConnector( $input: EventStream[List[UserInfo]], actionBus: Observer[Action] -)(using Router[Page]): +)(using router: Router[Page]): val $data = $input.startWithNone val $actionSignal = EventStream.fromValue(FetchDirectory) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(actionBus)( $data.split(_ => ())((_, _, s) => pages.directory.DirectoryPage.render( s.map( _.map( _.toUserRow(u => - a(Navigator.navigateTo[Page](Page.Detail(u.personalNumber))) + PageLink.container( + Page.Detail(u.personalNumber), + actionBus + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index 041ffc3..cf123e0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -23,18 +23,18 @@ ) extension (param: Parameter) - def toParametr: SeznamParametru.Parametr = + def toParametr(container: Parameter => Anchor): SeznamParametru.Parametr = SeznamParametru.Parametr( id = param.id, nazev = param.name, status = "Nesplněno", - statusColor = Color.red + statusColor = Color.red, + a = container(param) ) extension (crit: ParameterCriteria) def toKriterium( - container: ParameterCriteria => HtmlElement = (_: ParameterCriteria) => - div() + container: ParameterCriteria => Anchor ): SeznamKriterii.Kriterium = SeznamKriterii.Kriterium( nazev = crit.criteriumText, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala index 50afeea..c840fe2 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -5,6 +5,7 @@ import components._ import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object DetailPage: @@ -13,16 +14,12 @@ parametry: SeznamParametru.ViewModel ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", div( cls := "flex flex-col space-y-4", DetailOsoby.render($m.map(_.osoba)), - child <-- $m.map(m => - SeznamParametru.render($m.map(_.parametry))(p => - Page.DetailParametru(m.osoba.osobniCislo, p.id) - ) - ) + SeznamParametru($m.map(_.parametry)) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index b035789..e7bce53 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -26,7 +26,7 @@ def render($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList.render($m, _.id) { $i => + kritList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index 70b86f4..ad3c67b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -10,30 +10,24 @@ RowNext } import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* object SeznamParametru: - sealed trait Action - case object Selected extends Action - case class Parametr( id: String, nazev: String, status: String, - statusColor: Color + statusColor: Color, + a: Anchor ) type ViewModel = List[Parametr] private val parametrList = new StackedList[Parametr] - def render($m: Signal[ViewModel])(pageF: Parametr => Page)(using - router: Router[Page] - ): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList.render($m, _.id) { $i => + parametrList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, @@ -43,7 +37,7 @@ bottomLeft = emptyNode, bottomRight = emptyNode, farRight = RowNext.render, - containerElement = a(Navigator.navigateTo(pageF(i))) + containerElement = i.a ) } } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala index aa41c05..467c5f5 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -2,18 +2,23 @@ import com.raquo.domtypes.generic.codecs.StringAsIsCodec import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router -case class ErrorPage( - homePage: Page, - errorName: String, - title: String, - subTitle: String -)(using router: Router[Page]) - extends Navigator[Page]: - def render: HtmlElement = +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m div( cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", main( @@ -52,11 +57,12 @@ ), div( cls := "mt-6", - a( - navigateTo(homePage), - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala index 787a250..bd9baec 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -4,13 +4,18 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action -def NotFoundPage(homePage: Page, url: String)(using - router: Router[Page] -): HtmlElement = - ErrorPage( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ).render +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala index 8d79ee3..476fe3c 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -4,15 +4,29 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action -def UnhandledErrorPage( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] -)(using router: Router[Page]): HtmlElement = - ErrorPage( - homePage, - "Unexpected error occurred", - errorName.getOrElse("Uh oh!"), // TODO: translations, better text than uh oh - errorMessage.getOrElse("This wasn't supposed to happen! Please try again.") - ).render +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) 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 58cc570..a932514 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 @@ -63,34 +63,36 @@ .collectSignal[Page.Detail]( connectors .DetailPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailParametru]( connectors .DetailParametruPageConnector(state)(_) - .render + .apply ) .collectSignal[Page.DetailKriteria]( connectors .DetailKriteriaPageConnector(state)(_) - .render + .apply ) - .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url) + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) ) .collect[Page.UnhandledError](pg => pages.errors .UnhandledErrorPage( - Routes.homePage, - pg.errorName, - pg.errorMessage + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus ) ) .collectStatic(Page.Directory)( connectors .DirectoryPageConnector(state.users, state.actionBus) - .render + .apply ) div(child <-- pageSplitter.$view) 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 e77d2a3..7d51cbb 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 @@ -3,12 +3,13 @@ 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, OsobniCislo} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo object AppPage: // TODO: pages by logged in user - val pages = List(Page.Directory, Page.Dashboard) + val pages: List[Page] = List(Page.Directory, Page.Dashboard) import NavigationBar.{Logo, MenuItem} @@ -48,15 +49,26 @@ val $userInfo = $userProfile.signal.map(_.userInfo) type ViewModel = Option[HtmlElement] - def render($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - Router[Page] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] ): HtmlElement = - PageLayout.render( - $m.combineWith($userInfo).map((c, u) => + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => PageLayout.ViewModel( NavigationBar.ViewModel( u, - pages, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), userMenu, logo ), diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala index 147984b..6082718 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala @@ -3,12 +3,13 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import CustomAttrs.svg.ariaHidden -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import cz.e_bs.cmi.mdr.pdb.app.components.list.IconText.ViewModel +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.laminar.api.L.{*, given} import io.laminext.syntax.core.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.Action object Breadcrumbs: @@ -32,7 +33,9 @@ text: String, extraClasses: String ) - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = inline def alt[T]( homeVariant: => T, pageVariant: ViewModel => T @@ -40,26 +43,29 @@ $m.map { m => if (m.page.isRoot) then homeVariant else pageVariant(m) } - a( - Navigator.navigateTo($m.map(_.page)), - cls <-- alt( - "text-gray-400 hover:text-gray-500", - m => - s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" - ), - child.maybe <-- alt( - Some(Icons.solid.home), - _.icon - ), - child <-- alt( - span(cls := "sr-only", "Domů"), - m => span(m.text) + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- alt( + "text-gray-400 hover:text-gray-500", + m => + s"${m.extraClasses} text-sm font-medium text-gray-500 hover:text-gray-700" + ), + child.maybe <-- alt( + Some(Icons.solid.home), + _.icon + ), + child <-- alt( + span(cls := "sr-only", "Domů"), + m => span(m.text) + ) ) - ) object FullBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = ol( role := "list", cls := "flex items-center space-x-4", @@ -76,7 +82,8 @@ p.title, "ml-4" ) - ) + ), + actionBus ) ) ) @@ -85,27 +92,34 @@ object ShortBreadcrumbs: type ViewModel = Page - def apply($m: Signal[ViewModel])(using Router[Page]): HtmlElement = - Link($m.map { p => - val target = p.parent.getOrElse(p) - Link.ViewModel( - target, - Some(Icons.solid.`arrow-narrow-left`), - s"Zpět na ${target.title}", - "group inline-flex space-x-3" - ) - }) + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + Link.ViewModel( + target, + Some(Icons.solid.`arrow-narrow-left`), + s"Zpět na ${target.title}", + "group inline-flex space-x-3" + ) + }, + actionBus + ) - def apply()(using router: Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = nav( cls := "flex", aria.label := "Breadcrumb", div( cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage) + ShortBreadcrumbs(router.$currentPage, actionBus) ), div( cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage) + FullBreadcrumbs(router.$currentPage, actionBus) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala new file mode 100644 index 0000000..eadb044 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Display.scala @@ -0,0 +1,28 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +object Display: + + enum Breakpoint: + case sm, md, lg, xl, `2xl` + + enum DisplayClass: + case block, `inline-block`, `inline`, flex, `inline-flex`, table, + `inline-table`, `table-caption` + + object ShowUpFrom: + inline def apply( + br: Breakpoint, + dc: DisplayClass = DisplayClass.block + ): HtmlElement = + div( + cls := "hidden", + cls := s"${br}:${dc}" + ) + + object HideUpTo: + inline def apply(br: Breakpoint): HtmlElement = + div( + cls := s"${br}:hidden" + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala new file mode 100644 index 0000000..6459b73 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/LinkSupport.scala @@ -0,0 +1,12 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent + +object LinkSupport: + + extension [El <: org.scalajs.dom.EventTarget]( + ep: EventProcessor[TypedTargetMouseEvent[El], TypedTargetMouseEvent[El]] + ) + def noKeyMod = + ep.filter(ev => !(ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala index 8ba5360..c9a48e1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala @@ -1,25 +1,24 @@ package cz.e_bs.cmi.mdr.pdb.app.components import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator import CustomAttrs.ariaCurrent -import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.Page +import io.laminext.syntax.core.* object NavigationBar: case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) case class MenuItem(title: String) case class ViewModel( userInfo: UserInfo, - pages: List[Page], + pages: List[Link], userMenu: List[MenuItem], logo: Logo ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = val $userInfo = $m.map(_.userInfo) val mobileMenuOpen = Var(false) @@ -139,20 +138,17 @@ ) ) - def pageLink(page: Page, active: Signal[Boolean]): Anchor = - a( - Navigator.navigateTo(page), - cls <-- active.map { - case true => "bg-indigo-700" - case false => "hover:bg-indigo-500 hover:bg-opacity-75" - }, - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - ariaCurrent <-- active.map { - case true => "page" - case _ => "false" - }, - page.title - ) + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) def logoImg: Image = img( @@ -161,25 +157,18 @@ alt <-- $m.map(_.logo.name) ) - def pageLinks(mods: Modifier[HtmlElement]*) = - $m.map( - _.pages.map(p => - pageLink(p, router.$currentPage.map(p == _)).amend(mods) - ) - ) - def mobileMenuButton = button( tpe := "button", cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", aria.controls := "mobile-menu", aria.expanded <-- mobileMenuOpen.signal, span(cls := "sr-only", "Open main menu"), - Icons.outline.menu.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "hidden" else "block" - }), - Icons.outline.x.amend(svg.cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }), + Icons.outline.menu.amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline.x.amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), onClick.preventDefault.mapTo( !mobileMenuOpen.now() ) --> mobileMenuOpen.writer @@ -193,7 +182,9 @@ desktopOnly, div( cls := "ml-10 flex items-baseline space-x-4", - children <-- pageLinks() + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ) ) ) @@ -235,7 +226,9 @@ idAttr := "mobile-menu", div( cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- pageLinks(cls := "block") + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) ), mobileProfile ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala index e1b65e5..75339df 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala @@ -3,16 +3,17 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object PageHeader: - def render(using Router[Page]): HtmlElement = + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = header( cls := "bg-white shadow-sm", div( cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", h1( cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs() + Breadcrumbs(actionBus) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala index 3ee6483..c47e0a1 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala @@ -3,21 +3,22 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action object PageLayout: case class ViewModel( navigation: NavigationBar.ViewModel, content: Option[HtmlElement] ) - def render( + def apply(actionBus: Observer[Action])( $m: Signal[ViewModel], mods: Modifier[HtmlElement]* )(using Router[Page]): HtmlElement = val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) div( cls := "min-h-full", - NavigationBar.render($m.map(_.navigation)), - PageHeader.render, + NavigationBar($m.map(_.navigation)), + PageHeader(actionBus), main( mods, child <-- $maybeContent.map(_.getOrElse(Loading)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..0cb8fe5 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import LinkSupport.* +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala index 26a70ea..097fbb7 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -13,7 +13,7 @@ containerElement: HtmlElement = div() ) - def render($m: Signal[ViewModel]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = li( child <-- $m.map(m => m.containerElement.amend( diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala index 984282a..e80fba6 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -4,12 +4,12 @@ class StackedList[Item]: type ViewModel = List[Item] - def render( + def apply( $m: Signal[ViewModel], keyF: Item => String )(f: Signal[Item] => Signal[ListRow.ViewModel]): HtmlElement = ul( role := "list", cls := "divide-y divide-gray-200", - children <-- $m.split(keyF)((_, _, $d) => ListRow.render(f($d))) + children <-- $m.split(keyF)((_, _, $d) => ListRow(f($d))) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala index 6d2f77b..87f825a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -6,6 +6,8 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -class DashboardPageConnector(using router: Router[Page]): - def render: HtmlElement = - AppPage.render(Val(Some(DashboardPage.render))) +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala index 3af1a13..f700b30 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -9,7 +9,6 @@ import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator object DetailKriteriaPageConnector { trait AppState { @@ -44,8 +43,8 @@ } yield (da, pb, ka) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailKriteriaPage.render(s)), $pageChangeSignal --> state.actionBus @@ -58,6 +57,6 @@ ): DetailKriteriaPage.ViewModel = DetailKriteriaPage.ViewModel( o.toDetailOsoby, - p.toParametr, - k.toKriterium() + p.toParametr(_ => a()), + k.toKriterium(_ => a()) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala index 88cc209..4ba4e84 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -6,6 +6,7 @@ import cz.e_bs.cmi.mdr.pdb.UserInfo import pages.detail.DetailPage import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby import cz.e_bs.cmi.mdr.pdb.Parameter import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru @@ -34,11 +35,11 @@ val $data = state.details.startWithNone val $params = state.parameters.startWithNone - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $data.combineWithFn($params)(_ zip _) .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage.render(s)), + .split(_ => ())((_, _, s) => DetailPage(s)), $pageChangeSignal --> state.actionBus ) @@ -48,5 +49,9 @@ ): DetailPage.ViewModel = DetailPage.ViewModel( o.toDetailOsoby, - p.map(_.toParametr) + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala index d0836ee..e1b3a56 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -8,7 +8,7 @@ import pages.detail.DetailParametruPage import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink object DetailParametruPageConnector { trait AppState { @@ -40,8 +40,8 @@ } yield (da, pb) ) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(state.actionBus)( $merged.map(_.map(buildModel)) .split(_ => ())((_, _, s) => DetailParametruPage.render(s)), $pageChangeSignal --> state.actionBus @@ -53,10 +53,15 @@ ): DetailParametruPage.ViewModel = DetailParametruPage.ViewModel( o.toDetailOsoby, - p.toParametr, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), p.criteria.map( - _.toKriterium(c => - a(Navigator.navigateTo[Page](Page.DetailKriteria(o, p, c))) - ) + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala index 72af2d4..5a66b4e 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -4,24 +4,27 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.UserInfo +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator case class DirectoryPageConnector( $input: EventStream[List[UserInfo]], actionBus: Observer[Action] -)(using Router[Page]): +)(using router: Router[Page]): val $data = $input.startWithNone val $actionSignal = EventStream.fromValue(FetchDirectory) - def render: HtmlElement = - AppPage.render( + def apply: HtmlElement = + AppPage(actionBus)( $data.split(_ => ())((_, _, s) => pages.directory.DirectoryPage.render( s.map( _.map( _.toUserRow(u => - a(Navigator.navigateTo[Page](Page.Detail(u.personalNumber))) + PageLink.container( + Page.Detail(u.personalNumber), + actionBus + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala index 041ffc3..cf123e0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala @@ -23,18 +23,18 @@ ) extension (param: Parameter) - def toParametr: SeznamParametru.Parametr = + def toParametr(container: Parameter => Anchor): SeznamParametru.Parametr = SeznamParametru.Parametr( id = param.id, nazev = param.name, status = "Nesplněno", - statusColor = Color.red + statusColor = Color.red, + a = container(param) ) extension (crit: ParameterCriteria) def toKriterium( - container: ParameterCriteria => HtmlElement = (_: ParameterCriteria) => - div() + container: ParameterCriteria => Anchor ): SeznamKriterii.Kriterium = SeznamKriterii.Kriterium( nazev = crit.criteriumText, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala index 50afeea..c840fe2 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -5,6 +5,7 @@ import components._ import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Action object DetailPage: @@ -13,16 +14,12 @@ parametry: SeznamParametru.ViewModel ) - def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", div( cls := "flex flex-col space-y-4", DetailOsoby.render($m.map(_.osoba)), - child <-- $m.map(m => - SeznamParametru.render($m.map(_.parametry))(p => - Page.DetailParametru(m.osoba.osobniCislo, p.id) - ) - ) + SeznamParametru($m.map(_.parametry)) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala index b035789..e7bce53 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -26,7 +26,7 @@ def render($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList.render($m, _.id) { $i => + kritList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala index 70b86f4..ad3c67b 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -10,30 +10,24 @@ RowNext } import cz.e_bs.cmi.mdr.pdb.app.components.Color -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.components.LinkSupport.* object SeznamParametru: - sealed trait Action - case object Selected extends Action - case class Parametr( id: String, nazev: String, status: String, - statusColor: Color + statusColor: Color, + a: Anchor ) type ViewModel = List[Parametr] private val parametrList = new StackedList[Parametr] - def render($m: Signal[ViewModel])(pageF: Parametr => Page)(using - router: Router[Page] - ): HtmlElement = + def apply($m: Signal[ViewModel]): HtmlElement = div( cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList.render($m, _.id) { $i => + parametrList($m, _.id) { $i => $i.map { i => ListRow.ViewModel( title = i.nazev, @@ -43,7 +37,7 @@ bottomLeft = emptyNode, bottomRight = emptyNode, farRight = RowNext.render, - containerElement = a(Navigator.navigateTo(pageF(i))) + containerElement = i.a ) } } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala index aa41c05..467c5f5 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -2,18 +2,23 @@ import com.raquo.domtypes.generic.codecs.StringAsIsCodec import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.PageLink import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator +import cz.e_bs.cmi.mdr.pdb.app.Action +import com.raquo.waypoint.Router -case class ErrorPage( - homePage: Page, - errorName: String, - title: String, - subTitle: String -)(using router: Router[Page]) - extends Navigator[Page]: - def render: HtmlElement = +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m div( cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", main( @@ -52,11 +57,12 @@ ), div( cls := "mt-6", - a( - navigateTo(homePage), - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) ) ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala index 787a250..bd9baec 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -4,13 +4,18 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action -def NotFoundPage(homePage: Page, url: String)(using - router: Router[Page] -): HtmlElement = - ErrorPage( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ).render +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala index 8d79ee3..476fe3c 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -4,15 +4,29 @@ import com.raquo.laminar.api.L.{*, given} import com.raquo.waypoint.Router import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.app.Action -def UnhandledErrorPage( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] -)(using router: Router[Page]): HtmlElement = - ErrorPage( - homePage, - "Unexpected error occurred", - errorName.getOrElse("Uh oh!"), // TODO: translations, better text than uh oh - errorMessage.getOrElse("This wasn't supposed to happen! Please try again.") - ).render +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) 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 deleted file mode 100644 index 6dfc69d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/waypoint/components/Navigator.scala +++ /dev/null @@ -1,60 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.waypoint.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import org.scalajs.dom -import com.raquo.domtypes.jsdom.defs.events.TypedTargetMouseEvent -import com.raquo.laminar.nodes.ReactiveElement - -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: Signal[P])(using - router: Router[P] - ): Binder[HtmlElement] = - Binder { el => - - val isLinkElement = el.ref.isInstanceOf[dom.html.Anchor] - - if (isLinkElement) { - el.amend(href <-- $page.map(page => 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 - ( - composeEvents( - onClick - .filter(ev => - !(isLinkElement && (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) - ) - .preventDefault - )(_.sample($page)) --> (p => router.pushState(p)) - ).bind(el) - } - - def navigateTo[P](page: P)(using router: Router[P]): 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) - } -}