From b780f6f660cc5b2ff2b1ca3884871002823256a1 Mon Sep 17 00:00:00 2001
From: Joris
Date: Sun, 27 Nov 2016 18:38:58 +0100
Subject: Init
---
 src/main/resources/index-dev.html                  | 13 ++++++
 src/main/resources/index-prod.html                 | 13 ++++++
 src/main/resources/reset.css                       | 51 ++++++++++++++++++++++
 src/main/scala/reading/Main.scala                  | 19 ++++++++
 src/main/scala/reading/component/Index.scala       | 42 ++++++++++++++++++
 src/main/scala/reading/component/index/Books.scala | 33 ++++++++++++++
 .../scala/reading/component/index/Filters.scala    | 33 ++++++++++++++
 .../reading/component/index/FiltersMenu.scala      | 48 ++++++++++++++++++++
 .../reading/component/index/style/Books.scala      | 31 +++++++++++++
 .../reading/component/index/style/Filters.scala    | 21 +++++++++
 .../component/index/style/FiltersMenu.scala        | 24 ++++++++++
 src/main/scala/reading/component/style/Color.scala | 16 +++++++
 .../scala/reading/component/style/Global.scala     | 28 ++++++++++++
 src/main/scala/reading/component/style/Index.scala | 26 +++++++++++
 src/main/scala/reading/models/Book.scala           | 21 +++++++++
 src/main/scala/reading/models/Filter.scala         | 46 +++++++++++++++++++
 src/main/scala/reading/models/Genre.scala          | 21 +++++++++
 src/main/scala/reading/models/Theme.scala          | 21 +++++++++
 src/main/scala/reading/utils/Rx.scala              | 44 +++++++++++++++++++
 19 files changed, 551 insertions(+)
 create mode 100644 src/main/resources/index-dev.html
 create mode 100644 src/main/resources/index-prod.html
 create mode 100644 src/main/resources/reset.css
 create mode 100644 src/main/scala/reading/Main.scala
 create mode 100644 src/main/scala/reading/component/Index.scala
 create mode 100644 src/main/scala/reading/component/index/Books.scala
 create mode 100644 src/main/scala/reading/component/index/Filters.scala
 create mode 100644 src/main/scala/reading/component/index/FiltersMenu.scala
 create mode 100644 src/main/scala/reading/component/index/style/Books.scala
 create mode 100644 src/main/scala/reading/component/index/style/Filters.scala
 create mode 100644 src/main/scala/reading/component/index/style/FiltersMenu.scala
 create mode 100644 src/main/scala/reading/component/style/Color.scala
 create mode 100644 src/main/scala/reading/component/style/Global.scala
 create mode 100644 src/main/scala/reading/component/style/Index.scala
 create mode 100644 src/main/scala/reading/models/Book.scala
 create mode 100644 src/main/scala/reading/models/Filter.scala
 create mode 100644 src/main/scala/reading/models/Genre.scala
 create mode 100644 src/main/scala/reading/models/Theme.scala
 create mode 100644 src/main/scala/reading/utils/Rx.scala
(limited to 'src/main')
diff --git a/src/main/resources/index-dev.html b/src/main/resources/index-dev.html
new file mode 100644
index 0000000..f9b17e7
--- /dev/null
+++ b/src/main/resources/index-dev.html
@@ -0,0 +1,13 @@
+
+
+
+  
+    
+    reading
+    
+    
+  
+
+  
+  
+
diff --git a/src/main/resources/index-prod.html b/src/main/resources/index-prod.html
new file mode 100644
index 0000000..93ae2f0
--- /dev/null
+++ b/src/main/resources/index-prod.html
@@ -0,0 +1,13 @@
+
+
+
+  
+    
+    reading
+    
+    
+  
+
+  
+  
+
diff --git a/src/main/resources/reset.css b/src/main/resources/reset.css
new file mode 100644
index 0000000..636e49f
--- /dev/null
+++ b/src/main/resources/reset.css
@@ -0,0 +1,51 @@
+/* http://meyerweb.com/eric/tools/css/reset/
+ * v2.0 | 20110126
+ * License: none (public domain)
+ */
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font-size: 100%;
+  font: inherit;
+  vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+  display: block;
+}
+body {
+  line-height: 1;
+}
+ol, ul {
+  list-style: none;
+}
+blockquote, q {
+  quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+  content: '';
+  content: none;
+}
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+a {
+  text-decoration: none;
+}
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)
+  }
+}
-- 
cgit v1.2.3