diff options
author | Joris | 2017-02-24 09:44:16 +0100 |
---|---|---|
committer | Joris | 2017-02-24 09:44:16 +0100 |
commit | f1de0dd7632eb29a40ea1f5cf136ab43ee945926 (patch) | |
tree | 0e9045ffad0925afeaa6b8cbf07a6e0ff50f38d2 | |
parent | 626d5ee7ea4ea4f90637e453e0dc488fc6b1a19e (diff) |
Add search field
27 files changed, 422 insertions, 187 deletions
diff --git a/public/cover/Après la vague.jpg b/public/cover/Après la vague.jpg Binary files differindex 48e67de..1c103b1 100644 --- a/public/cover/Après la vague.jpg +++ b/public/cover/Après la vague.jpg diff --git a/public/cover/Bjorn le Morphir.jpg b/public/cover/Bjorn le Morphir.jpg Binary files differindex b572d48..e6775c9 100644 --- a/public/cover/Bjorn le Morphir.jpg +++ b/public/cover/Bjorn le Morphir.jpg diff --git a/public/cover/Les Orangers de Versailles.jpg b/public/cover/Les Orangers de Versailles.jpg Binary files differindex cb53018..ade3129 100644 --- a/public/cover/Les Orangers de Versailles.jpg +++ b/public/cover/Les Orangers de Versailles.jpg diff --git a/public/cover/Les Plus belles histoires du Prince de Motordu.jpg b/public/cover/Les Plus belles histoires du Prince de Motordu.jpg Binary files differindex 9826b6b..cf15296 100644 --- a/public/cover/Les Plus belles histoires du Prince de Motordu.jpg +++ b/public/cover/Les Plus belles histoires du Prince de Motordu.jpg diff --git a/public/cover/Caïus.jpg b/public/cover/L’affaire Caïus.jpg Binary files differindex 3a64ab1..3a64ab1 100644 --- a/public/cover/Caïus.jpg +++ b/public/cover/L’affaire Caïus.jpg diff --git a/public/cover/Max.jpg b/public/cover/Max.jpg Binary files differindex b11d1b5..36e584b 100644 --- a/public/cover/Max.jpg +++ b/public/cover/Max.jpg diff --git a/src/main/scala/reading/Main.scala b/src/main/scala/reading/Main.scala index 6104891..a9fc3e2 100644 --- a/src/main/scala/reading/Main.scala +++ b/src/main/scala/reading/Main.scala @@ -1,13 +1,13 @@ package reading import scala.scalajs.js.JSApp - +import rx._ +import rx.Ctx.Owner.Unsafe._ import org.scalajs.dom - import scalacss.Defaults._ import reading.component.style.{ Global => GlobalStyle } -import reading.utils.RxTag +import reading.utils.RxUtils._ object Main extends JSApp { def main(): Unit = { @@ -16,7 +16,7 @@ object Main extends JSApp { dom.document.head.appendChild(style) val _ = dom.document.body.appendChild { - RxTag { implicit context => + Rx { Route.current() match { case Route.Books(filters) => component.Index(filters) } diff --git a/src/main/scala/reading/component/Index.scala b/src/main/scala/reading/component/Index.scala index 57f5b4b..78890de 100644 --- a/src/main/scala/reading/component/Index.scala +++ b/src/main/scala/reading/component/Index.scala @@ -1,34 +1,30 @@ package reading.component import rx._ -import Ctx.Owner.Unsafe._ - -import scalatags.JsDom.all._ import scalacss.Defaults._ import scalacss.ScalatagsCss._ +import scalatags.JsDom.all._ -import reading.Books -import reading.component.style.{ Index => IndexStyle } import reading.component.index.{ Menu, Header, Books => BooksComponent } -import reading.models.{ Book, Filter } +import reading.component.style.{ Index => IndexStyle } +import reading.models.{ Book, Books, Filter } object Index { - def apply(initialFilters: Seq[Filter]): HtmlTag = { + def apply(initialFilters: Seq[Filter])(implicit ctx: Ctx.Owner): Frag = { val filters: Var[Seq[Filter]] = Var(initialFilters) - val books: Rx[Seq[Book]] = Rx { - if (filters().isEmpty) Books() else Book.filter(Books(), filters()) - } - val count: Rx[Int] = Rx(books().length) + val books: Rx[Seq[Book]] = Rx(Filter.add(Books(), filters())) + val search: Var[String] = Var("") val showFiltersMenu: Var[Boolean] = Var(false) + val searchedBooks: Rx[Seq[Book]] = Rx(Book.filter(books(), search())) div( IndexStyle.render, IndexStyle.page, - Menu(books, filters, showFiltersMenu), + Menu(books, filters, search, showFiltersMenu), div( IndexStyle.main, - Header(filters, showFiltersMenu, count), - BooksComponent(books) + Header(searchedBooks, filters, search, showFiltersMenu), + BooksComponent(searchedBooks) ) ) } diff --git a/src/main/scala/reading/component/index/BookDetail.scala b/src/main/scala/reading/component/index/BookDetail.scala index 7df771b..c42029f 100644 --- a/src/main/scala/reading/component/index/BookDetail.scala +++ b/src/main/scala/reading/component/index/BookDetail.scala @@ -8,7 +8,7 @@ import reading.component.index.style.{ BookDetail => BookStyle } import reading.models.{ Program, Book } object BookDetail { - def apply(book: Book): HtmlTag = + def apply(book: Book): Frag = div( BookStyle.render, BookStyle.detail, @@ -20,6 +20,8 @@ object BookDetail { ), div( + BookStyle.items, + if (book.programs.nonEmpty) { item("classe", book.programs.map(Program.grade(_).prettyPrint).distinct.sorted) }, @@ -33,11 +35,14 @@ object BookDetail { item("genre", book.genres.sorted.map(_.prettyPrint)) }, book.period.map { period => - item("période", Seq(period.prettyPrint)) - } + item("période", period.prettyPrint) + }, + item("niveau", book.level.prettyPrint) ) ) + private def item(key: String, value: String): Frag = item(key, Seq(value)) + private def item(key: String, values: Seq[String]): Frag = div( BookStyle.item, diff --git a/src/main/scala/reading/component/index/Books.scala b/src/main/scala/reading/component/index/Books.scala index 20b308d..c22639f 100644 --- a/src/main/scala/reading/component/index/Books.scala +++ b/src/main/scala/reading/component/index/Books.scala @@ -1,7 +1,6 @@ package reading.component.index import rx._ -import Ctx.Owner.Unsafe._ import scalatags.JsDom.all._ import scalacss.Defaults._ @@ -10,16 +9,16 @@ import scalacss.ScalatagsCss._ import reading.component.index.style.{ Books => BooksStyle } import reading.component.widget.Modal import reading.models.{ Book } -import reading.utils.{ RxTag, RxAttr } +import reading.utils.RxUtils._ object Books { - def apply(books: Rx[Seq[Book]]): Frag = { + def apply(books: Rx[Seq[Book]])(implicit ctx: Ctx.Owner): Frag = { val focus: Var[Option[Book]] = Var(None) div( BooksStyle.render, - RxTag { implicit context => + Rx { div( div( BooksStyle.books, @@ -31,13 +30,13 @@ object Books { BooksStyle.cover, src := s"cover/${book.title}.jpg", alt := s"${book.title}, ${book.author}", - RxAttr(onclick, Rx(() => focus() = Some(book))) + onclick := (() => focus() = Some(book)) ) ) } ), - RxTag { implicit context => + Rx { focus() match { case Some(book) => Modal(onClose = focus() = None)(BookDetail(book)) case None => span("") diff --git a/src/main/scala/reading/component/index/FilterUtils.scala b/src/main/scala/reading/component/index/FilterUtils.scala new file mode 100644 index 0000000..d4b24e4 --- /dev/null +++ b/src/main/scala/reading/component/index/FilterUtils.scala @@ -0,0 +1,39 @@ +package reading.component.index + +import rx._ + +import reading.models._ +import reading.Route + +object FilterUtils { + def remove( + filters: Var[Seq[Filter]], + search: Var[String], + filter: Filter + ): Unit = { + val newFilters = Filter.remove(filters.now, filter) + filters() = newFilters + search() = "" + Route.push(Route.Books(newFilters)) + } + + def removeAll( + filters: Var[Seq[Filter]], + search: Var[String] + ): Unit = { + filters() = Nil + search() = "" + Route.push(Route.Books(Nil)) + } + + def add( + filters: Var[Seq[Filter]], + search: Var[String], + filter: Filter + ): Unit = { + val newFilters = filter +: filters.now + filters() = newFilters + search() = "" + Route.push(Route.Books(newFilters)) + } +} diff --git a/src/main/scala/reading/component/index/Header.scala b/src/main/scala/reading/component/index/Header.scala index cf078ad..50d520e 100644 --- a/src/main/scala/reading/component/index/Header.scala +++ b/src/main/scala/reading/component/index/Header.scala @@ -1,32 +1,39 @@ package reading.component.index import rx._ -import Ctx.Owner.Unsafe._ import scalatags.JsDom.all._ import scalacss.Defaults._ import scalacss.ScalatagsCss._ import reading.component.index.style.{ Header => HeaderStyle } -import reading.component.widget.Cross +import reading.component.widget.{ Cross, Input } import reading.component.style.{ Color => C } -import reading.models.Filter -import reading.Route -import reading.utils.{ RxTag, RxAttr } +import reading.models.{ Book, Filter } +import reading.utils.RxUtils._ object Header { - def apply(filters: Var[Seq[Filter]], showFiltersMenu: Var[Boolean], booksCount: Rx[Int]): Frag = { + def apply( + books: Rx[Seq[Book]], + filters: Var[Seq[Filter]], + search: Var[String], + showFiltersMenu: Var[Boolean] + )( + implicit + ctx: Ctx.Owner + ): Frag = { val filtersCount: Rx[Int] = Rx(filters().length) + val booksCount: Rx[Int] = books.map(_.length) div( HeaderStyle.render, HeaderStyle.header, - RxTag { implicit context => + Rx { div( div( HeaderStyle.showFiltersMenu, - RxAttr(onclick, Rx(() => showFiltersMenu() = true)), + onclick := (() => showFiltersMenu() = true), "Filtrer", if (filtersCount() > 0) span(HeaderStyle.filtersCount, filtersCount()) else span("") ), @@ -39,14 +46,14 @@ object Header { div( HeaderStyle.clear, - RxAttr(onclick, Rx(() => updateFilters(filters, Nil))), + onclick := (() => FilterUtils.removeAll(filters, search)), "Effacer les filtres" ), filters().sortBy(_.name).map { filter => div( HeaderStyle.filter, - RxAttr(onclick, Rx(() => updateFilters(filters, Filter.remove(filters(), filter)))), + onclick := (() => FilterUtils.remove(filters, search, filter)), span(HeaderStyle.name, filter.name.capitalize), Cross(15.px, C.black.value) ) @@ -55,17 +62,16 @@ object Header { ) }, - RxTag { implicit context => - div( - HeaderStyle.booksCount, - span(s"${booksCount()} livre${if (booksCount() > 1) "s" else ""}") - ) - } + div( + HeaderStyle.searchAndCount, + Input(HeaderStyle.search, search, "Rechercher"), + Rx { + div( + HeaderStyle.booksCount, + span(s"${booksCount()} livre${if (booksCount() > 1) "s" else ""}") + ) + } + ) ) } - - private def updateFilters(filters: Var[Seq[Filter]], newFilters: Seq[Filter]): Unit = { - filters() = newFilters - Route.push(Route.Books(newFilters)) - } } diff --git a/src/main/scala/reading/component/index/Menu.scala b/src/main/scala/reading/component/index/Menu.scala index a0aabd1..4c118bd 100644 --- a/src/main/scala/reading/component/index/Menu.scala +++ b/src/main/scala/reading/component/index/Menu.scala @@ -1,7 +1,6 @@ package reading.component.index import rx._ -import Ctx.Owner.Unsafe._ import scalatags.JsDom.all._ import scalacss.Defaults._ @@ -9,45 +8,60 @@ import scalacss.ScalatagsCss._ import reading.component.index.style.{ Menu => MenuStyle } import reading.models._ -import reading.utils.{ RxTag, RxAttr } -import reading.Route +import reading.utils.RxUtils._ object Menu { - def apply(books: Rx[Seq[Book]], filters: Var[Seq[Filter]], showFiltersMenu: Var[Boolean]): Frag = - RxTag { implicit context => + def apply( + books: Rx[Seq[Book]], + filters: Var[Seq[Filter]], + search: Var[String], + showFiltersMenu: Var[Boolean] + )( + implicit + ctx: Ctx.Owner + ): Frag = + div( + MenuStyle.render, + Rx(if (showFiltersMenu()) MenuStyle.show else MenuStyle.empty), + MenuStyle.menu, + + div(MenuStyle.background), + div( - MenuStyle.render, - if (showFiltersMenu()) MenuStyle.show else "", - MenuStyle.menu, + MenuStyle.content, - header(showFiltersMenu, filters().length), + Rx(header(showFiltersMenu, filters().length)), div( MenuStyle.groups, - filters().find(_.kind == FilterKind.Grade) match { - case Some(grade) => { - val programs = Program.values.filter(p => Program.grade(p).toString() == grade.nonFormattedName) - group(books, filters, grade.name, programs.map(Filter.apply(_)), Some(grade)) + Rx { + filters().find(_.kind == FilterKind.Grade) match { + case Some(grade) => + val programs = Program.values.filter(p => Program.grade(p).toString() == grade.nonFormattedName) + group(books, filters, search, grade.name, programs.map(Filter.apply(_)), Some(grade)) + case None => + group(books, filters, search, "Classe", Grade.values.map(Filter.apply(_))) } - case None => group(books, filters, "Classe", Grade.values.map(Filter.apply(_))) }, - filters().find(_.kind == FilterKind.GroupedTheme) match { - case Some(groupedTheme) => { - val themes = Theme.values.filter(t => Theme.groupedTheme(t).toString() == groupedTheme.nonFormattedName) - group(books, filters, groupedTheme.name, themes.map(Filter.apply(_)), Some(groupedTheme)) + Rx { + filters().find(_.kind == FilterKind.GroupedTheme) match { + case Some(groupedTheme) => + val themes = Theme.values.filter(t => Theme.groupedTheme(t).toString() == groupedTheme.nonFormattedName) + group(books, filters, search, groupedTheme.name, themes.map(Filter.apply(_)), Some(groupedTheme)) + case None => + group(books, filters, search, "Theme", GroupedTheme.values.map(Filter.apply(_))) } - case None => group(books, filters, "Theme", GroupedTheme.values.map(Filter.apply(_))) }, - group(books, filters, "Genre", Genre.values.sorted.map(Filter.apply(_))), - group(books, filters, "Niveau", Level.values.map(Filter.apply(_))), - group(books, filters, "Période", Period.values.map(Filter.apply(_))) + group(books, filters, search, "Genre", Genre.values.sorted.map(Filter.apply(_))), + group(books, filters, search, "Niveau", Level.values.map(Filter.apply(_))), + group(books, filters, search, "Période", Period.values.map(Filter.apply(_))) ), - footer(Rx(books().length), filters, showFiltersMenu) + footer(books, filters, search, showFiltersMenu) ) - } + ) - def header(showFiltersMenu: Var[Boolean], count: Int): HtmlTag = + def header(showFiltersMenu: Var[Boolean], count: Int): Frag = div( MenuStyle.header, "Filtrer", @@ -57,78 +71,84 @@ object Menu { def group( books: Rx[Seq[Book]], filters: Var[Seq[Filter]], + search: Var[String], name: String, groupFilters: Seq[Filter], parentFilter: Option[Filter] = None )( implicit - context: Ctx.Data + ctx: Ctx.Owner ): Frag = { val filtersWithCount = Rx { groupFilters - .map(filter => (filter, Book.filter(books(), Seq(filter)).length)) + .map(filter => (filter, Filter.add(books(), filter).length)) .filter(_._2 > 0) } - if (filtersWithCount().isEmpty) - span("") - else - div( - MenuStyle.filterGroup, + Rx { + if (filtersWithCount().isEmpty) + span("") + else div( - MenuStyle.filterTitle, - parentFilter.map { filter => - RxAttr(onclick, Rx(() => updateFilters(filters, Filter.remove(filters(), filter)))) - }.getOrElse(""), - if (parentFilter.isDefined) MenuStyle.activeFilter else "", - name, - RxTag { implicit context => - val count = filters().filter(f => groupFilters.exists(Filter.equals(f, _))).length - if (count > 0) span(MenuStyle.filterTitleCount, count) else span("") - } - ), - div( - filtersWithCount().map { - case (filter, count) => { - val isActive = Filter.contains(filters(), filter) + div( + MenuStyle.filterTitle, + parentFilter.map { filter => + onclick := (() => FilterUtils.remove(filters, search, filter)) + }.getOrElse(""), + if (parentFilter.isDefined) MenuStyle.activeFilter else "", + name, + Rx { + val count = filters().filter(f => groupFilters.exists(f == _)).length + if (count > 0) span(MenuStyle.filterTitleCount, count) else span("") + } + ), + div( + filtersWithCount().map { + case (filter, count) => { + val isActive = Filter.contains(filters(), filter) - button( - MenuStyle.filter, - if (isActive) MenuStyle.activeFilter else "", - RxAttr(onclick, Rx(() => updateFilters( - filters, - if (isActive) Filter.remove(filters(), filter) else filter +: filters() - ))), - span( - span(filter.name.capitalize), - span(MenuStyle.filterCount, count) + button( + MenuStyle.filter, + if (isActive) MenuStyle.activeFilter else "", + onclick := (() => + if (isActive) + FilterUtils.remove(filters, search, filter) + else + FilterUtils.add(filters, search, filter)), + span( + span(filter.name.capitalize), + span(MenuStyle.filterCount, count) + ) ) - ) + } } - } + ) ) - ) - } - - private def updateFilters(filters: Var[Seq[Filter]], newFilters: Seq[Filter]): Unit = { - filters() = newFilters - Route.push(Route.Books(newFilters)) + } } - def footer(bookCount: Rx[Int], filters: Var[Seq[Filter]], showFiltersMenu: Var[Boolean]): HtmlTag = + def footer( + books: Rx[Seq[Book]], + filters: Var[Seq[Filter]], + search: Var[String], + showFiltersMenu: Var[Boolean] + )( + implicit + ctx: Ctx.Owner + ): Frag = div( MenuStyle.footer, div( MenuStyle.clear, - RxAttr(onclick, Rx(() => filters() = Nil)), + onclick := (() => FilterUtils.removeAll(filters, search)), "Effacer" ), div( MenuStyle.returnToBooks, - RxAttr(onclick, Rx(() => showFiltersMenu() = false)), + onclick := (() => showFiltersMenu() = false), "Afficher", - RxTag { implicit context => - span(MenuStyle.bookCount, bookCount()) + Rx { + span(MenuStyle.bookCount, books().length) } ) ) diff --git a/src/main/scala/reading/component/index/style/BookDetail.scala b/src/main/scala/reading/component/index/style/BookDetail.scala index e8f970e..f432fda 100644 --- a/src/main/scala/reading/component/index/style/BookDetail.scala +++ b/src/main/scala/reading/component/index/style/BookDetail.scala @@ -18,6 +18,10 @@ object BookDetail extends StyleSheet.Inline { marginBottom(30.px) ) + val items = style( + marginBottom(25.px) + ) + val item = style( lineHeight(25.px), margin(0.px, 15.px, 15.px), diff --git a/src/main/scala/reading/component/index/style/Header.scala b/src/main/scala/reading/component/index/style/Header.scala index 2260c91..2eb6eb2 100644 --- a/src/main/scala/reading/component/index/style/Header.scala +++ b/src/main/scala/reading/component/index/style/Header.scala @@ -66,6 +66,18 @@ object Header extends StyleSheet.Inline { height(15.px) ) + val searchAndCount = style( + display.flex, + flexWrap.wrap, + alignItems.center, + Media.mobile(justifyContent.center) + ) + + val search = style( + Media.mobile(display.none), + Media.desktop(marginRight(30.px)) + ) + val booksCount = style( fontSize(20.px), color(C.gray.value), diff --git a/src/main/scala/reading/component/index/style/Menu.scala b/src/main/scala/reading/component/index/style/Menu.scala index 12b0646..dd74039 100644 --- a/src/main/scala/reading/component/index/style/Menu.scala +++ b/src/main/scala/reading/component/index/style/Menu.scala @@ -10,9 +10,27 @@ object Menu extends StyleSheet.Inline { val menu = style( Media.mobile(display.none), - color(C.white.value), + Media.desktop( + color(C.white.value), + position.relative, + width(280.px) + ) + ) + + val background = style( + Media.desktop( + position.fixed, + width(280.px), + height(100.%%), + backgroundColor(C.englishWalnut.value), + boxShadow := "4px 0px 6px -1px rgba(0, 0, 0, 0.2)" + ) + ) + + val content = style( position.relative, - width(280.px) + width(100.%%), + height(100.%%) ) val header = style( @@ -50,6 +68,8 @@ object Menu extends StyleSheet.Inline { ) ) + val empty = style() + val groups = style( Media.mobile( height :=! "calc(100% - 120px)", @@ -57,8 +77,6 @@ object Menu extends StyleSheet.Inline { ) ) - val filterGroup = style() - val filterTitle = style( Commons.filter(), minHeight(50.px), diff --git a/src/main/scala/reading/component/style/Index.scala b/src/main/scala/reading/component/style/Index.scala index 99e4746..e02ebd9 100644 --- a/src/main/scala/reading/component/style/Index.scala +++ b/src/main/scala/reading/component/style/Index.scala @@ -3,7 +3,6 @@ package reading.component.style import scalacss.Defaults._ import reading.Media -import reading.component.style.{ Color => C } object Index extends StyleSheet.Inline { import dsl._ @@ -11,22 +10,11 @@ object Index extends StyleSheet.Inline { val page = style( display.flex, overflowY.scroll, - height(100.%%), - - Media.desktop( - &.before( - content := "\"\"", - display.block, - position.fixed, - width(280.px), - height(100.%%), - backgroundColor(C.englishWalnut.value), - boxShadow := "4px 0px 6px -1px rgba(0, 0, 0, 0.2)" - ) - ) + height(100.%%) ) val main = style( - width(100.%%) + Media.desktop(width :=! "calc(100% - 280px)"), + Media.mobile(width(100.%%)) ) } diff --git a/src/main/scala/reading/component/widget/Cross.scala b/src/main/scala/reading/component/widget/Cross.scala index c9e3054..40087a1 100644 --- a/src/main/scala/reading/component/widget/Cross.scala +++ b/src/main/scala/reading/component/widget/Cross.scala @@ -8,7 +8,7 @@ import scalacss.internal.ValueT, ValueT.Color import reading.component.widget.style.{ Cross => CrossStyle } object Cross { - def apply(size: String, color: ValueT[Color]): HtmlTag = + def apply(size: String, color: ValueT[Color]): Frag = div( CrossStyle.render, CrossStyle.cross, diff --git a/src/main/scala/reading/component/widget/Input.scala b/src/main/scala/reading/component/widget/Input.scala new file mode 100644 index 0000000..7dac47a --- /dev/null +++ b/src/main/scala/reading/component/widget/Input.scala @@ -0,0 +1,44 @@ +package reading.component.widget + +import scalatags.JsDom.all._ + +import org.scalajs.dom.KeyboardEvent +import org.scalajs.dom.html.Input + +import scalacss.Defaults._ +import scalacss.ScalatagsCss._ + +import rx._ + +import reading.component.widget.style.{ Input => InputStyle } + +object Input { + def apply( + style: StyleA, + query: Var[String], + label: String = "", + onEnter: => Unit = () + )( + implicit + ctx: Ctx.Owner + ): Frag = { + val inputBox = input( + InputStyle.render, + InputStyle.input, + style, + placeholder := label, + onkeyup := { (e: KeyboardEvent) => + val input = e.target.asInstanceOf[Input] + query() = input.value + input.value = input.value + if (e.keyCode == 13) onEnter + } + ).render + + query.trigger { + inputBox.value = query.now + } + + inputBox + } +} diff --git a/src/main/scala/reading/component/widget/Modal.scala b/src/main/scala/reading/component/widget/Modal.scala index 81d0c78..db1f7e6 100644 --- a/src/main/scala/reading/component/widget/Modal.scala +++ b/src/main/scala/reading/component/widget/Modal.scala @@ -3,29 +3,27 @@ package reading.component.widget import scala.util.Random import org.scalajs.dom.raw.HTMLElement -import rx._ -import rx.Ctx.Owner.Unsafe._ import scalacss.Defaults._ import scalacss.ScalatagsCss._ import scalatags.JsDom.all._ import reading.component.widget.style.{ Modal => ModalStyle } -import reading.utils.{ RxAttr } object Modal { - def apply(onClose: => Unit)(content: HtmlTag): HtmlTag = { + def apply(onClose: => Unit)(content: Frag): Frag = { val modalId = s"modal${Random.nextInt}" Animate( id = modalId, duration = 250, transition = Transition.easeOut, - animate = (progress, element) => { - element.style.opacity = s"$progress" - element.childNodes(2) match { - case e: HTMLElement => e.style.transform = s"scale(${0.85 + 0.15 * progress})" + animate = + (progress, element) => { + element.style.opacity = s"$progress" + element.childNodes(2) match { + case e: HTMLElement => e.style.transform = s"scale(${0.85 + 0.15 * progress})" + } } - } ) div( @@ -35,7 +33,7 @@ object Modal { div( ModalStyle.curtain, - RxAttr(onclick, Rx(() => close(modalId, onClose))) + onclick := (() => close(modalId, onClose)) ), div( @@ -43,7 +41,7 @@ object Modal { content, button( ModalStyle.close, - RxAttr(onclick, Rx(() => close(modalId, onClose))), + onclick := (() => close(modalId, onClose)), "Fermer" ) ) diff --git a/src/main/scala/reading/component/widget/style/Input.scala b/src/main/scala/reading/component/widget/style/Input.scala new file mode 100644 index 0000000..967393b --- /dev/null +++ b/src/main/scala/reading/component/widget/style/Input.scala @@ -0,0 +1,16 @@ +package reading.component.widget.style + +import scalacss.Defaults._ + +import reading.component.style.{ Color => C } + +object Input extends StyleSheet.Inline { + import dsl._ + + val input = style( + border(1.px, solid, C.mickado.value), + borderRadius(2.px), + padding(10.px), + &.hover(borderColor(C.gray.value)) + ) +} diff --git a/src/main/scala/reading/component/widget/style/Modal.scala b/src/main/scala/reading/component/widget/style/Modal.scala index 1872344..faf325d 100644 --- a/src/main/scala/reading/component/widget/style/Modal.scala +++ b/src/main/scala/reading/component/widget/style/Modal.scala @@ -55,8 +55,11 @@ object Modal extends StyleSheet.Inline { ) val close = style( - Button.simple, - marginTop(20.px), - marginBottom(30.px) + Media.desktop(display.none), + Media.mobile( + Button.simple, + marginTop(20.px), + marginBottom(30.px) + ) ) } diff --git a/src/main/scala/reading/models/Book.scala b/src/main/scala/reading/models/Book.scala index 7d72f23..6f4d8dd 100644 --- a/src/main/scala/reading/models/Book.scala +++ b/src/main/scala/reading/models/Book.scala @@ -16,6 +16,9 @@ case class Book( } object Book { - def filter(books: Seq[Book], filters: Seq[Filter]): Seq[Book] = - books.filter(b => filters.forall(_.filter(b))) + def filter(books: Seq[Book], search: String = ""): Seq[Book] = + books.filter { book => + (Search(book.title, search) + || Search(book.author, search)) + } } diff --git a/src/main/scala/reading/Books.scala b/src/main/scala/reading/models/Books.scala index eb4722a..43ed2b8 100644 --- a/src/main/scala/reading/Books.scala +++ b/src/main/scala/reading/models/Books.scala @@ -1,6 +1,5 @@ -package reading +package reading.models -import reading.models.{ Book, Period, Theme, Genre, Program, Level } import Period._ import Theme._ import Genre._ @@ -1090,7 +1089,7 @@ object Books { ), Book( - title = "Caïus", + title = "L’affaire Caïus", author = "Henry WINTERFELD", year = "2014", parts = 2, diff --git a/src/main/scala/reading/models/Filter.scala b/src/main/scala/reading/models/Filter.scala index d14ca63..7ec6340 100644 --- a/src/main/scala/reading/models/Filter.scala +++ b/src/main/scala/reading/models/Filter.scala @@ -5,6 +5,16 @@ trait Filter { def kind: FilterKind def nonFormattedName: String def name: String + + override def equals(that: Any): Boolean = + that match { + case that: Filter => + this.kind == that.kind && this.name == that.name + case _ => + false + } + + override def hashCode: Int = this.kind.hashCode + this.nonFormattedName.hashCode } object Filter { @@ -23,15 +33,33 @@ object Filter { } def contains(filters: Seq[Filter], filter: Filter): Boolean = - filters.find(equals(_, filter)).nonEmpty - - def equals(f1: Filter, f2: Filter): Boolean = - f1.kind == f2.kind && f1.name == f2.name + filters.find(_ == filter).nonEmpty def remove(fs: Seq[Filter], rf: Filter): Seq[Filter] = fs.filterNot { f => - (equals(f, rf) + (f == rf || rf.kind == FilterKind.Grade && f.kind == FilterKind.Program || rf.kind == FilterKind.GroupedTheme && f.kind == FilterKind.Theme) } + + val onBooks: Map[Filter, Seq[Book]] = + Seq( + Grade.values.map(Filter.apply(_)), + Program.values.map(Filter.apply(_)), + Theme.values.map(Filter.apply(_)), + GroupedTheme.values.map(Filter.apply(_)), + Genre.values.map(Filter.apply(_)), + Level.values.map(Filter.apply(_)), + Period.values.map(Filter.apply(_)) + ) + .flatten + .map(f => (f, Books().filter(f.filter))) + .toMap + + def add(books: Seq[Book], filters: Seq[Filter]): Seq[Book] = + filters.foldLeft(books)(add) + + def add(books: Seq[Book], filter: Filter): Seq[Book] = + books.intersect(onBooks.getOrElse(filter, Nil)) + } diff --git a/src/main/scala/reading/models/Search.scala b/src/main/scala/reading/models/Search.scala new file mode 100644 index 0000000..5ef97c1 --- /dev/null +++ b/src/main/scala/reading/models/Search.scala @@ -0,0 +1,16 @@ +package reading.models + +object Search { + def apply(text: String, search: String): Boolean = + format(text).contains(format(search)) + + private def format(str: String): String = + str + .toLowerCase + .replace('’', '\'') + .replaceAll("[èéêë]", "e") + .replaceAll("[ûù]", "u") + .replaceAll("[ïî]", "i") + .replaceAll("[àâ]", "a") + .replaceAll("ô", "o") +} diff --git a/src/main/scala/reading/utils/Rx.scala b/src/main/scala/reading/utils/Rx.scala index 76d05eb..a5b56ee 100644 --- a/src/main/scala/reading/utils/Rx.scala +++ b/src/main/scala/reading/utils/Rx.scala @@ -1,44 +1,85 @@ package reading.utils +import java.util.concurrent.atomic.AtomicReference + +import scala.annotation.tailrec +import scala.language.implicitConversions import scala.util.{ Failure, Success } import org.scalajs.dom.Element - -import scalatags.JsDom.all._ import rx._ +import scalacss.Defaults.StyleA +import scalatags.JsDom.all._ -import Ctx.Owner.Unsafe._ +object RxUtils { -object RxTag { - def apply(r: Ctx.Data => HtmlTag): HtmlTag = - rxMod(Rx(r(implicitly[Ctx.Data]))) + implicit def rxFrag[T](n: Rx[T])(implicit f: T => Frag, ctx: Ctx.Owner): Frag = { - private def rxMod(r: Rx[HtmlTag]): HtmlTag = { - def rSafe = r.toTry match { - case Success(v) => v.render - case Failure(e) => span(e.toString, backgroundColor := "red").render + @tailrec def clearChildren(node: org.scalajs.dom.Node): Unit = { + if (node.firstChild != null) { + node.removeChild(node.firstChild) + clearChildren(node) + } } - var last = rSafe - r.trigger { - val newLast = rSafe - Option(last.parentElement).foreach { - _.replaceChild(newLast, last) + + def fSafe: Frag = n match { + case r: Rx.Dynamic[T] => r.toTry match { + case Success(v) => v.render + case Failure(e) => span(e.getMessage, backgroundColor := "red").render } + case v: Var[T] => v.now.render + } + + var last = fSafe.render + + val container = span(last).render + + n.triggerLater { + val newLast = fSafe.render + //Rx[Seq[T]] can generate multiple children per propagate, so use clearChildren instead of replaceChild + clearChildren(container) + container.appendChild(newLast) last = newLast } - span( - bindNode(last) - ) + bindNode(container) } -} -object RxAttr { - def apply[Builder, T: AttrValue](attr: scalatags.generic.Attr, v: Rx[T]) = { - val attrValue = new AttrValue[Rx[T]] { - def apply(t: Element, a: Attr, r: Rx[T]): Unit = { - val _ = r.trigger { implicitly[AttrValue[T]].apply(t, a, r.now) } + implicit def RxAttrValue[T: AttrValue](implicit ctx: Ctx.Owner) = new AttrValue[Rx.Dynamic[T]] { + def apply(t: Element, a: Attr, r: Rx.Dynamic[T]): Unit = { + r.trigger { implicitly[AttrValue[T]].apply(t, a, r.now) } + () + } + } + + implicit def RxStyleValue[T: StyleValue](implicit ctx: Ctx.Owner) = new StyleValue[Rx.Dynamic[T]] { + def apply(t: Element, s: Style, r: Rx.Dynamic[T]): Unit = { + r.trigger { implicitly[StyleValue[T]].apply(t, s, r.now) } + () + } + } + + implicit class bindRxStyle(rx: Rx[StyleA])(implicit ctx: Ctx.Owner) extends Modifier { + def applyTo(container: Element) = { + val atomicReference = new AtomicReference(rx.now) + applyStyle(container, atomicReference.get()) + rx.triggerLater { + val current = rx.now + val previous = atomicReference.getAndSet(current) + removeStyle(container, previous) + applyStyle(container, current) + () } + () } - scalatags.generic.AttrPair(attr, v, attrValue) + + private def removeStyle(container: Element, style: StyleA): Unit = + style.classNameIterator.foreach { className => + container.classList.remove(className.value) + } + + private def applyStyle(container: Element, style: StyleA): Unit = + style.classNameIterator.foreach { className => + container.classList.add(className.value) + } } } |