diff options
Diffstat (limited to 'src/main/scala')
-rw-r--r-- | src/main/scala/reading/Main.scala | 19 | ||||
-rw-r--r-- | src/main/scala/reading/component/Index.scala | 42 | ||||
-rw-r--r-- | src/main/scala/reading/component/index/Books.scala | 33 | ||||
-rw-r--r-- | src/main/scala/reading/component/index/Filters.scala | 33 | ||||
-rw-r--r-- | src/main/scala/reading/component/index/FiltersMenu.scala | 48 | ||||
-rw-r--r-- | src/main/scala/reading/component/index/style/Books.scala | 31 | ||||
-rw-r--r-- | src/main/scala/reading/component/index/style/Filters.scala | 21 | ||||
-rw-r--r-- | src/main/scala/reading/component/index/style/FiltersMenu.scala | 24 | ||||
-rw-r--r-- | src/main/scala/reading/component/style/Color.scala | 16 | ||||
-rw-r--r-- | src/main/scala/reading/component/style/Global.scala | 28 | ||||
-rw-r--r-- | src/main/scala/reading/component/style/Index.scala | 26 | ||||
-rw-r--r-- | src/main/scala/reading/models/Book.scala | 21 | ||||
-rw-r--r-- | src/main/scala/reading/models/Filter.scala | 46 | ||||
-rw-r--r-- | src/main/scala/reading/models/Genre.scala | 21 | ||||
-rw-r--r-- | src/main/scala/reading/models/Theme.scala | 21 | ||||
-rw-r--r-- | src/main/scala/reading/utils/Rx.scala | 44 |
16 files changed, 474 insertions, 0 deletions
diff --git a/src/main/scala/reading/Main.scala b/src/main/scala/reading/Main.scala new file mode 100644 index 0000000..09bd76c --- /dev/null +++ b/src/main/scala/reading/Main.scala @@ -0,0 +1,19 @@ +package reading + +import scala.scalajs.js.JSApp + +import org.scalajs.dom + +import scalacss.Defaults._ + +import reading.component.style.{Global => GlobalStyle} + +object Main extends JSApp { + def main(): Unit = { + val style = dom.document.createElement("style") + style.appendChild(dom.document.createTextNode(GlobalStyle.render)) + dom.document.head.appendChild(style) + + val _ = dom.document.body.appendChild(component.Index().render) + } +} diff --git a/src/main/scala/reading/component/Index.scala b/src/main/scala/reading/component/Index.scala new file mode 100644 index 0000000..28d9081 --- /dev/null +++ b/src/main/scala/reading/component/Index.scala @@ -0,0 +1,42 @@ +package reading.component + +import rx._ +import Ctx.Owner.Unsafe._ + +import scalatags.JsDom.all._ +import scalacss.Defaults._ +import scalacss.ScalatagsCss._ + +import reading.component.style.{Index => IndexStyle} +import reading.component.index.{FiltersMenu, Filters, Books} +import reading.models.{Book, Filter} +import reading.utils.RxAttr + +object Index { + def apply(): Frag = { + val filters: Var[Seq[Filter]] = Var(Nil) + val books: Rx[Seq[Book]] = Rx { + if(filters().isEmpty) Book.all else Book.filter(Book.all, filters()) + } + + div( + IndexStyle.render, + + button( + IndexStyle.header, + RxAttr(onclick, Rx(() => filters() = Nil)), + "Conseils de lecture" + ), + + div( + IndexStyle.page, + FiltersMenu(books, filters), + div( + IndexStyle.main, + Filters(filters), + Books(books) + ) + ) + ) + } +} diff --git a/src/main/scala/reading/component/index/Books.scala b/src/main/scala/reading/component/index/Books.scala new file mode 100644 index 0000000..6ce1b2b --- /dev/null +++ b/src/main/scala/reading/component/index/Books.scala @@ -0,0 +1,33 @@ +package reading.component.index + +import rx._ + +import scalatags.JsDom.all._ +import scalacss.Defaults._ +import scalacss.ScalatagsCss._ + +import reading.component.index.style.{Books => BooksStyle} +import reading.models.Book +import reading.utils.RxTag + +object Books { + def apply(books: Rx[Seq[Book]]): Frag = + div( + BooksStyle.render, + BooksStyle.books, + + RxTag { implicit context => + div( + books().sortBy(_.title).map { book => + div( + BooksStyle.book, + div(BooksStyle.title, book.title), + div(BooksStyle.author, book.author), + div(BooksStyle.genres, book.genres.mkString(", ")), + div(BooksStyle.themes, book.themes.mkString(", ")) + ) + } + ) + } + ) +} diff --git a/src/main/scala/reading/component/index/Filters.scala b/src/main/scala/reading/component/index/Filters.scala new file mode 100644 index 0000000..1d9cc93 --- /dev/null +++ b/src/main/scala/reading/component/index/Filters.scala @@ -0,0 +1,33 @@ +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.{Filters => FiltersStyle} +import reading.models.Filter +import reading.utils.{RxTag, RxAttr} + +object Filters { + def apply(filters: Var[Seq[Filter]]): Frag = + RxTag { implicit context => + if(filters().isEmpty) + span("") + else + div( + FiltersStyle.render, + FiltersStyle.filters, + + filters().sortBy(_.name).map { filter => + button( + FiltersStyle.filter, + RxAttr(onclick, Rx(() => filters() = filters().filter(!Filter.equals(_, filter)))), + filter.name + ) + } + ) + } +} diff --git a/src/main/scala/reading/component/index/FiltersMenu.scala b/src/main/scala/reading/component/index/FiltersMenu.scala new file mode 100644 index 0000000..880c3e7 --- /dev/null +++ b/src/main/scala/reading/component/index/FiltersMenu.scala @@ -0,0 +1,48 @@ +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.{FiltersMenu => FiltersMenuStyle} +import reading.models.{Book, Filter, Genre, Theme, FilterFactory} +import reading.utils.{RxTag, RxAttr} + +object FiltersMenu { + def apply(books: Rx[Seq[Book]], filters: Var[Seq[Filter]]): Frag = + div( + FiltersMenuStyle.render, + group(books, filters, "Genre", Genre.values), + group(books, filters, "Theme", Theme.values) + ) + + def group[T: FilterFactory](books: Rx[Seq[Book]], filters: Var[Seq[Filter]], name: String, groupFilters: Seq[T]): Frag = { + val filtersWithCount = Rx { + groupFilters + .filter(filter => !Filter.contains(filters(), Filter(filter))) + .map(filter => (filter, Book.filter(books(), Filter(filter) +: filters()).length)) + .filter(_._2 > 0) + } + + div( + FiltersMenuStyle.group, + + div(FiltersMenuStyle.groupTitle, name), + + RxTag { implicit context => + div( + filtersWithCount().map { case (filter, count) => + button( + FiltersMenuStyle.filter, + RxAttr(onclick, Rx(() => filters() = Filter(filter) +: filters())), + span(s"${filter.toString} ($count)") + ) + } + ) + } + ) + } +} diff --git a/src/main/scala/reading/component/index/style/Books.scala b/src/main/scala/reading/component/index/style/Books.scala new file mode 100644 index 0000000..2c0dfc0 --- /dev/null +++ b/src/main/scala/reading/component/index/style/Books.scala @@ -0,0 +1,31 @@ +package reading.component.index.style + +import scalacss.Defaults._ + +import reading.component.style.Col + +object Books extends StyleSheet.Inline { + import dsl._ + + val books = style( + ) + + val book = style( + marginBottom(30.px) + ) + + val title = style( + fontWeight.bold, + marginBottom(10.px), + color(Col.congoBrown) + ) + + val author = style( + ) + + val genres = style( + ) + + val themes = style( + ) +} diff --git a/src/main/scala/reading/component/index/style/Filters.scala b/src/main/scala/reading/component/index/style/Filters.scala new file mode 100644 index 0000000..c2d0aaf --- /dev/null +++ b/src/main/scala/reading/component/index/style/Filters.scala @@ -0,0 +1,21 @@ +package reading.component.index.style + +import scalacss.Defaults._ + +import reading.component.style.Col + +object Filters extends StyleSheet.Inline { + import dsl._ + + val filters = style( + marginBottom(30.px), + display.flex + ) + + val filter = style( + backgroundColor(Col.gray), + color(Col.white), + padding(5.px, 10.px), + marginRight(10.px) + ) +} diff --git a/src/main/scala/reading/component/index/style/FiltersMenu.scala b/src/main/scala/reading/component/index/style/FiltersMenu.scala new file mode 100644 index 0000000..9fd50f0 --- /dev/null +++ b/src/main/scala/reading/component/index/style/FiltersMenu.scala @@ -0,0 +1,24 @@ +package reading.component.index.style + +import scalacss.Defaults._ + +import reading.component.style.Col + +object FiltersMenu extends StyleSheet.Inline { + import dsl._ + + val group = style( + marginBottom(30.px) + ) + + val groupTitle = style( + color(Col.congoBrown), + fontWeight.bold, + textTransform.uppercase, + padding(10.px, 30.px, 15.px) + ) + + val filter = style( + padding(10.px, 30.px) + ) +} diff --git a/src/main/scala/reading/component/style/Color.scala b/src/main/scala/reading/component/style/Color.scala new file mode 100644 index 0000000..b9f9cf4 --- /dev/null +++ b/src/main/scala/reading/component/style/Color.scala @@ -0,0 +1,16 @@ +package reading.component.style + +import scalacss.Defaults._ + +// http://chir.ag/projects/name-that-color +object Col extends StyleSheet.Inline { + import dsl._ + + val black = c"#000000" + val white = c"#FFFFFF" + val gray = c"#7E7E7E" + val eastBay = c"#505080" + val tawnyPort = c"#7F2447" + val cosmic = c"#683649" + val congoBrown = c"#57363E" +} diff --git a/src/main/scala/reading/component/style/Global.scala b/src/main/scala/reading/component/style/Global.scala new file mode 100644 index 0000000..276a30d --- /dev/null +++ b/src/main/scala/reading/component/style/Global.scala @@ -0,0 +1,28 @@ +package reading.component.style + +import scalacss.Defaults._ + +object Global extends StyleSheet.Standalone { + import dsl._ + + "html" - + boxSizing.borderBox + + "a" - ( + color(Col.eastBay), + &.hover ( + textDecoration := "underline" + ) + ) + + "*, *:before, *:after" - + boxSizing.inherit + + "button" - ( + cursor.pointer, + display.flex, + backgroundColor(initial), + color(Col.black), + border.none + ) +} diff --git a/src/main/scala/reading/component/style/Index.scala b/src/main/scala/reading/component/style/Index.scala new file mode 100644 index 0000000..78e0630 --- /dev/null +++ b/src/main/scala/reading/component/style/Index.scala @@ -0,0 +1,26 @@ +package reading.component.style + +import scalacss.Defaults._ + +object Index extends StyleSheet.Inline { + import dsl._ + + val header = style( + fontSize(40.px), + color(Col.congoBrown), + textAlign.center, + margin(10.px, auto), + padding(20.px), + &.hover ( + textDecoration := "none" + ) + ) + + val page = style( + display.flex + ) + + val main = style( + marginLeft(20.px) + ) +} diff --git a/src/main/scala/reading/models/Book.scala b/src/main/scala/reading/models/Book.scala new file mode 100644 index 0000000..1e4b81a --- /dev/null +++ b/src/main/scala/reading/models/Book.scala @@ -0,0 +1,21 @@ +package reading.models + +case class Book( + title: String, + author: String, + genres: Seq[Genre], + themes: Seq[Theme] +) + +object Book { + def all: Seq[Book] = Seq( + Book("Les dix petits nègres", "Agatha Christie", Seq(Genre.Detective), Seq(Theme.Fear)), + Book("Le joueur", "Fiódor Dostoyevski", Seq(Genre.Adventure), Seq(Theme.Fear)), + Book("Voyage au bout de la nuit", "Céline", Seq(Genre.Adventure), Seq(Theme.Fear)), + Book("Le petit prince", "Antoine de Saint Exupéry", Seq(Genre.Adventure), Seq(Theme.Friendship)), + Book("Les frères Karamazov", "Fiódor Dostoyevski", Seq(Genre.Adventure), Seq(Theme.Family)) + ) + + def filter(books: Seq[Book], filters: Seq[Filter]): Seq[Book] = + books.filter(b => filters.forall(_.filter(b))) +} diff --git a/src/main/scala/reading/models/Filter.scala b/src/main/scala/reading/models/Filter.scala new file mode 100644 index 0000000..c4836bb --- /dev/null +++ b/src/main/scala/reading/models/Filter.scala @@ -0,0 +1,46 @@ +package reading.models + +trait Filter { + def filter(book: Book): Boolean + def kind: FilterKind + def name: String +} + +sealed trait FilterKind +case object ThemeKind extends FilterKind +case object GenreKind extends FilterKind + +object Filter { + def apply[T](in: T)(implicit filterFactory: FilterFactory[T]): Filter = + filterFactory.create(in) + + def contains(filters: Seq[Filter], filter: Filter): Boolean = + filters.find(f => f.kind == filter.kind && f.name == filter.name).nonEmpty + + def equals(f1: Filter, f2: Filter): Boolean = + f1.kind == f2.kind && f1.name == f2.name +} + +trait FilterFactory[T] { + def create(in: T): Filter +} + +object FilterFactory { + 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() + } + } +} diff --git a/src/main/scala/reading/models/Genre.scala b/src/main/scala/reading/models/Genre.scala new file mode 100644 index 0000000..44da79d --- /dev/null +++ b/src/main/scala/reading/models/Genre.scala @@ -0,0 +1,21 @@ +package reading.models + +import enumeratum._ + +sealed trait Genre extends EnumEntry { + override def toString(): String = this match { + case Genre.Adventure => "aventure" + case Genre.Fantastic => "fantastique" + case Genre.Detective => "policier" + case Genre.Marvellous => "merveilleux" + } +} + +object Genre extends Enum[Genre] { + val values = findValues + + case object Adventure extends Genre + case object Fantastic extends Genre + case object Detective extends Genre + case object Marvellous extends Genre +} diff --git a/src/main/scala/reading/models/Theme.scala b/src/main/scala/reading/models/Theme.scala new file mode 100644 index 0000000..ed7ee0b --- /dev/null +++ b/src/main/scala/reading/models/Theme.scala @@ -0,0 +1,21 @@ +package reading.models + +import enumeratum._ + +sealed trait Theme extends EnumEntry { + override def toString(): String = this match { + case Theme.Love => "amour" + case Theme.Friendship => "amitié" + case Theme.Family => "famille" + case Theme.Fear => "peur" + } +} + +object Theme extends Enum[Theme] { + val values = findValues + + case object Love extends Theme + case object Friendship extends Theme + case object Family extends Theme + case object Fear extends Theme +} diff --git a/src/main/scala/reading/utils/Rx.scala b/src/main/scala/reading/utils/Rx.scala new file mode 100644 index 0000000..83de617 --- /dev/null +++ b/src/main/scala/reading/utils/Rx.scala @@ -0,0 +1,44 @@ +package reading.utils + +import scala.util.{Failure, Success} + +import org.scalajs.dom.Element + +import scalatags.JsDom.all._ +import rx._ + +import Ctx.Owner.Unsafe._ + +object RxTag { + def apply(r: Ctx.Data => HtmlTag): HtmlTag = + rxMod(Rx(r(implicitly[Ctx.Data]))) + + 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 + } + var last = rSafe + r.trigger { + val newLast = rSafe + Option(last.parentElement).foreach { + _.replaceChild(newLast, last) + } + last = newLast + } + span( + bindNode(last) + ) + } +} + +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) } + } + } + scalatags.generic.AttrPair(attr, v, attrValue) + } +} |