From 36fc53409c9119ac94788d8296d6b171eb0a6500 Mon Sep 17 00:00:00 2001 From: Joris Date: Mon, 16 Jan 2017 22:23:38 +0100 Subject: Add navigation history with urls --- src/main/scala/reading/Main.scala | 9 ++- src/main/scala/reading/Route.scala | 64 +++++++++++++++++ src/main/scala/reading/component/Index.scala | 4 +- src/main/scala/reading/component/index/Books.scala | 6 +- .../scala/reading/component/index/Filters.scala | 7 +- .../reading/component/index/FiltersMenu.scala | 11 ++- .../reading/component/index/style/Books.scala | 2 - src/main/scala/reading/models/Filter.scala | 83 ++++------------------ src/main/scala/reading/models/FilterFactory.scala | 67 +++++++++++++++++ src/main/scala/reading/models/FilterKind.scala | 16 +++++ src/main/scala/reading/models/Genre.scala | 2 +- src/main/scala/reading/models/Grade.scala | 2 +- src/main/scala/reading/models/Level.scala | 2 +- src/main/scala/reading/models/Period.scala | 2 +- src/main/scala/reading/models/Program.scala | 2 +- src/main/scala/reading/models/Theme.scala | 2 +- 16 files changed, 194 insertions(+), 87 deletions(-) create mode 100644 src/main/scala/reading/Route.scala create mode 100644 src/main/scala/reading/models/FilterFactory.scala create mode 100644 src/main/scala/reading/models/FilterKind.scala (limited to 'src') diff --git a/src/main/scala/reading/Main.scala b/src/main/scala/reading/Main.scala index 09bd76c..41057fd 100644 --- a/src/main/scala/reading/Main.scala +++ b/src/main/scala/reading/Main.scala @@ -7,6 +7,7 @@ import org.scalajs.dom import scalacss.Defaults._ import reading.component.style.{Global => GlobalStyle} +import reading.utils.RxTag object Main extends JSApp { def main(): Unit = { @@ -14,6 +15,12 @@ object Main extends JSApp { style.appendChild(dom.document.createTextNode(GlobalStyle.render)) dom.document.head.appendChild(style) - val _ = dom.document.body.appendChild(component.Index().render) + val _ = dom.document.body.appendChild( + RxTag { implicit context => + Route.current() match { + case Route.Books(filters) => component.Index(filters) + } + }.render + ) } } diff --git a/src/main/scala/reading/Route.scala b/src/main/scala/reading/Route.scala new file mode 100644 index 0000000..85d1d6b --- /dev/null +++ b/src/main/scala/reading/Route.scala @@ -0,0 +1,64 @@ +package reading + +import org.scalajs.dom +import scala.scalajs.js.URIUtils + +import rx.Var + +import reading.models.{Filter, FilterKind} + +sealed trait Route + +object Route { + case class Books(filters: Seq[Filter]) extends Route + + val current: Var[Route] = Var(parse(dom.window.location.hash)) + + dom.window.onpopstate = (e: dom.raw.PopStateEvent) => { + current() = parse(dom.window.location.hash) + } + + def parse(hash: String): Route = + pathAndParams(hash) match { + case ("books" :: Nil, params) => { + val filters = params.flatMap { param => + param.split("=") match { + case Array(kind, nonFormattedName) => + for { + kind <- FilterKind.withNameOption(kind) + filter <- Filter(kind, nonFormattedName) + } yield filter + case _ => None + } + } + Books(filters) + } + case _ => + Books(Nil) + } + + def pathAndParams(hash: String): (List[String], List[String]) = { + def splitPath(path: String) = path.split("/").drop(1).toList + URIUtils.decodeURI(hash.drop(1)).split('?') match { + case Array(path) => (splitPath(path), Nil) + case Array(path, params) => (splitPath(path), params.split("&").toList) + } + } + + def url(route: Route): String = { + val hash = route match { + case Books(filters) => "/books" ++ (if(filters.nonEmpty) filters.map(filter => s"${filter.kind}=${filter.nonFormattedName}").mkString("?", "&", "") else "") + case _ => "/books" + } + dom.window.location.origin + dom.window.location.pathname + "#" + URIUtils.encodeURI(hash) + } + + def goTo(route: Route): Unit = { + push(route) + current() = route + } + + def push(route: Route): Unit = { + dom.window.history.pushState(null, "", url(route)); + } +} diff --git a/src/main/scala/reading/component/Index.scala b/src/main/scala/reading/component/Index.scala index ef8ae51..41139cc 100644 --- a/src/main/scala/reading/component/Index.scala +++ b/src/main/scala/reading/component/Index.scala @@ -14,8 +14,8 @@ import reading.models.{Book, Filter} import reading.utils.RxAttr object Index { - def apply(): Frag = { - val filters: Var[Seq[Filter]] = Var(Nil) + def apply(initialFilters: Seq[Filter]): HtmlTag = { + val filters: Var[Seq[Filter]] = Var(initialFilters) val books: Rx[Seq[Book]] = Rx { if(filters().isEmpty) Books() else Book.filter(Books(), filters()) } diff --git a/src/main/scala/reading/component/index/Books.scala b/src/main/scala/reading/component/index/Books.scala index 435bf50..3faa130 100644 --- a/src/main/scala/reading/component/index/Books.scala +++ b/src/main/scala/reading/component/index/Books.scala @@ -32,10 +32,12 @@ object Books { ), div( div(BooksStyle.item, s"classe : ${book.programs.map(Program.grade).distinct.sorted.mkString(", ")}"), - div(BooksStyle.item, s"programme : ${book.programs.sorted.mkString(", ")}"), + div(BooksStyle.item, s"programme : ${book.programs.map(p => "« " ++ p.toString ++ " »").sorted.mkString(", ")}"), div(BooksStyle.item, s"thème : ${book.themes.sorted.mkString(", ")}"), div(BooksStyle.item, s"genre : ${book.genres.sorted.mkString(", ")}"), - div(BooksStyle.item, s"période : ${book.period}") + book.period.map { period => + div(BooksStyle.item, s"période : $period") + } ) ) ) diff --git a/src/main/scala/reading/component/index/Filters.scala b/src/main/scala/reading/component/index/Filters.scala index a53e46d..a5ea3f6 100644 --- a/src/main/scala/reading/component/index/Filters.scala +++ b/src/main/scala/reading/component/index/Filters.scala @@ -11,6 +11,7 @@ import reading.component.index.style.{Filters => FiltersStyle} import reading.component.widget.Cross import reading.component.style.Col import reading.models.Filter +import reading.Route import reading.utils.{RxTag, RxAttr} object Filters { @@ -26,7 +27,11 @@ object Filters { filters().sortBy(_.name).map { filter => div( FiltersStyle.filter, - RxAttr(onclick, Rx(() => filters() = Filter.remove(filters(), filter))), + RxAttr(onclick, Rx(() => { + val newFilters = Filter.remove(filters(), filter) + filters() = newFilters + Route.push(Route.Books(newFilters)) + })), span(FiltersStyle.name, filter.name), Cross(15.px, Col.white) ) diff --git a/src/main/scala/reading/component/index/FiltersMenu.scala b/src/main/scala/reading/component/index/FiltersMenu.scala index febc52c..1015df1 100644 --- a/src/main/scala/reading/component/index/FiltersMenu.scala +++ b/src/main/scala/reading/component/index/FiltersMenu.scala @@ -10,6 +10,7 @@ import scalacss.ScalatagsCss._ import reading.component.index.style.{FiltersMenu => FiltersMenuStyle} import reading.models._ import reading.utils.{RxTag, RxAttr} +import reading.Route object FiltersMenu { def apply(books: Rx[Seq[Book]], filters: Var[Seq[Filter]]): Frag = @@ -17,7 +18,7 @@ object FiltersMenu { div( FiltersMenuStyle.render, FiltersMenuStyle.groups, - filters().find(_.kind == GradeKind) match { + filters().find(_.kind == FilterKind.Grade) match { case None => group(books, filters, "Classe", Grade.values.map(Filter.apply(_))) case Some(grade) => @@ -41,7 +42,7 @@ object FiltersMenu { val filtersWithCount = Rx { groupFilters .filter(filter => !Filter.contains(filters(), filter)) - .map(filter => (filter, Book.filter(books(), filter +: filters()).length)) + .map(filter => (filter, Book.filter(books(), Seq(filter)).length)) .filter(_._2 > 0) } @@ -55,7 +56,11 @@ object FiltersMenu { filtersWithCount().map { case (filter, count) => button( FiltersMenuStyle.filter, - RxAttr(onclick, Rx(() => filters() = filter +: filters())), + RxAttr(onclick, Rx(() => { + val newFilters = filter +: filters() + filters() = newFilters + Route.push(Route.Books(newFilters)) + })), span(s"${filter.name.capitalize} ($count)") ) } diff --git a/src/main/scala/reading/component/index/style/Books.scala b/src/main/scala/reading/component/index/style/Books.scala index fc3a18f..ad7375d 100644 --- a/src/main/scala/reading/component/index/style/Books.scala +++ b/src/main/scala/reading/component/index/style/Books.scala @@ -8,8 +8,6 @@ object Books extends StyleSheet.Inline { import dsl._ val books = style( - display.flex, - flexWrap.wrap ) val book = style( diff --git a/src/main/scala/reading/models/Filter.scala b/src/main/scala/reading/models/Filter.scala index 5aabcc6..c3d81c6 100644 --- a/src/main/scala/reading/models/Filter.scala +++ b/src/main/scala/reading/models/Filter.scala @@ -3,89 +3,32 @@ package reading.models trait Filter { def filter(book: Book): Boolean def kind: FilterKind + def nonFormattedName: String def name: String } -sealed trait FilterKind -case object PeriodKind extends FilterKind -case object ThemeKind extends FilterKind -case object GenreKind extends FilterKind -case object LevelKind extends FilterKind -case object ProgramKind extends FilterKind -case object GradeKind extends FilterKind - object Filter { def apply[T](in: T)(implicit filterFactory: FilterFactory[T]): Filter = filterFactory.create(in) + def apply(kind: FilterKind, nonFormattedName: String): Option[Filter] = + kind match { + case FilterKind.Period => Period.withNameOption(nonFormattedName).map(apply[Period]) + case FilterKind.Theme => Theme.withNameOption(nonFormattedName).map(apply[Theme]) + case FilterKind.Genre => Genre.withNameOption(nonFormattedName).map(apply[Genre]) + case FilterKind.Level => Level.withNameOption(nonFormattedName).map(apply[Level]) + case FilterKind.Program => Program.withNameOption(nonFormattedName).map(apply[Program]) + case FilterKind.Grade => Grade.withNameOption(nonFormattedName).map(apply[Grade]) + } + def contains(filters: Seq[Filter], filter: Filter): Boolean = - filters.find(f => f.kind == filter.kind && f.name == filter.name).nonEmpty + filters.find(equals(_, filter)).nonEmpty def equals(f1: Filter, f2: Filter): Boolean = f1.kind == f2.kind && f1.name == f2.name def remove(fs: Seq[Filter], rf: Filter): Seq[Filter] = fs.filterNot { f => - equals(f, rf) || rf.kind == GradeKind && f.kind == ProgramKind + equals(f, rf) || rf.kind == FilterKind.Grade && f.kind == FilterKind.Program } } - -trait FilterFactory[T] { - def create(in: T): Filter -} - -object FilterFactory { - implicit object PeriodFilter extends FilterFactory[Period] { - def create(period: Period): Filter = - new Filter { - def filter(book: Book): Boolean = book.period == Some(period) - val kind: FilterKind = PeriodKind - val name: String = period.toString() - } - } - - implicit object ThemeFilter extends FilterFactory[Theme] { - def create(theme: Theme): Filter = - new Filter { - def filter(book: Book): Boolean = book.themes.contains(theme) - val kind: FilterKind = ThemeKind - val name: String = theme.toString() - } - } - - implicit object GenreFilter extends FilterFactory[Genre] { - def create(genre: Genre): Filter = - new Filter { - def filter(book: Book): Boolean = book.genres.contains(genre) - val kind: FilterKind = GenreKind - val name: String = genre.toString() - } - } - - implicit object ProgramFilter extends FilterFactory[Program] { - def create(program: Program): Filter = - new Filter { - def filter(book: Book): Boolean = book.programs.contains(program) - val kind: FilterKind = ProgramKind - val name: String = program.toString() - } - } - - implicit object GradeFilter extends FilterFactory[Grade] { - def create(grade: Grade): Filter = - new Filter { - def filter(book: Book): Boolean = book.programs.map(Program.grade).contains(grade) - val kind: FilterKind = GradeKind - val name: String = grade.toString() - } - } - - implicit object LevelFilter extends FilterFactory[Level] { - def create(level: Level): Filter = - new Filter { - def filter(book: Book): Boolean = book.level == level - val kind: FilterKind = LevelKind - val name: String = level.toString() - } - } -} diff --git a/src/main/scala/reading/models/FilterFactory.scala b/src/main/scala/reading/models/FilterFactory.scala new file mode 100644 index 0000000..269af82 --- /dev/null +++ b/src/main/scala/reading/models/FilterFactory.scala @@ -0,0 +1,67 @@ +package reading.models + +trait FilterFactory[T] { + def create(in: T): Filter +} + +object FilterFactory { + implicit object PeriodFilter extends FilterFactory[Period] { + def create(period: Period): Filter = + new Filter { + def filter(book: Book): Boolean = book.period == Some(period) + val kind: FilterKind = FilterKind.Period + val nonFormattedName: String = period.toString() + val name: String = period.prettyPrint() + } + } + + implicit object ThemeFilter extends FilterFactory[Theme] { + def create(theme: Theme): Filter = + new Filter { + def filter(book: Book): Boolean = book.themes.contains(theme) + val kind: FilterKind = FilterKind.Theme + val nonFormattedName: String = theme.toString() + val name: String = theme.prettyPrint() + } + } + + implicit object GenreFilter extends FilterFactory[Genre] { + def create(genre: Genre): Filter = + new Filter { + def filter(book: Book): Boolean = book.genres.contains(genre) + val kind: FilterKind = FilterKind.Genre + val nonFormattedName: String = genre.toString() + val name: String = genre.prettyPrint() + } + } + + implicit object ProgramFilter extends FilterFactory[Program] { + def create(program: Program): Filter = + new Filter { + def filter(book: Book): Boolean = book.programs.contains(program) + val kind: FilterKind = FilterKind.Program + val nonFormattedName: String = program.toString() + val name: String = program.prettyPrint() + } + } + + implicit object GradeFilter extends FilterFactory[Grade] { + def create(grade: Grade): Filter = + new Filter { + def filter(book: Book): Boolean = book.programs.map(Program.grade).contains(grade) + val kind: FilterKind = FilterKind.Grade + val nonFormattedName: String = grade.toString() + val name: String = grade.prettyPrint() + } + } + + implicit object LevelFilter extends FilterFactory[Level] { + def create(level: Level): Filter = + new Filter { + def filter(book: Book): Boolean = book.level == level + val kind: FilterKind = FilterKind.Level + val nonFormattedName: String = level.toString() + val name: String = level.prettyPrint() + } + } +} diff --git a/src/main/scala/reading/models/FilterKind.scala b/src/main/scala/reading/models/FilterKind.scala new file mode 100644 index 0000000..ba63f15 --- /dev/null +++ b/src/main/scala/reading/models/FilterKind.scala @@ -0,0 +1,16 @@ +package reading.models + +import enumeratum._ + +sealed trait FilterKind extends EnumEntry + +object FilterKind extends Enum[FilterKind] { + val values = findValues + + case object Period extends FilterKind + case object Theme extends FilterKind + case object Genre extends FilterKind + case object Level extends FilterKind + case object Program extends FilterKind + case object Grade extends FilterKind +} diff --git a/src/main/scala/reading/models/Genre.scala b/src/main/scala/reading/models/Genre.scala index 3fa13bc..51d2394 100644 --- a/src/main/scala/reading/models/Genre.scala +++ b/src/main/scala/reading/models/Genre.scala @@ -9,7 +9,7 @@ sealed trait Genre extends EnumEntry with Ordered[Genre] { values.indexOf(that) - values.indexOf(this) } - override def toString(): String = this match { + def prettyPrint(): String = this match { case JournalIntime => "journal intime" case RomanHistorique => "roman historique" case Policier => "policier" diff --git a/src/main/scala/reading/models/Grade.scala b/src/main/scala/reading/models/Grade.scala index e41d1a8..c48234b 100644 --- a/src/main/scala/reading/models/Grade.scala +++ b/src/main/scala/reading/models/Grade.scala @@ -9,7 +9,7 @@ sealed trait Grade extends EnumEntry with Ordered[Grade] { values.indexOf(that) - values.indexOf(this) } - override def toString(): String = this match { + def prettyPrint(): String = this match { case Sixieme => "6ème" case Cinquieme => "5ème" case Quatrieme => "4ème" diff --git a/src/main/scala/reading/models/Level.scala b/src/main/scala/reading/models/Level.scala index ebec020..c06776e 100644 --- a/src/main/scala/reading/models/Level.scala +++ b/src/main/scala/reading/models/Level.scala @@ -9,7 +9,7 @@ sealed trait Level extends EnumEntry with Ordered[Level] { values.indexOf(that) - values.indexOf(this) } - override def toString(): String = this match { + def prettyPrint(): String = this match { case Facile => "facile" case Moyen => "moyen" case Difficile => "difficile" diff --git a/src/main/scala/reading/models/Period.scala b/src/main/scala/reading/models/Period.scala index 8500591..f16bde3 100644 --- a/src/main/scala/reading/models/Period.scala +++ b/src/main/scala/reading/models/Period.scala @@ -5,7 +5,7 @@ import enumeratum._ sealed trait Period extends EnumEntry { import Period._ - override def toString(): String = this match { + def prettyPrint(): String = this match { case Louis14 => "Louis XIV" case Siecle19 => "19ème siècle" case Siecle20 => "20ème siècle" diff --git a/src/main/scala/reading/models/Program.scala b/src/main/scala/reading/models/Program.scala index 00e2b3b..da07653 100644 --- a/src/main/scala/reading/models/Program.scala +++ b/src/main/scala/reading/models/Program.scala @@ -9,7 +9,7 @@ sealed trait Program extends EnumEntry with Ordered[Program] { values.indexOf(that) - values.indexOf(this) } - override def toString(): String = this match { + def prettyPrint(): String = this match { case Monstre => "Le monstre, aux limites de l'humain" case RecitAventure => "Récits d'aventures" case CreationPoetique => "Récit de création, création poétique" diff --git a/src/main/scala/reading/models/Theme.scala b/src/main/scala/reading/models/Theme.scala index be8e5a9..5905d49 100644 --- a/src/main/scala/reading/models/Theme.scala +++ b/src/main/scala/reading/models/Theme.scala @@ -9,7 +9,7 @@ sealed trait Theme extends EnumEntry with Ordered[Theme] { values.indexOf(that) - values.indexOf(this) } - override def toString(): String = this match { + def prettyPrint(): String = this match { case Amitie => "amitié" case Aventure => "aventure" case Americain => "américain" -- cgit v1.2.3