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 1b5138c..7737b2f 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 @@ -72,7 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage( + .DirectoryPage(() => EventStream .fromValue( mockUsers 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 1b5138c..7737b2f 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 @@ -72,7 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage( + .DirectoryPage(() => EventStream .fromValue( mockUsers 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 9c86d49..43f16c9 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo => ModelUserInfo} +import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo} import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator trait AppPage @@ -33,7 +33,7 @@ val $userProfile = Var( UserProfile( "tom", - ModelUserInfo( + UserInfo( "1031", "tom", "Tom", @@ -49,6 +49,4 @@ ) ) - override val $userInfo = $userProfile.signal.map(p => - UserInfo(p.userInfo.name, p.userInfo.email, p.userInfo.img) - ) + override val $userInfo = $userProfile.signal.map(_.userInfo) 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 1b5138c..7737b2f 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 @@ -72,7 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage( + .DirectoryPage(() => EventStream .fromValue( mockUsers 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 9c86d49..43f16c9 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo => ModelUserInfo} +import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo} import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator trait AppPage @@ -33,7 +33,7 @@ val $userProfile = Var( UserProfile( "tom", - ModelUserInfo( + UserInfo( "1031", "tom", "Tom", @@ -49,6 +49,4 @@ ) ) - override val $userInfo = $userProfile.signal.map(p => - UserInfo(p.userInfo.name, p.userInfo.email, p.userInfo.img) - ) + override val $userInfo = $userProfile.signal.map(_.userInfo) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala new file mode 100644 index 0000000..7c7bb07 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala @@ -0,0 +1,13 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "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 1b5138c..7737b2f 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 @@ -72,7 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage( + .DirectoryPage(() => EventStream .fromValue( mockUsers 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 9c86d49..43f16c9 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo => ModelUserInfo} +import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo} import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator trait AppPage @@ -33,7 +33,7 @@ val $userProfile = Var( UserProfile( "tom", - ModelUserInfo( + UserInfo( "1031", "tom", "Tom", @@ -49,6 +49,4 @@ ) ) - override val $userInfo = $userProfile.signal.map(p => - UserInfo(p.userInfo.name, p.userInfo.email, p.userInfo.img) - ) + override val $userInfo = $userProfile.signal.map(_.userInfo) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala new file mode 100644 index 0000000..7c7bb07 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala @@ -0,0 +1,13 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) 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 bf9379e..ea6c8bc 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 @@ -4,13 +4,13 @@ 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 trait NavigationBar[Page](using router: Router[Page]): self: Navigator[Page] => case class Logo(img: String, name: String) case class MenuItem(title: String) - case class UserInfo(name: String, email: Option[String], img: Option[String]) def $userInfo: Signal[UserInfo] 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 1b5138c..7737b2f 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 @@ -72,7 +72,7 @@ ) .collectStatic(Page.Directory)( pages - .DirectoryPage( + .DirectoryPage(() => EventStream .fromValue( mockUsers 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 9c86d49..43f16c9 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala @@ -2,7 +2,7 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo => ModelUserInfo} +import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo} import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator trait AppPage @@ -33,7 +33,7 @@ val $userProfile = Var( UserProfile( "tom", - ModelUserInfo( + UserInfo( "1031", "tom", "Tom", @@ -49,6 +49,4 @@ ) ) - override val $userInfo = $userProfile.signal.map(p => - UserInfo(p.userInfo.name, p.userInfo.email, p.userInfo.img) - ) + override val $userInfo = $userProfile.signal.map(_.userInfo) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala new file mode 100644 index 0000000..7c7bb07 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Loader.scala @@ -0,0 +1,13 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} + +// TODO: proper loader +def Loading = + div( + cls := "bg-gray-50 overflow-hidden rounded-lg", + div( + cls := "px-4 py-5 sm:p-6", + "Loading..." + ) + ) 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 bf9379e..ea6c8bc 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 @@ -4,13 +4,13 @@ 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 trait NavigationBar[Page](using router: Router[Page]): self: Navigator[Page] => case class Logo(img: String, name: String) case class MenuItem(title: String) - case class UserInfo(name: String, email: Option[String], img: Option[String]) def $userInfo: Signal[UserInfo] diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala index 3f92d09..64791a0 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DirectoryPage.scala @@ -4,105 +4,125 @@ import cz.e_bs.cmi.mdr.pdb.app.components.Icons import cz.e_bs.cmi.mdr.pdb.app.Page import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.components.{AppPage, Loading} import cz.e_bs.cmi.mdr.pdb.UserInfo -case class DirectoryPage(data: EventStream[List[UserInfo]])(using +case class DirectoryPage(fetch: () => EventStream[List[UserInfo]])(using router: Router[Page] ) extends AppPage: - def pageContent: HtmlElement = + override def pageContent: HtmlElement = + val data = Var[Option[List[UserInfo]]](None) + val $maybeDirectory = + data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) div( cls := "max-w-7xl mx-auto", //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", - form( - cls := "p-4 mt-6 flex space-x-4", - action := "#", - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - """Search""" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Search" - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter, - span( - cls := "sr-only", - """Search""" - ) - ) - ), - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", + searchForm, + fetch().delay(1000) --> data.writer.contramapSome, + child <-- $maybeDirectory.map(_.getOrElse(Loading)) + ) + + private def renderDirectory(data: Signal[List[UserInfo]]): HtmlElement = + val byLetter = for { + d <- data + } yield for { + (letter, users) <- d.groupBy(_.surname.head).to(List).sortBy(_._1) + } yield (letter.toString, users.sortBy(_.surname)) + + val rendered = byLetter + .split(_._1)((_, _, s) => div( cls := "relative", // TODO: group by surname div( cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3( - """A""" - ) + h3(child.text <-- s.map(_._1)) ), ul( role := "list", cls := "relative z-0 divide-y divide-gray-200", // TODO: zero / loading page - children <-- data.map(_.map({ o => - val page = Page.Detail(o.personalNumber) - li( - div( - cls := "relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - img( - cls := "h-10 w-10 rounded-full", - src := "https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", - alt := "" - ) - ), - div( - cls := "flex-1 min-w-0", - a( - href := router.absoluteUrlForPage(page), - navigateTo(page), - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.name - ), - p( - cls := "text-sm text-gray-500 truncate", - o.mainFunction - ) - ) - ) - ) - ) - })) + children <-- s.map(_._2.map(renderUser)) ) ) ) + + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- rendered + ) + + private def renderUser(o: UserInfo) = + val page = Page.Detail(o.personalNumber) + li( + div( + cls := "relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + img( + cls := "h-10 w-10 rounded-full", + src := "https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80", + alt := "" + ) + ), + div( + cls := "flex-1 min-w-0", + a( + href := router.absoluteUrlForPage(page), + navigateTo(page), + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.name + ), + p( + cls := "text-sm text-gray-500 truncate", + o.mainFunction + ) + ) + ) + ) + ) + + private def searchForm: HtmlElement = + form( + cls := "p-4 mt-6 flex space-x-4", + action := "#", + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + """Search""" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Search" + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter, + span( + cls := "sr-only", + """Search""" + ) + ) )