diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala deleted file mode 100644 index 0f54b7a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.airstream.core.EventStream -import com.raquo.laminar.api.L.{_, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Parametr -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.Loading -import cz.e_bs.cmi.mdr.pdb.app.components.OsobaView -import cz.e_bs.cmi.mdr.pdb.app.components.list.BaseList -import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable -import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails - -case class DetailPage( - $input: EventStream[Osoba], - actionBus: Observer[Action], - $page: Signal[Page.Detail] -)(using router: Router[Page]) - extends AppPage: - override def pageContent: HtmlElement = - val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = $input.startWithNone - val $maybeOsoba = - $data.split(_ => ())((_, _, s) => renderView(s)) - val $pageChangeSignal = - $oscChangeSignal.map(FetchUserDetails.apply) - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $pageChangeSignal --> actionBus, - // $fetchedData --> (o => router.replaceState(Page.Detail(o))), - child <-- $maybeOsoba.map(_.getOrElse(Loading)) - ) - - private def renderView($osoba: Signal[Osoba]): HtmlElement = - given BaseList.AsRow[(Osoba, Parametr)] with - extension (d: (Osoba, Parametr)) - def asRow = d match { - case (os, param) => - BaseList.Row( - param.id, - param.nazev, - BaseList.Tag("Splněno", BaseList.Color.Green), - Nil, - BaseList.IconText( - p( - """do """, - time( - datetime := "2020-01-07", - "01.07.2020" - ) - ), - Icons.solid.calendar - ) - ) - } - - given Navigable[(Osoba, Parametr)] with - extension (x: (Osoba, Parametr)) - def navigate: Modifier[HtmlElement] = - Navigator.navigateTo[Page]( - Page.DetailParametru(x._1, x._2) - ) - - import BaseList.Row.given - - val parameterList = new BaseList[(Osoba, Parametr)] - with NavigableList[(Osoba, Parametr), Page] - - div( - cls := "flex flex-col space-y-4", - OsobaView($osoba), - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parameterList.render( - for { o <- $osoba } yield for { p <- o.parametry } yield o -> p - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala deleted file mode 100644 index 0f54b7a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.airstream.core.EventStream -import com.raquo.laminar.api.L.{_, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Parametr -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.Loading -import cz.e_bs.cmi.mdr.pdb.app.components.OsobaView -import cz.e_bs.cmi.mdr.pdb.app.components.list.BaseList -import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable -import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails - -case class DetailPage( - $input: EventStream[Osoba], - actionBus: Observer[Action], - $page: Signal[Page.Detail] -)(using router: Router[Page]) - extends AppPage: - override def pageContent: HtmlElement = - val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = $input.startWithNone - val $maybeOsoba = - $data.split(_ => ())((_, _, s) => renderView(s)) - val $pageChangeSignal = - $oscChangeSignal.map(FetchUserDetails.apply) - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $pageChangeSignal --> actionBus, - // $fetchedData --> (o => router.replaceState(Page.Detail(o))), - child <-- $maybeOsoba.map(_.getOrElse(Loading)) - ) - - private def renderView($osoba: Signal[Osoba]): HtmlElement = - given BaseList.AsRow[(Osoba, Parametr)] with - extension (d: (Osoba, Parametr)) - def asRow = d match { - case (os, param) => - BaseList.Row( - param.id, - param.nazev, - BaseList.Tag("Splněno", BaseList.Color.Green), - Nil, - BaseList.IconText( - p( - """do """, - time( - datetime := "2020-01-07", - "01.07.2020" - ) - ), - Icons.solid.calendar - ) - ) - } - - given Navigable[(Osoba, Parametr)] with - extension (x: (Osoba, Parametr)) - def navigate: Modifier[HtmlElement] = - Navigator.navigateTo[Page]( - Page.DetailParametru(x._1, x._2) - ) - - import BaseList.Row.given - - val parameterList = new BaseList[(Osoba, Parametr)] - with NavigableList[(Osoba, Parametr), Page] - - div( - cls := "flex flex-col space-y-4", - OsobaView($osoba), - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parameterList.render( - for { o <- $osoba } yield for { p <- o.parametry } yield o -> p - ) - ) - ) 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 fc3fe2d..8b13789 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 @@ -1,135 +1 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages -import com.raquo.laminar.api.L.{*, given} -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, Loading} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory - -case class DirectoryPage( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using - router: Router[Page] -) extends AppPage: - - override def pageContent: HtmlElement = - val data = $input.startWithNone - val $maybeDirectory = - data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) - val $actionSignal = EventStream.fromValue(FetchDirectory) - div( - cls := "max-w-7xl mx-auto", - //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", - searchForm, - $actionSignal --> actionBus, - // 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(child.text <-- s.map(_._1)) - ), - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - // TODO: zero / loading page - 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) = - inline def avatarImage = - Avatar(Val(o.img).signal).avatarImage(10) - - 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", - child <-- avatarImage - ), - 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""" - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala deleted file mode 100644 index 0f54b7a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.airstream.core.EventStream -import com.raquo.laminar.api.L.{_, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Parametr -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.Loading -import cz.e_bs.cmi.mdr.pdb.app.components.OsobaView -import cz.e_bs.cmi.mdr.pdb.app.components.list.BaseList -import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable -import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails - -case class DetailPage( - $input: EventStream[Osoba], - actionBus: Observer[Action], - $page: Signal[Page.Detail] -)(using router: Router[Page]) - extends AppPage: - override def pageContent: HtmlElement = - val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = $input.startWithNone - val $maybeOsoba = - $data.split(_ => ())((_, _, s) => renderView(s)) - val $pageChangeSignal = - $oscChangeSignal.map(FetchUserDetails.apply) - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $pageChangeSignal --> actionBus, - // $fetchedData --> (o => router.replaceState(Page.Detail(o))), - child <-- $maybeOsoba.map(_.getOrElse(Loading)) - ) - - private def renderView($osoba: Signal[Osoba]): HtmlElement = - given BaseList.AsRow[(Osoba, Parametr)] with - extension (d: (Osoba, Parametr)) - def asRow = d match { - case (os, param) => - BaseList.Row( - param.id, - param.nazev, - BaseList.Tag("Splněno", BaseList.Color.Green), - Nil, - BaseList.IconText( - p( - """do """, - time( - datetime := "2020-01-07", - "01.07.2020" - ) - ), - Icons.solid.calendar - ) - ) - } - - given Navigable[(Osoba, Parametr)] with - extension (x: (Osoba, Parametr)) - def navigate: Modifier[HtmlElement] = - Navigator.navigateTo[Page]( - Page.DetailParametru(x._1, x._2) - ) - - import BaseList.Row.given - - val parameterList = new BaseList[(Osoba, Parametr)] - with NavigableList[(Osoba, Parametr), Page] - - div( - cls := "flex flex-col space-y-4", - OsobaView($osoba), - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parameterList.render( - for { o <- $osoba } yield for { p <- o.parametry } yield o -> p - ) - ) - ) 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 fc3fe2d..8b13789 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 @@ -1,135 +1 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages -import com.raquo.laminar.api.L.{*, given} -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, Loading} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory - -case class DirectoryPage( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using - router: Router[Page] -) extends AppPage: - - override def pageContent: HtmlElement = - val data = $input.startWithNone - val $maybeDirectory = - data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) - val $actionSignal = EventStream.fromValue(FetchDirectory) - div( - cls := "max-w-7xl mx-auto", - //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", - searchForm, - $actionSignal --> actionBus, - // 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(child.text <-- s.map(_._1)) - ), - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - // TODO: zero / loading page - 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) = - inline def avatarImage = - Avatar(Val(o.img).signal).avatarImage(10) - - 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", - child <-- avatarImage - ), - 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""" - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..7d5b2c1 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala deleted file mode 100644 index 0f54b7a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.airstream.core.EventStream -import com.raquo.laminar.api.L.{_, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Parametr -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.Loading -import cz.e_bs.cmi.mdr.pdb.app.components.OsobaView -import cz.e_bs.cmi.mdr.pdb.app.components.list.BaseList -import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable -import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails - -case class DetailPage( - $input: EventStream[Osoba], - actionBus: Observer[Action], - $page: Signal[Page.Detail] -)(using router: Router[Page]) - extends AppPage: - override def pageContent: HtmlElement = - val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = $input.startWithNone - val $maybeOsoba = - $data.split(_ => ())((_, _, s) => renderView(s)) - val $pageChangeSignal = - $oscChangeSignal.map(FetchUserDetails.apply) - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $pageChangeSignal --> actionBus, - // $fetchedData --> (o => router.replaceState(Page.Detail(o))), - child <-- $maybeOsoba.map(_.getOrElse(Loading)) - ) - - private def renderView($osoba: Signal[Osoba]): HtmlElement = - given BaseList.AsRow[(Osoba, Parametr)] with - extension (d: (Osoba, Parametr)) - def asRow = d match { - case (os, param) => - BaseList.Row( - param.id, - param.nazev, - BaseList.Tag("Splněno", BaseList.Color.Green), - Nil, - BaseList.IconText( - p( - """do """, - time( - datetime := "2020-01-07", - "01.07.2020" - ) - ), - Icons.solid.calendar - ) - ) - } - - given Navigable[(Osoba, Parametr)] with - extension (x: (Osoba, Parametr)) - def navigate: Modifier[HtmlElement] = - Navigator.navigateTo[Page]( - Page.DetailParametru(x._1, x._2) - ) - - import BaseList.Row.given - - val parameterList = new BaseList[(Osoba, Parametr)] - with NavigableList[(Osoba, Parametr), Page] - - div( - cls := "flex flex-col space-y-4", - OsobaView($osoba), - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parameterList.render( - for { o <- $osoba } yield for { p <- o.parametry } yield o -> p - ) - ) - ) 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 fc3fe2d..8b13789 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 @@ -1,135 +1 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages -import com.raquo.laminar.api.L.{*, given} -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, Loading} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory - -case class DirectoryPage( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using - router: Router[Page] -) extends AppPage: - - override def pageContent: HtmlElement = - val data = $input.startWithNone - val $maybeDirectory = - data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) - val $actionSignal = EventStream.fromValue(FetchDirectory) - div( - cls := "max-w-7xl mx-auto", - //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", - searchForm, - $actionSignal --> actionBus, - // 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(child.text <-- s.map(_._1)) - ), - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - // TODO: zero / loading page - 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) = - inline def avatarImage = - Avatar(Val(o.img).signal).avatarImage(10) - - 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", - child <-- avatarImage - ), - 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""" - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..7d5b2c1 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") 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 new file mode 100644 index 0000000..c3b60ae --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def render($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)), + SeznamParametru.render($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala deleted file mode 100644 index 0f54b7a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.airstream.core.EventStream -import com.raquo.laminar.api.L.{_, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Parametr -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.Loading -import cz.e_bs.cmi.mdr.pdb.app.components.OsobaView -import cz.e_bs.cmi.mdr.pdb.app.components.list.BaseList -import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable -import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails - -case class DetailPage( - $input: EventStream[Osoba], - actionBus: Observer[Action], - $page: Signal[Page.Detail] -)(using router: Router[Page]) - extends AppPage: - override def pageContent: HtmlElement = - val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = $input.startWithNone - val $maybeOsoba = - $data.split(_ => ())((_, _, s) => renderView(s)) - val $pageChangeSignal = - $oscChangeSignal.map(FetchUserDetails.apply) - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $pageChangeSignal --> actionBus, - // $fetchedData --> (o => router.replaceState(Page.Detail(o))), - child <-- $maybeOsoba.map(_.getOrElse(Loading)) - ) - - private def renderView($osoba: Signal[Osoba]): HtmlElement = - given BaseList.AsRow[(Osoba, Parametr)] with - extension (d: (Osoba, Parametr)) - def asRow = d match { - case (os, param) => - BaseList.Row( - param.id, - param.nazev, - BaseList.Tag("Splněno", BaseList.Color.Green), - Nil, - BaseList.IconText( - p( - """do """, - time( - datetime := "2020-01-07", - "01.07.2020" - ) - ), - Icons.solid.calendar - ) - ) - } - - given Navigable[(Osoba, Parametr)] with - extension (x: (Osoba, Parametr)) - def navigate: Modifier[HtmlElement] = - Navigator.navigateTo[Page]( - Page.DetailParametru(x._1, x._2) - ) - - import BaseList.Row.given - - val parameterList = new BaseList[(Osoba, Parametr)] - with NavigableList[(Osoba, Parametr), Page] - - div( - cls := "flex flex-col space-y-4", - OsobaView($osoba), - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parameterList.render( - for { o <- $osoba } yield for { p <- o.parametry } yield o -> p - ) - ) - ) 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 fc3fe2d..8b13789 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 @@ -1,135 +1 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages -import com.raquo.laminar.api.L.{*, given} -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, Loading} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory - -case class DirectoryPage( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using - router: Router[Page] -) extends AppPage: - - override def pageContent: HtmlElement = - val data = $input.startWithNone - val $maybeDirectory = - data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) - val $actionSignal = EventStream.fromValue(FetchDirectory) - div( - cls := "max-w-7xl mx-auto", - //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", - searchForm, - $actionSignal --> actionBus, - // 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(child.text <-- s.map(_._1)) - ), - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - // TODO: zero / loading page - 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) = - inline def avatarImage = - Avatar(Val(o.img).signal).avatarImage(10) - - 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", - child <-- avatarImage - ), - 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""" - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..7d5b2c1 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") 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 new file mode 100644 index 0000000..c3b60ae --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def render($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)), + SeznamParametru.render($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..4d90453 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,82 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +import java.time.LocalDate +import cz.e_bs.cmi.mdr.pdb.app.components.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-5", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala deleted file mode 100644 index 0f54b7a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.airstream.core.EventStream -import com.raquo.laminar.api.L.{_, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Parametr -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.Loading -import cz.e_bs.cmi.mdr.pdb.app.components.OsobaView -import cz.e_bs.cmi.mdr.pdb.app.components.list.BaseList -import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable -import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails - -case class DetailPage( - $input: EventStream[Osoba], - actionBus: Observer[Action], - $page: Signal[Page.Detail] -)(using router: Router[Page]) - extends AppPage: - override def pageContent: HtmlElement = - val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = $input.startWithNone - val $maybeOsoba = - $data.split(_ => ())((_, _, s) => renderView(s)) - val $pageChangeSignal = - $oscChangeSignal.map(FetchUserDetails.apply) - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $pageChangeSignal --> actionBus, - // $fetchedData --> (o => router.replaceState(Page.Detail(o))), - child <-- $maybeOsoba.map(_.getOrElse(Loading)) - ) - - private def renderView($osoba: Signal[Osoba]): HtmlElement = - given BaseList.AsRow[(Osoba, Parametr)] with - extension (d: (Osoba, Parametr)) - def asRow = d match { - case (os, param) => - BaseList.Row( - param.id, - param.nazev, - BaseList.Tag("Splněno", BaseList.Color.Green), - Nil, - BaseList.IconText( - p( - """do """, - time( - datetime := "2020-01-07", - "01.07.2020" - ) - ), - Icons.solid.calendar - ) - ) - } - - given Navigable[(Osoba, Parametr)] with - extension (x: (Osoba, Parametr)) - def navigate: Modifier[HtmlElement] = - Navigator.navigateTo[Page]( - Page.DetailParametru(x._1, x._2) - ) - - import BaseList.Row.given - - val parameterList = new BaseList[(Osoba, Parametr)] - with NavigableList[(Osoba, Parametr), Page] - - div( - cls := "flex flex-col space-y-4", - OsobaView($osoba), - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parameterList.render( - for { o <- $osoba } yield for { p <- o.parametry } yield o -> p - ) - ) - ) 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 fc3fe2d..8b13789 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 @@ -1,135 +1 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages -import com.raquo.laminar.api.L.{*, given} -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, Loading} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory - -case class DirectoryPage( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using - router: Router[Page] -) extends AppPage: - - override def pageContent: HtmlElement = - val data = $input.startWithNone - val $maybeDirectory = - data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) - val $actionSignal = EventStream.fromValue(FetchDirectory) - div( - cls := "max-w-7xl mx-auto", - //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", - searchForm, - $actionSignal --> actionBus, - // 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(child.text <-- s.map(_._1)) - ), - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - // TODO: zero / loading page - 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) = - inline def avatarImage = - Avatar(Val(o.img).signal).avatarImage(10) - - 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", - child <-- avatarImage - ), - 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""" - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..7d5b2c1 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") 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 new file mode 100644 index 0000000..c3b60ae --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def render($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)), + SeznamParametru.render($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..4d90453 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,82 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +import java.time.LocalDate +import cz.e_bs.cmi.mdr.pdb.app.components.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-5", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) 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 new file mode 100644 index 0000000..c47dad4 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,42 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import cz.e_bs.cmi.mdr.pdb.app.components.Color + +object SeznamParametru: + case class Parametr( + id: String, + nazev: String, + status: String, + statusColor: Color + ) + type ViewModel = List[Parametr] + + private val parametrList = new StackedList[Parametr] + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList.render($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = a() + ) + } + } + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala deleted file mode 100644 index 0f54b7a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.airstream.core.EventStream -import com.raquo.laminar.api.L.{_, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Parametr -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.Loading -import cz.e_bs.cmi.mdr.pdb.app.components.OsobaView -import cz.e_bs.cmi.mdr.pdb.app.components.list.BaseList -import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable -import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails - -case class DetailPage( - $input: EventStream[Osoba], - actionBus: Observer[Action], - $page: Signal[Page.Detail] -)(using router: Router[Page]) - extends AppPage: - override def pageContent: HtmlElement = - val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = $input.startWithNone - val $maybeOsoba = - $data.split(_ => ())((_, _, s) => renderView(s)) - val $pageChangeSignal = - $oscChangeSignal.map(FetchUserDetails.apply) - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $pageChangeSignal --> actionBus, - // $fetchedData --> (o => router.replaceState(Page.Detail(o))), - child <-- $maybeOsoba.map(_.getOrElse(Loading)) - ) - - private def renderView($osoba: Signal[Osoba]): HtmlElement = - given BaseList.AsRow[(Osoba, Parametr)] with - extension (d: (Osoba, Parametr)) - def asRow = d match { - case (os, param) => - BaseList.Row( - param.id, - param.nazev, - BaseList.Tag("Splněno", BaseList.Color.Green), - Nil, - BaseList.IconText( - p( - """do """, - time( - datetime := "2020-01-07", - "01.07.2020" - ) - ), - Icons.solid.calendar - ) - ) - } - - given Navigable[(Osoba, Parametr)] with - extension (x: (Osoba, Parametr)) - def navigate: Modifier[HtmlElement] = - Navigator.navigateTo[Page]( - Page.DetailParametru(x._1, x._2) - ) - - import BaseList.Row.given - - val parameterList = new BaseList[(Osoba, Parametr)] - with NavigableList[(Osoba, Parametr), Page] - - div( - cls := "flex flex-col space-y-4", - OsobaView($osoba), - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parameterList.render( - for { o <- $osoba } yield for { p <- o.parametry } yield o -> p - ) - ) - ) 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 fc3fe2d..8b13789 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 @@ -1,135 +1 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages -import com.raquo.laminar.api.L.{*, given} -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, Loading} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory - -case class DirectoryPage( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using - router: Router[Page] -) extends AppPage: - - override def pageContent: HtmlElement = - val data = $input.startWithNone - val $maybeDirectory = - data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) - val $actionSignal = EventStream.fromValue(FetchDirectory) - div( - cls := "max-w-7xl mx-auto", - //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", - searchForm, - $actionSignal --> actionBus, - // 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(child.text <-- s.map(_._1)) - ), - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - // TODO: zero / loading page - 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) = - inline def avatarImage = - Avatar(Val(o.img).signal).avatarImage(10) - - 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", - child <-- avatarImage - ), - 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""" - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..7d5b2c1 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") 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 new file mode 100644 index 0000000..c3b60ae --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def render($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)), + SeznamParametru.render($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..4d90453 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,82 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +import java.time.LocalDate +import cz.e_bs.cmi.mdr.pdb.app.components.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-5", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) 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 new file mode 100644 index 0000000..c47dad4 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,42 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import cz.e_bs.cmi.mdr.pdb.app.components.Color + +object SeznamParametru: + case class Parametr( + id: String, + nazev: String, + status: String, + statusColor: Color + ) + type ViewModel = List[Parametr] + + private val parametrList = new StackedList[Parametr] + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList.render($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = a() + ) + } + } + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..75f276e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,26 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.directory + +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.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserInfo] + + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + val byLetter = for { + d <- $m + } yield for { + (letter, users) <- d.groupBy(_.surname.head).to(List).sortBy(_._1) + } yield (letter.toString, users.sortBy(_.surname)) + + div( + cls := "max-w-7xl mx-auto", + //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", + SearchForm.render, + Directory.render(byLetter) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala deleted file mode 100644 index 0f54b7a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.airstream.core.EventStream -import com.raquo.laminar.api.L.{_, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Parametr -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.Loading -import cz.e_bs.cmi.mdr.pdb.app.components.OsobaView -import cz.e_bs.cmi.mdr.pdb.app.components.list.BaseList -import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable -import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails - -case class DetailPage( - $input: EventStream[Osoba], - actionBus: Observer[Action], - $page: Signal[Page.Detail] -)(using router: Router[Page]) - extends AppPage: - override def pageContent: HtmlElement = - val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = $input.startWithNone - val $maybeOsoba = - $data.split(_ => ())((_, _, s) => renderView(s)) - val $pageChangeSignal = - $oscChangeSignal.map(FetchUserDetails.apply) - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $pageChangeSignal --> actionBus, - // $fetchedData --> (o => router.replaceState(Page.Detail(o))), - child <-- $maybeOsoba.map(_.getOrElse(Loading)) - ) - - private def renderView($osoba: Signal[Osoba]): HtmlElement = - given BaseList.AsRow[(Osoba, Parametr)] with - extension (d: (Osoba, Parametr)) - def asRow = d match { - case (os, param) => - BaseList.Row( - param.id, - param.nazev, - BaseList.Tag("Splněno", BaseList.Color.Green), - Nil, - BaseList.IconText( - p( - """do """, - time( - datetime := "2020-01-07", - "01.07.2020" - ) - ), - Icons.solid.calendar - ) - ) - } - - given Navigable[(Osoba, Parametr)] with - extension (x: (Osoba, Parametr)) - def navigate: Modifier[HtmlElement] = - Navigator.navigateTo[Page]( - Page.DetailParametru(x._1, x._2) - ) - - import BaseList.Row.given - - val parameterList = new BaseList[(Osoba, Parametr)] - with NavigableList[(Osoba, Parametr), Page] - - div( - cls := "flex flex-col space-y-4", - OsobaView($osoba), - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parameterList.render( - for { o <- $osoba } yield for { p <- o.parametry } yield o -> p - ) - ) - ) 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 fc3fe2d..8b13789 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 @@ -1,135 +1 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages -import com.raquo.laminar.api.L.{*, given} -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, Loading} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory - -case class DirectoryPage( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using - router: Router[Page] -) extends AppPage: - - override def pageContent: HtmlElement = - val data = $input.startWithNone - val $maybeDirectory = - data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) - val $actionSignal = EventStream.fromValue(FetchDirectory) - div( - cls := "max-w-7xl mx-auto", - //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", - searchForm, - $actionSignal --> actionBus, - // 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(child.text <-- s.map(_._1)) - ), - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - // TODO: zero / loading page - 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) = - inline def avatarImage = - Avatar(Val(o.img).signal).avatarImage(10) - - 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", - child <-- avatarImage - ), - 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""" - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..7d5b2c1 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") 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 new file mode 100644 index 0000000..c3b60ae --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def render($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)), + SeznamParametru.render($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..4d90453 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,82 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +import java.time.LocalDate +import cz.e_bs.cmi.mdr.pdb.app.components.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-5", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) 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 new file mode 100644 index 0000000..c47dad4 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,42 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import cz.e_bs.cmi.mdr.pdb.app.components.Color + +object SeznamParametru: + case class Parametr( + id: String, + nazev: String, + status: String, + statusColor: Color + ) + type ViewModel = List[Parametr] + + private val parametrList = new StackedList[Parametr] + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList.render($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = a() + ) + } + } + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..75f276e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,26 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.directory + +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.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserInfo] + + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + val byLetter = for { + d <- $m + } yield for { + (letter, users) <- d.groupBy(_.surname.head).to(List).sortBy(_._1) + } yield (letter.toString, users.sortBy(_.surname)) + + div( + cls := "max-w-7xl mx-auto", + //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", + SearchForm.render, + Directory.render(byLetter) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..82cf1b6 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,41 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.UserInfo +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Page + +object Directory: + + object Header: + type ViewModel = String + def render($m: Signal[ViewModel]): HtmlElement = + 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(child.text <-- $m) + ) + + import com.raquo.laminar.api.L.{*, given} + + object UserList: + type ViewModel = List[UserInfo] + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.username)((_, _, s) => UserRow.render(s)) + ) + + type ViewModel = List[(String, List[UserInfo])] + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header.render(s.map(_._1)), + UserList.render(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala deleted file mode 100644 index 0f54b7a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.airstream.core.EventStream -import com.raquo.laminar.api.L.{_, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Parametr -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.Loading -import cz.e_bs.cmi.mdr.pdb.app.components.OsobaView -import cz.e_bs.cmi.mdr.pdb.app.components.list.BaseList -import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable -import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails - -case class DetailPage( - $input: EventStream[Osoba], - actionBus: Observer[Action], - $page: Signal[Page.Detail] -)(using router: Router[Page]) - extends AppPage: - override def pageContent: HtmlElement = - val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = $input.startWithNone - val $maybeOsoba = - $data.split(_ => ())((_, _, s) => renderView(s)) - val $pageChangeSignal = - $oscChangeSignal.map(FetchUserDetails.apply) - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $pageChangeSignal --> actionBus, - // $fetchedData --> (o => router.replaceState(Page.Detail(o))), - child <-- $maybeOsoba.map(_.getOrElse(Loading)) - ) - - private def renderView($osoba: Signal[Osoba]): HtmlElement = - given BaseList.AsRow[(Osoba, Parametr)] with - extension (d: (Osoba, Parametr)) - def asRow = d match { - case (os, param) => - BaseList.Row( - param.id, - param.nazev, - BaseList.Tag("Splněno", BaseList.Color.Green), - Nil, - BaseList.IconText( - p( - """do """, - time( - datetime := "2020-01-07", - "01.07.2020" - ) - ), - Icons.solid.calendar - ) - ) - } - - given Navigable[(Osoba, Parametr)] with - extension (x: (Osoba, Parametr)) - def navigate: Modifier[HtmlElement] = - Navigator.navigateTo[Page]( - Page.DetailParametru(x._1, x._2) - ) - - import BaseList.Row.given - - val parameterList = new BaseList[(Osoba, Parametr)] - with NavigableList[(Osoba, Parametr), Page] - - div( - cls := "flex flex-col space-y-4", - OsobaView($osoba), - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parameterList.render( - for { o <- $osoba } yield for { p <- o.parametry } yield o -> p - ) - ) - ) 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 fc3fe2d..8b13789 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 @@ -1,135 +1 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages -import com.raquo.laminar.api.L.{*, given} -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, Loading} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory - -case class DirectoryPage( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using - router: Router[Page] -) extends AppPage: - - override def pageContent: HtmlElement = - val data = $input.startWithNone - val $maybeDirectory = - data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) - val $actionSignal = EventStream.fromValue(FetchDirectory) - div( - cls := "max-w-7xl mx-auto", - //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", - searchForm, - $actionSignal --> actionBus, - // 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(child.text <-- s.map(_._1)) - ), - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - // TODO: zero / loading page - 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) = - inline def avatarImage = - Avatar(Val(o.img).signal).avatarImage(10) - - 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", - child <-- avatarImage - ), - 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""" - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..7d5b2c1 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") 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 new file mode 100644 index 0000000..c3b60ae --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def render($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)), + SeznamParametru.render($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..4d90453 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,82 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +import java.time.LocalDate +import cz.e_bs.cmi.mdr.pdb.app.components.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-5", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) 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 new file mode 100644 index 0000000..c47dad4 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,42 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import cz.e_bs.cmi.mdr.pdb.app.components.Color + +object SeznamParametru: + case class Parametr( + id: String, + nazev: String, + status: String, + statusColor: Color + ) + type ViewModel = List[Parametr] + + private val parametrList = new StackedList[Parametr] + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList.render($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = a() + ) + } + } + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..75f276e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,26 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.directory + +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.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserInfo] + + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + val byLetter = for { + d <- $m + } yield for { + (letter, users) <- d.groupBy(_.surname.head).to(List).sortBy(_._1) + } yield (letter.toString, users.sortBy(_.surname)) + + div( + cls := "max-w-7xl mx-auto", + //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", + SearchForm.render, + Directory.render(byLetter) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..82cf1b6 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,41 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.UserInfo +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Page + +object Directory: + + object Header: + type ViewModel = String + def render($m: Signal[ViewModel]): HtmlElement = + 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(child.text <-- $m) + ) + + import com.raquo.laminar.api.L.{*, given} + + object UserList: + type ViewModel = List[UserInfo] + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.username)((_, _, s) => UserRow.render(s)) + ) + + type ViewModel = List[(String, List[UserInfo])] + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header.render(s.map(_._1)), + UserList.render(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..e40ada3 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,42 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.Icons + +object SearchForm: + def render: 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""" + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala index 582cff9..3bf5c69 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/AppState.scala @@ -12,7 +12,7 @@ trait AppState: def users: EventStream[List[UserInfo]] - def details: EventStream[Osoba] + def details: EventStream[UserInfo] def actionBus: Observer[Action] class MockAppState(implicit owner: Owner, router: Router[Page]) @@ -24,7 +24,7 @@ private val actions = EventBus[Action]() private val (usersStream, pushUsers) = EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[Osoba] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] private val mockData = mockUsers @@ -40,9 +40,10 @@ actions.events.foreach { case FetchDirectory => pushUsers(mockData) case FetchUserDetails(osc) => - val o = ExampleData.persons.jmeistrova.copy(osobniCislo = osc) - pushDetails(o) - router.replaceState(Page.Detail(o)) + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } case NavigateTo(page) => router.pushState(page) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala deleted file mode 100644 index 8699fbe..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/ExampleData.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate - -object ExampleData: - object persons: - val jmeistrova = - Osoba( - OsobniCislo("60308"), - "Ing. Jana Meistrová", - "jmeistrova@cmi.cz", - "+420222866180", - None, - Funkce( - "manažerka jakosti ČMI", - "generální ředitel, MJ ČMI", - "úsek generálního ředitele" - ), - PracovniPomer("HPP", LocalDate.of(2005, 7, 1), None), - List( - Parametr( - "1", - "Komise pro pověřování pracovníků", - Nil - ) - ) - ) 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 c612f6f..70a7c64 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 @@ -54,11 +54,11 @@ def renderPage(state: AppState)(using router: Router[Page]): HtmlElement = val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) .collectSignal[Page.Detail]( - pages - .DetailPage(state.details, state.actionBus, _) + connectors + .DetailPageConnector(state.details, state.actionBus, _) .render ) - .collectStatic(Page.Dashboard)(pages.DashboardPage().render) + .collectStatic(Page.Dashboard)(connectors.DashboardPageConnector().render) .collect[Page.NotFound](pg => pages.errors.NotFoundPage(Routes.homePage, pg.url) ) @@ -71,8 +71,8 @@ ) ) .collectStatic(Page.Directory)( - pages - .DirectoryPage(state.users, state.actionBus) + connectors + .DirectoryPageConnector(state.users, state.actionBus) .render ) div(child <-- pageSplitter.$view) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala index 24702b8..e37aa8a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Osoba.scala @@ -26,26 +26,3 @@ nazev: String, kriteria: List[Kriterium] ) - -case class PracovniPomer( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] -) - -case class Funkce( - nazev: String, - stredisko: String, - voj: String -) - -case class Osoba( - osobniCislo: OsobniCislo, - jmeno: String, - email: String, - telefon: String, - img: Option[String], - hlavniFunkce: Funkce, - pracovniPomer: PracovniPomer, - parametry: List[Parametr] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala index 55cb1f0..de29954 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala @@ -7,6 +7,7 @@ import cz.e_bs.cmi.mdr.pdb.OsobniCislo import scala.scalajs.js +import cz.e_bs.cmi.mdr.pdb.UserInfo // enum is not working with Waypoints' SplitRender collectStatic sealed abstract class Page(val title: String, val parent: Option[Page]) @@ -21,7 +22,7 @@ extends Page(jmenoOsoby.getOrElse("Detail"), Some(Directory)) object Detail { - def apply(o: Osoba): Detail = Detail(o.osobniCislo, Some(o.jmeno)) + def apply(o: UserInfo): Detail = Detail(o.personalNumber, Some(o.name)) } case class DetailParametru( @@ -35,8 +36,8 @@ ) object DetailParametru { - def apply(o: Osoba, p: Parametr): DetailParametru = - DetailParametru(o.osobniCislo, p.id, Some(o.jmeno), Some(p.nazev)) + def apply(o: UserInfo, p: Parametr): DetailParametru = + DetailParametru(o.personalNumber, p.id, Some(o.name), Some(p.nazev)) } case class NotFound(url: String) extends Page("404", Some(Directory)) 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 ab68c35..ea5600a 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 @@ -4,8 +4,9 @@ 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 -trait AppPage +class AppPage(using router: Router[Page]) extends PageLayout with PageHeader with Breadcrumbs diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala new file mode 100644 index 0000000..9aea064 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Color.scala @@ -0,0 +1,77 @@ +package cz.e_bs.cmi.mdr.pdb.app.components + +enum ColorWeight(value: String): + inline def toCSS: String = value + // To be used for black and white until + // we support better mechanism via extension methods + case w__ extends ColorWeight("") + case w50 extends ColorWeight("50") + case w100 extends ColorWeight("100") + case w200 extends ColorWeight("200") + case w300 extends ColorWeight("300") + case w400 extends ColorWeight("400") + case w500 extends ColorWeight("500") + case w600 extends ColorWeight("600") + case w700 extends ColorWeight("700") + case w800 extends ColorWeight("800") + case w900 extends ColorWeight("900") + +enum Color(name: String): + import ColorWeight._ + + inline def toCSSNoColorWeight(prefix: String): String = + s"${prefix}-${name}" + inline def toCSSWithColorWeight( + prefix: String, + weight: ColorWeight + ): String = + s"${prefix}-${name}-${weight.toCSS}" + inline def toCSS(prefix: String)(weight: ColorWeight): String = + weight match { + case `w__` => toCSSNoColorWeight(prefix) + case _ => toCSSWithColorWeight(prefix, weight) + } + inline def bg: ColorWeight => String = toCSS("bg")(_) + inline def text: ColorWeight => String = toCSS("text")(_) + inline def decoration: ColorWeight => String = toCSS("decoration")(_) + inline def border: ColorWeight => String = toCSS("border")(_) + inline def outline: ColorWeight => String = toCSS("outline")(_) + inline def divide: ColorWeight => String = toCSS("divide")(_) + inline def ring: ColorWeight => String = toCSS("ring")(_) + inline def ringOffset: ColorWeight => String = toCSS("ring-offset")(_) + inline def shadow: ColorWeight => String = toCSS("shadow")(_) + inline def accent: ColorWeight => String = toCSS("accent")(_) + + // TODO: change the "stupid" methods to extension methods + // that will keep the invariants in comments lower + case current extends Color("current") + case inherit extends Color("inherit") + // Not present in for all methods + case transp extends Color("transparent") + // Seen in accent, not preset otherwise + case auto extends Color("auto") + // Black and white do not have weight + case black extends Color("black") + case white extends Color("white") + case slate extends Color("slate") + case gray extends Color("gray") + case zinc extends Color("zinc") + case neutral extends Color("neutral") + case stone extends Color("stone") + case red extends Color("red") + case orange extends Color("orange") + case amber extends Color("amber") + case yellow extends Color("yellow") + case lime extends Color("lime") + case green extends Color("green") + case emerald extends Color("emerald") + case teal extends Color("teal") + case cyan extends Color("cyan") + case sky extends Color("sky") + case blue extends Color("blue") + case indigo extends Color("indigo") + case violet extends Color("violet") + case purple extends Color("purple") + case fuchsia extends Color("fuchsia") + case pink extends Color("pink") + case rose extends Color("rose") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala deleted file mode 100644 index 133cccf..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/OsobaView.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Funkce -import cz.e_bs.cmi.mdr.pdb.app.PracovniPomer -import CustomAttrs.datetime - -// TODO: refactor to view model -def OsobaView($osoba: Signal[Osoba]): HtmlElement = - def funkce($fce: Signal[Funkce]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $fce.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $fce.map(_.stredisko), - ", ", - child.text <-- $fce.map(_.voj) - ) - ) - - def pp($pp: Signal[PracovniPomer]) = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $pp.map(_.druh), - " od ", - time( - datetime <-- $pp.map(_.pocatek.toString), - child.text <-- $pp.map(_.pocatek.toString) - ) - ) - - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-5", - div( - cls := "flex-shrink-0", - Avatar($osoba.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $osoba.map(_.jmeno) - ), - funkce($osoba.map(_.hlavniFunkce)), - pp($osoba.map(_.pracovniPomer)) - ) - ) - ) 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 fcb89aa..006efa0 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 @@ -5,13 +5,19 @@ trait PageLayout { def navigation: HtmlElement def pageHeader: HtmlElement - def pageContent: HtmlElement - def render: HtmlElement = + def render( + $m: Signal[Option[HtmlElement]], + mods: Modifier[HtmlElement]* + ): HtmlElement = + val $maybeContent = $m.split(_ => ())((_, c, _) => c) div( cls := "min-h-full", navigation, pageHeader, - main(pageContent) + main( + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) ) } diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala new file mode 100644 index 0000000..13f747d --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/IconText.scala @@ -0,0 +1,19 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom +import com.raquo.laminar.builders.HtmlTag + +object IconText: + case class ViewModel(text: HtmlElement, icon: SvgElement) + def render($m: Signal[ViewModel]): HtmlElement = render($m, div) + def render( + $m: Signal[ViewModel], + container: HtmlTag[dom.html.Element] + ): HtmlElement = + container( + cls := "flex items-center text-sm text-gray-500", + child <-- $m.map(_.icon), + child <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..26a70ea --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/ListRow.scala @@ -0,0 +1,46 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} +import org.scalajs.dom + +object ListRow: + case class ViewModel( + title: String, + topRight: Modifier[HtmlElement], + bottomLeft: Modifier[HtmlElement], + bottomRight: Modifier[HtmlElement], + farRight: Modifier[HtmlElement], + containerElement: HtmlElement = div() + ) + + def render($m: Signal[ViewModel]): HtmlElement = + li( + child <-- $m.map(m => + m.containerElement.amend( + cls := "block hover:bg-gray-50", + div( + cls := "px-4 py-4 sm:px-6 items-center flex", + div( + cls := "min-w-0 flex-1 pr-4", + div( + cls := "flex items-center justify-between", + p( + cls := "text-sm font-medium text-indigo-600 truncate", + m.title + ), + div( + cls := "ml-2 flex-shrink-0 flex", + m.topRight + ) + ), + div( + cls := "mt-2 sm:flex sm:justify-between", + m.bottomLeft, + m.bottomRight + ) + ), + m.farRight + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala new file mode 100644 index 0000000..9c6c5eb --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/PropList.scala @@ -0,0 +1,16 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object PropList: + type ViewModel = List[HtmlElement] + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "sm:flex", + children <-- $m.map(_.zipWithIndex.map { case (i, idx) => + i.amend( + cls := Map("mt-2 sm:mt-0 sm:ml-6" -> (idx == 0)) + ) + }) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala new file mode 100644 index 0000000..05e571e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowNext.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowNext: + def render: HtmlElement = + div( + cls := "flex-shrink-0", + Icons.solid.`chevron-right` + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala new file mode 100644 index 0000000..cc85a37 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/RowTag.scala @@ -0,0 +1,17 @@ +package cz.e_bs.cmi.mdr.pdb.app.components +package list + +import com.raquo.laminar.api.L.{*, given} + +object RowTag: + case class ViewModel(text: String, color: Color) + def render($m: Signal[ViewModel]): HtmlElement = + inline def colorClass(color: Color): Seq[String] = + import ColorWeight._ + List(color.bg(w100), color.text(w800)) + + p( + cls := "px-2 inline-flex text-xs leading-5 font-semibold rounded-full", + cls <-- $m.map(t => colorClass(t.color)), + child.text <-- $m.map(_.text) + ) 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 new file mode 100644 index 0000000..984282a --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/list/StackedList.scala @@ -0,0 +1,15 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.list + +import com.raquo.laminar.api.L.{*, given} + +class StackedList[Item]: + type ViewModel = List[Item] + def render( + $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))) + ) 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 new file mode 100644 index 0000000..0f89d66 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage +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))) 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 new file mode 100644 index 0000000..91937ac --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,44 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +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.pages.detail.components.DetailOsoby + +case class DetailPageConnector( + $input: EventStream[UserInfo], + actionBus: Observer[Action], + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = $input.startWithNone + val $pageChangeSignal = + $oscChangeSignal.map(FetchUserDetails.apply) + + def render: HtmlElement = + AppPage().render( + $data.map( + _.map(o => + DetailPage.ViewModel( + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + None, + None + ), + Nil + ) + ) + ).split(_ => ())((_, _, s) => DetailPage.render(s)), + $pageChangeSignal --> 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 new file mode 100644 index 0000000..0038e88 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app +package connectors + +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.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def render: HtmlElement = + AppPage().render( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage.render(s) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala deleted file mode 100644 index cc59bee..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DashboardPage.scala +++ /dev/null @@ -1,10 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -class DashboardPage(using router: Router[Page]) extends AppPage: - override def pageContent: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala deleted file mode 100644 index 0f54b7a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/DetailPage.scala +++ /dev/null @@ -1,90 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages - -import com.raquo.airstream.core.EventStream -import com.raquo.laminar.api.L.{_, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Osoba -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Parametr -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime -import cz.e_bs.cmi.mdr.pdb.app.components.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.Loading -import cz.e_bs.cmi.mdr.pdb.app.components.OsobaView -import cz.e_bs.cmi.mdr.pdb.app.components.list.BaseList -import cz.e_bs.cmi.mdr.pdb.app.components.list.Navigable -import cz.e_bs.cmi.mdr.pdb.app.components.list.NavigableList -import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchUserDetails - -case class DetailPage( - $input: EventStream[Osoba], - actionBus: Observer[Action], - $page: Signal[Page.Detail] -)(using router: Router[Page]) - extends AppPage: - override def pageContent: HtmlElement = - val $oscChangeSignal = $page.splitOne(_.osobniCislo)((osc, _, _) => osc) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = $input.startWithNone - val $maybeOsoba = - $data.split(_ => ())((_, _, s) => renderView(s)) - val $pageChangeSignal = - $oscChangeSignal.map(FetchUserDetails.apply) - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - $pageChangeSignal --> actionBus, - // $fetchedData --> (o => router.replaceState(Page.Detail(o))), - child <-- $maybeOsoba.map(_.getOrElse(Loading)) - ) - - private def renderView($osoba: Signal[Osoba]): HtmlElement = - given BaseList.AsRow[(Osoba, Parametr)] with - extension (d: (Osoba, Parametr)) - def asRow = d match { - case (os, param) => - BaseList.Row( - param.id, - param.nazev, - BaseList.Tag("Splněno", BaseList.Color.Green), - Nil, - BaseList.IconText( - p( - """do """, - time( - datetime := "2020-01-07", - "01.07.2020" - ) - ), - Icons.solid.calendar - ) - ) - } - - given Navigable[(Osoba, Parametr)] with - extension (x: (Osoba, Parametr)) - def navigate: Modifier[HtmlElement] = - Navigator.navigateTo[Page]( - Page.DetailParametru(x._1, x._2) - ) - - import BaseList.Row.given - - val parameterList = new BaseList[(Osoba, Parametr)] - with NavigableList[(Osoba, Parametr), Page] - - div( - cls := "flex flex-col space-y-4", - OsobaView($osoba), - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parameterList.render( - for { o <- $osoba } yield for { p <- o.parametry } yield o -> p - ) - ) - ) 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 fc3fe2d..8b13789 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 @@ -1,135 +1 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages -import com.raquo.laminar.api.L.{*, given} -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, Loading} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.Avatar -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.FetchDirectory - -case class DirectoryPage( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using - router: Router[Page] -) extends AppPage: - - override def pageContent: HtmlElement = - val data = $input.startWithNone - val $maybeDirectory = - data.signal.split(_ => ())((_, _, s) => renderDirectory(s)) - val $actionSignal = EventStream.fromValue(FetchDirectory) - div( - cls := "max-w-7xl mx-auto", - //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", - searchForm, - $actionSignal --> actionBus, - // 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(child.text <-- s.map(_._1)) - ), - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - // TODO: zero / loading page - 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) = - inline def avatarImage = - Avatar(Val(o.img).signal).avatarImage(10) - - 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", - child <-- avatarImage - ), - 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""" - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..7d5b2c1 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.AppPage +import cz.e_bs.cmi.mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") 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 new file mode 100644 index 0000000..c3b60ae --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,22 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def render($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)), + SeznamParametru.render($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..4d90453 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,82 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.OsobniCislo + +import java.time.LocalDate +import cz.e_bs.cmi.mdr.pdb.app.components.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import cz.e_bs.cmi.mdr.pdb.app.components.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-5", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) 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 new file mode 100644 index 0000000..c47dad4 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,42 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import cz.e_bs.cmi.mdr.pdb.app.components.Color + +object SeznamParametru: + case class Parametr( + id: String, + nazev: String, + status: String, + statusColor: Color + ) + type ViewModel = List[Parametr] + + private val parametrList = new StackedList[Parametr] + + def render($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList.render($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = a() + ) + } + } + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..75f276e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,26 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.directory + +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.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserInfo] + + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + val byLetter = for { + d <- $m + } yield for { + (letter, users) <- d.groupBy(_.surname.head).to(List).sortBy(_._1) + } yield (letter.toString, users.sortBy(_.surname)) + + div( + cls := "max-w-7xl mx-auto", + //cls := "xl:order-first xl:flex xl:flex-col flex-shrink-0 w-96 border-r border-gray-200", + SearchForm.render, + Directory.render(byLetter) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..82cf1b6 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,41 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.UserInfo +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.Page + +object Directory: + + object Header: + type ViewModel = String + def render($m: Signal[ViewModel]): HtmlElement = + 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(child.text <-- $m) + ) + + import com.raquo.laminar.api.L.{*, given} + + object UserList: + type ViewModel = List[UserInfo] + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.username)((_, _, s) => UserRow.render(s)) + ) + + type ViewModel = List[(String, List[UserInfo])] + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header.render(s.map(_._1)), + UserList.render(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..e40ada3 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,42 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.app.components.Icons + +object SearchForm: + def render: 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""" + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..174b92e --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,50 @@ +package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import cz.e_bs.cmi.mdr.pdb.UserInfo +import com.raquo.airstream.core.Signal +import com.raquo.waypoint.Router +import cz.e_bs.cmi.mdr.pdb.app.components.Avatar +import cz.e_bs.cmi.mdr.pdb.app.Page +import cz.e_bs.cmi.mdr.pdb.waypoint.components.Navigator + +object UserRow: + type ViewModel = UserInfo + + sealed trait Action + case class Selected(value: ViewModel) extends Action + + def render($m: Signal[ViewModel])(using router: Router[Page]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + 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", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + a( + Navigator.navigateTo[Page](Page.Detail(o.personalNumber)), + 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 + ) + ) + } + ) + ) + )