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) +  } +} | 
