aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoris2016-11-27 18:38:58 +0100
committerJoris2016-12-20 23:05:43 +0100
commitb780f6f660cc5b2ff2b1ca3884871002823256a1 (patch)
treec530166206743be1925688f8b3319c630208a695
parent0aab7be2cf2cf73c62b305450902cdc3fe77cd4a (diff)
downloadreading-b780f6f660cc5b2ff2b1ca3884871002823256a1.tar.gz
reading-b780f6f660cc5b2ff2b1ca3884871002823256a1.tar.bz2
reading-b780f6f660cc5b2ff2b1ca3884871002823256a1.zip
Init
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml26
-rw-r--r--.tmuxinator.yml12
-rw-r--r--README.md41
-rw-r--r--build.sbt30
-rwxr-xr-xdev2
-rw-r--r--project/plugins.sbt1
-rw-r--r--shell.nix11
-rw-r--r--src/main/resources/index-dev.html13
-rw-r--r--src/main/resources/index-prod.html13
-rw-r--r--src/main/resources/reset.css51
-rw-r--r--src/main/scala/reading/Main.scala19
-rw-r--r--src/main/scala/reading/component/Index.scala42
-rw-r--r--src/main/scala/reading/component/index/Books.scala33
-rw-r--r--src/main/scala/reading/component/index/Filters.scala33
-rw-r--r--src/main/scala/reading/component/index/FiltersMenu.scala48
-rw-r--r--src/main/scala/reading/component/index/style/Books.scala31
-rw-r--r--src/main/scala/reading/component/index/style/Filters.scala21
-rw-r--r--src/main/scala/reading/component/index/style/FiltersMenu.scala24
-rw-r--r--src/main/scala/reading/component/style/Color.scala16
-rw-r--r--src/main/scala/reading/component/style/Global.scala28
-rw-r--r--src/main/scala/reading/component/style/Index.scala26
-rw-r--r--src/main/scala/reading/models/Book.scala21
-rw-r--r--src/main/scala/reading/models/Filter.scala46
-rw-r--r--src/main/scala/reading/models/Genre.scala21
-rw-r--r--src/main/scala/reading/models/Theme.scala21
-rw-r--r--src/main/scala/reading/utils/Rx.scala44
27 files changed, 675 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eb5a316
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..b8926a0
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,26 @@
+image: java:8
+
+before_script:
+ - apt-get update -y
+ - apt-get install apt-transport-https -y
+ # Install SBT
+ - echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list
+ - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823
+ - apt-get update -y
+ - apt-get install sbt -y
+ - sbt sbt-version
+
+pages:
+ stage: deploy
+ script:
+ - sbt clean fullOptJS
+ - mkdir -p public
+ - cp target/scala-2.12/classes/index-prod.html public/index.html
+ - cp target/scala-2.12/classes/reset.css public/reset.css
+ - cp target/scala-2.12/reading-advice-opt.js public/main.js
+ - sed 's/src="[^"]*"/src="main.js"/' -i public/index.html
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/.tmuxinator.yml b/.tmuxinator.yml
new file mode 100644
index 0000000..ec56e7d
--- /dev/null
+++ b/.tmuxinator.yml
@@ -0,0 +1,12 @@
+name: reading
+
+windows:
+ - main:
+ layout: 3747,239x59,0,0{144x59,0,0,0,94x59,145,0[94x30,145,0,1,94x28,145,31,2]}
+ panes:
+ - # Empty
+ - sbt:
+ - sbt
+ - ~fastOptJS
+ - server:
+ - (cd target/scala-2.12; python -m http.server 9000)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d4ff57d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,41 @@
+Reading
+=======
+
+Getting started
+---------------
+
+Client:
+
+``` sh
+sbt "~fastOptJS"
+```
+
+Server:
+
+``` sh
+(cd target/scala-2.12; python -m http.server 9000)
+```
+
+Open your browser at http://localhost:9000/classes/index-dev.html
+
+Build
+-----
+
+``` sh
+sbt fullOptJS
+```
+
+With nix
+--------
+
+Install nix and follow the instructions:
+
+``` sh
+curl https://nixos.org/nix/install | sh
+```
+
+Dev environment:
+
+``` sh
+./dev
+```
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000..2596216
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,30 @@
+enablePlugins(ScalaJSPlugin)
+
+scalaVersion := "2.12.0"
+
+name := "reading"
+
+version := "0.1"
+
+libraryDependencies ++= Seq(
+ "org.scala-js" %%% "scalajs-dom" % "0.9.1",
+ "com.lihaoyi" %%% "scalatags" % "0.6.2",
+ "com.github.japgolly.scalacss" %%% "core" % "0.5.1",
+ "com.github.japgolly.scalacss" %%% "ext-scalatags" % "0.5.1",
+ "com.lihaoyi" %%% "scalarx" % "0.3.2",
+ "com.beachape" %%% "enumeratum" % "1.5.3"
+)
+
+scalacOptions ++= Seq(
+ "-Xfatal-warnings",
+ "-deprecation",
+ "-unchecked",
+ "-feature",
+ "-Xlint",
+ "-Yno-adapted-args",
+ "-Ywarn-dead-code",
+ "-Ywarn-numeric-widen",
+ "-Ywarn-value-discard",
+ "-Ywarn-unused",
+ "-Ywarn-unused-import"
+)
diff --git a/dev b/dev
new file mode 100755
index 0000000..ddd2ada
--- /dev/null
+++ b/dev
@@ -0,0 +1,2 @@
+#!/bin/sh
+nix-shell --command "tmuxinator local"
diff --git a/project/plugins.sbt b/project/plugins.sbt
new file mode 100644
index 0000000..bfdab78
--- /dev/null
+++ b/project/plugins.sbt
@@ -0,0 +1 @@
+addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.13")
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..73eeaf6
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,11 @@
+with import <nixpkgs> {}; {
+ env = stdenv.mkDerivation {
+ name = "env";
+ buildInputs = with pkgs; [
+ sbt
+ python3
+ tmux
+ tmuxinator
+ ];
+ };
+}
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 @@
+<!DOCTYPE html>
+
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>reading</title>
+ <link rel="stylesheet" href="reset.css">
+ <script src="../reading-fastopt.js"></script>
+ </head>
+
+ <body onload="reading.Main().main()">
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>reading</title>
+ <link rel="stylesheet" href="reset.css">
+ <script src="../reading-opt.js"></script>
+ </head>
+
+ <body onload="reading.Main().main()">
+ </body>
+</html>
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)
+ }
+}