From bbe5788cdcfbb26358566bfc74426ec38029cc73 Mon Sep 17 00:00:00 2001
From: Joris
Date: Sat, 25 Feb 2017 22:25:03 +0100
Subject: Add detailed book page instead of a modal.
---
 src/main/scala/reading/Main.scala                  |   2 +-
 src/main/scala/reading/Route.scala                 |  55 ++++--
 src/main/scala/reading/component/Index.scala       |  11 +-
 .../scala/reading/component/index/BookDetail.scala |  81 +++++---
 src/main/scala/reading/component/index/Books.scala |  94 ++++++---
 .../reading/component/index/FilterUtils.scala      |   6 +
 .../scala/reading/component/index/Filters.scala    |  65 +++++++
 .../scala/reading/component/index/Header.scala     |  45 +----
 src/main/scala/reading/component/index/Menu.scala  | 104 +++++-----
 .../reading/component/index/style/BookDetail.scala |  65 +++++--
 .../reading/component/index/style/Books.scala      |  15 ++
 .../reading/component/index/style/Filters.scala    |  69 +++++++
 .../reading/component/index/style/Header.scala     |  61 +-----
 .../scala/reading/component/index/style/Menu.scala |  52 ++---
 .../scala/reading/component/style/Global.scala     |   3 +-
 src/main/scala/reading/component/style/Index.scala |   4 +-
 .../scala/reading/component/widget/Animate.scala   |  17 +-
 .../reading/component/widget/AnimateMethod.scala   |  29 +++
 .../scala/reading/component/widget/Input.scala     |  23 ++-
 .../scala/reading/component/widget/Modal.scala     |  59 ------
 .../reading/component/widget/style/Input.scala     |  15 ++
 .../reading/component/widget/style/Modal.scala     |  65 -------
 src/main/scala/reading/models/Book.scala           |   3 +-
 src/main/scala/reading/models/Books.scala          | 209 +++++++++++++++++++--
 src/main/scala/reading/models/FilterFactory.scala  |   2 +-
 src/main/scala/reading/models/Genre.scala          |   4 +-
 src/main/scala/reading/models/Grade.scala          |   2 +-
 src/main/scala/reading/models/GroupedTheme.scala   |   5 +-
 src/main/scala/reading/models/Level.scala          |   2 +-
 src/main/scala/reading/models/Period.scala         |  14 +-
 src/main/scala/reading/models/Theme.scala          |   4 +-
 31 files changed, 729 insertions(+), 456 deletions(-)
 create mode 100644 src/main/scala/reading/component/index/Filters.scala
 create mode 100644 src/main/scala/reading/component/index/style/Filters.scala
 create mode 100644 src/main/scala/reading/component/widget/AnimateMethod.scala
 delete mode 100644 src/main/scala/reading/component/widget/Modal.scala
 delete mode 100644 src/main/scala/reading/component/widget/style/Modal.scala
(limited to 'src')
diff --git a/src/main/scala/reading/Main.scala b/src/main/scala/reading/Main.scala
index a9fc3e2..b97abd2 100644
--- a/src/main/scala/reading/Main.scala
+++ b/src/main/scala/reading/Main.scala
@@ -18,7 +18,7 @@ object Main extends JSApp {
     val _ = dom.document.body.appendChild {
       Rx {
         Route.current() match {
-          case Route.Books(filters) => component.Index(filters)
+          case Route.Books(filters, detail) => component.Index(filters, detail)
         }
       }.render
     }
diff --git a/src/main/scala/reading/Route.scala b/src/main/scala/reading/Route.scala
index 9295d49..76a9431 100644
--- a/src/main/scala/reading/Route.scala
+++ b/src/main/scala/reading/Route.scala
@@ -6,12 +6,12 @@ import scala.scalajs.js.URIUtils
 
 import rx.Var
 
-import reading.models.{ Filter, FilterKind }
+import reading.models.{ Filter, FilterKind, Book, Books => BooksModel }
 
 sealed trait Route
 
 object Route {
-  case class Books(filters: Seq[Filter]) extends Route
+  case class Books(filters: Seq[Filter] = Nil, detail: Option[Book] = None) extends Route
 
   val current: Var[Route] = Var(parse(window.location.hash))
 
@@ -22,20 +22,22 @@ object Route {
   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
-          }
+        val filters = params.flatMap {
+          case Param(key, value) =>
+            for {
+              kind <- FilterKind.withNameOption(key)
+              filter <- Filter(kind, value)
+            } yield filter
+          case _ =>
+            None
         }
-        Books(filters)
+        val detail = params.collectFirst {
+          case Param("detail", title) => BooksModel().find(_.title == title)
+        }.flatten
+        Books(filters, detail)
       }
       case _ =>
-        Books(Nil)
+        Books()
     }
 
   def pathAndParams(hash: String): (List[String], List[String]) = {
@@ -48,7 +50,11 @@ object Route {
 
   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(filters, detail) => {
+        val filterParams = filters.map(filter => (filter.kind.toString, filter.nonFormattedName))
+        val detailParams = detail.map(book => ("detail", book.title))
+        s"/books${Param.format(filterParams ++ detailParams)}"
+      }
       case _ => "/books"
     }
     window.location.origin + window.location.pathname + "#" + URIUtils.encodeURI(hash)
@@ -63,3 +69,24 @@ object Route {
     window.history.pushState(null, "", url(route));
   }
 }
+
+object Param {
+  def apply(key: String, value: String): (String, String) = (key, value)
+  def unapply(x: Any): Option[(String, String)] =
+    x match {
+      case str: String =>
+        str.split("=") match {
+          case Array(key, value) => Some((key, value))
+          case _ => None
+        }
+      case _ => None
+    }
+
+  def format(params: Seq[(String, String)]): String =
+    if (params.isEmpty)
+      ""
+    else
+      params
+        .map { case (key, value) => s"$key=$value" }
+        .mkString("?", "&", "")
+}
diff --git a/src/main/scala/reading/component/Index.scala b/src/main/scala/reading/component/Index.scala
index 78890de..0105150 100644
--- a/src/main/scala/reading/component/Index.scala
+++ b/src/main/scala/reading/component/Index.scala
@@ -5,26 +5,25 @@ import scalacss.Defaults._
 import scalacss.ScalatagsCss._
 import scalatags.JsDom.all._
 
-import reading.component.index.{ Menu, Header, Books => BooksComponent }
+import reading.component.index.{ Menu, Books => BooksComponent }
 import reading.component.style.{ Index => IndexStyle }
 import reading.models.{ Book, Books, Filter }
 
 object Index {
-  def apply(initialFilters: Seq[Filter])(implicit ctx: Ctx.Owner): Frag = {
+  def apply(initialFilters: Seq[Filter], initialDetail: Option[Book])(implicit ctx: Ctx.Owner): Frag = {
     val filters: Var[Seq[Filter]] = Var(initialFilters)
     val books: Rx[Seq[Book]] = Rx(Filter.add(Books(), filters()))
     val search: Var[String] = Var("")
     val showFiltersMenu: Var[Boolean] = Var(false)
-    val searchedBooks: Rx[Seq[Book]] = Rx(Book.filter(books(), search()))
+    val detail: Var[Option[Book]] = Var(initialDetail)
 
     div(
       IndexStyle.render,
       IndexStyle.page,
-      Menu(books, filters, search, showFiltersMenu),
+      Menu(books, filters, detail, search, showFiltersMenu),
       div(
         IndexStyle.main,
-        Header(searchedBooks, filters, search, showFiltersMenu),
-        BooksComponent(searchedBooks)
+        BooksComponent(books, filters, detail, search, showFiltersMenu)
       )
     )
   }
diff --git a/src/main/scala/reading/component/index/BookDetail.scala b/src/main/scala/reading/component/index/BookDetail.scala
index c42029f..ed91211 100644
--- a/src/main/scala/reading/component/index/BookDetail.scala
+++ b/src/main/scala/reading/component/index/BookDetail.scala
@@ -1,17 +1,28 @@
 package reading.component.index
 
-import scalatags.JsDom.all._
+import scala.util.Random
+
 import scalacss.Defaults._
 import scalacss.ScalatagsCss._
+import scalatags.JsDom.all._
 
 import reading.component.index.style.{ BookDetail => BookStyle }
-import reading.models.{ Program, Book }
+import reading.component.widget.AnimateMethod
+import reading.models.{ Book, Program }
 
 object BookDetail {
-  def apply(book: Book): Frag =
+  val componentId = s"books${Random.nextInt}"
+
+  def apply(book: Book, parentId: String, onClose: => Unit): Frag = {
+    val titleParts = if (book.parts > 1) s", ${book.parts} volumes" else ""
+    val grades = book.programs.map(Program.grade(_)).distinct.sorted
+
+    AnimateMethod.fadeIn(componentId)
+
     div(
       BookStyle.render,
       BookStyle.detail,
+      id := componentId,
 
       img(
         BookStyle.cover,
@@ -20,36 +31,48 @@ object BookDetail {
       ),
 
       div(
-        BookStyle.items,
+        BookStyle.presentation,
+        div(BookStyle.title, s"${book.title}$titleParts"),
+        div(BookStyle.author, book.author),
 
-        if (book.programs.nonEmpty) {
-          item("classe", book.programs.map(Program.grade(_).prettyPrint).distinct.sorted)
-        },
-        if (book.programs.nonEmpty) {
-          item("programme", book.programs.map(p => "« " ++ p.prettyPrint ++ " »").sorted)
-        },
-        if (book.themes.nonEmpty) {
-          item("thème", book.themes.sorted.map(_.prettyPrint))
+        book.resume match {
+          case Some(resume) =>
+            p(BookStyle.resume, raw(resume))
+          case _ =>
+            span("")
         },
-        if (book.genres.nonEmpty) {
-          item("genre", book.genres.sorted.map(_.prettyPrint))
-        },
-        book.period.map { period =>
-          item("période", period.prettyPrint)
-        },
-        item("niveau", book.level.prettyPrint)
-      )
-    )
 
-  private def item(key: String, value: String): Frag = item(key, Seq(value))
+        dl(
+          BookStyle.definitions,
 
-  private def item(key: String, values: Seq[String]): Frag =
-    div(
-      BookStyle.item,
-      div(BookStyle.itemName, key),
-      ul(
-        BookStyle.itemValues,
-        values.map(value => li(BookStyle.itemValue, value.capitalize))
+          grades.map { grade =>
+            val programs = book.programs.filter(p => Program.grade(p) == grade).sorted
+            val pp = grade.prettyPrint
+            definition(pp, pp, programs.map(p => s"« ${p.prettyPrint} »"))
+          },
+          if (book.themes.nonEmpty) {
+            definition("thème", "thèmes", book.themes.sorted.map(_.prettyPrint))
+          },
+          if (book.genres.nonEmpty) {
+            definition("genre", "genres", book.genres.sorted.map(_.prettyPrint))
+          },
+          definition("niveau", "niveaux", Seq(book.level.prettyPrint))
+        ),
+
+        button(
+          BookStyle.close,
+          onclick := (() => onClose),
+          "Fermer"
+        )
       )
     )
+  }
+
+  private def definition(key: String, pluralKey: String, values: Seq[String]): Seq[Frag] = {
+    val term = if (values.length > 1) pluralKey else key
+    Seq(
+      dt(BookStyle.definitionTerm, s"${term.capitalize} :"),
+      dd(BookStyle.definitionDescription, values.mkString(", "))
+    )
+  }
 }
diff --git a/src/main/scala/reading/component/index/Books.scala b/src/main/scala/reading/component/index/Books.scala
index c22639f..b5e172b 100644
--- a/src/main/scala/reading/component/index/Books.scala
+++ b/src/main/scala/reading/component/index/Books.scala
@@ -1,49 +1,89 @@
 package reading.component.index
 
-import rx._
+import scala.util.Random
 
-import scalatags.JsDom.all._
+import rx._
 import scalacss.Defaults._
 import scalacss.ScalatagsCss._
+import scalatags.JsDom.all._
 
 import reading.component.index.style.{ Books => BooksStyle }
-import reading.component.widget.Modal
-import reading.models.{ Book }
+import reading.component.widget.AnimateMethod
+import reading.models.{ Book, Filter }
+import reading.Route
 import reading.utils.RxUtils._
 
 object Books {
-  def apply(books: Rx[Seq[Book]])(implicit ctx: Ctx.Owner): Frag = {
-    val focus: Var[Option[Book]] = Var(None)
+  val componentId = s"books${Random.nextInt}"
+
+  def apply(
+    books: Rx[Seq[Book]],
+    filters: Var[Seq[Filter]],
+    detail: Var[Option[Book]],
+    search: Var[String],
+    showFiltersMenu: Var[Boolean]
+  )(
+    implicit
+    ctx: Ctx.Owner
+  ): Frag = {
+    val searchedBooks: Rx[Seq[Book]] = Rx(Book.filter(books(), search()))
+
+    if (detail.now.isEmpty) AnimateMethod.fadeIn(id = componentId)
 
     div(
-      BooksStyle.render,
+      BooksStyle.booksParent,
+
+      div(
+        id := componentId,
+        BooksStyle.render,
+        BooksStyle.books,
+
+        Header(searchedBooks, filters, detail, search, showFiltersMenu),
 
-      Rx {
         div(
-          div(
-            BooksStyle.books,
-
-            books().sorted.map { book =>
-              div(
-                BooksStyle.book,
-                img(
-                  BooksStyle.cover,
-                  src := s"cover/${book.title}.jpg",
-                  alt := s"${book.title}, ${book.author}",
-                  onclick := (() => focus() = Some(book))
-                )
-              )
-            }
-          ),
+          BooksStyle.listParent,
 
           Rx {
-            focus() match {
-              case Some(book) => Modal(onClose = focus() = None)(BookDetail(book))
-              case None => span("")
-            }
+            div(
+              BooksStyle.list,
+
+              searchedBooks().sorted.map { book =>
+                div(
+                  BooksStyle.book,
+                  img(
+                    BooksStyle.cover,
+                    src := s"cover/${book.title}.jpg",
+                    alt := s"${book.title}, ${book.author}",
+                    onclick := (() => {
+                      Route.push(Route.Books(filters.now, Some(book)))
+                      AnimateMethod.fadeOut(
+                        id = componentId,
+                        onEnd = detail() = Some(book)
+                      )
+                    })
+                  )
+                )
+              }
+            )
           }
         )
+      ),
+
+      Rx {
+        detail() match {
+          case Some(book) =>
+            BookDetail(book, componentId, onClose = closeDetail(filters, detail))
+          case None =>
+            span("")
+        }
       }
     )
   }
+
+  def closeDetail(filters: Var[Seq[Filter]], detail: Var[Option[Book]]): Unit =
+    AnimateMethod.fadeOut(BookDetail.componentId, onEnd = {
+      detail() = None
+      Route.push(Route.Books(filters.now, None))
+      AnimateMethod.fadeIn(componentId)
+    })
 }
diff --git a/src/main/scala/reading/component/index/FilterUtils.scala b/src/main/scala/reading/component/index/FilterUtils.scala
index d4b24e4..89f993a 100644
--- a/src/main/scala/reading/component/index/FilterUtils.scala
+++ b/src/main/scala/reading/component/index/FilterUtils.scala
@@ -8,31 +8,37 @@ import reading.Route
 object FilterUtils {
   def remove(
     filters: Var[Seq[Filter]],
+    detail: Var[Option[Book]],
     search: Var[String],
     filter: Filter
   ): Unit = {
     val newFilters = Filter.remove(filters.now, filter)
     filters() = newFilters
+    if (detail.now.nonEmpty) Books.closeDetail(filters, detail)
     search() = ""
     Route.push(Route.Books(newFilters))
   }
 
   def removeAll(
     filters: Var[Seq[Filter]],
+    detail: Var[Option[Book]],
     search: Var[String]
   ): Unit = {
     filters() = Nil
+    if (detail.now.nonEmpty) Books.closeDetail(filters, detail)
     search() = ""
     Route.push(Route.Books(Nil))
   }
 
   def add(
     filters: Var[Seq[Filter]],
+    detail: Var[Option[Book]],
     search: Var[String],
     filter: Filter
   ): Unit = {
     val newFilters = filter +: filters.now
     filters() = newFilters
+    if (detail.now.nonEmpty) Books.closeDetail(filters, detail)
     search() = ""
     Route.push(Route.Books(newFilters))
   }
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..3aa26e8
--- /dev/null
+++ b/src/main/scala/reading/component/index/Filters.scala
@@ -0,0 +1,65 @@
+package reading.component.index
+
+import rx._
+
+import scalatags.JsDom.all._
+import scalacss.Defaults._
+import scalacss.ScalatagsCss._
+
+import reading.component.index.style.{ Filters => FiltersStyle }
+import reading.component.widget.Cross
+import reading.component.style.{ Color => C }
+import reading.models.{ Filter, Book }
+import reading.utils.RxUtils._
+
+object Filters {
+  def apply(
+    filters: Var[Seq[Filter]],
+    detail: Var[Option[Book]],
+    search: Var[String],
+    showFiltersMenu: Var[Boolean]
+  )(
+    implicit
+    ctx: Ctx.Owner
+  ): Frag = {
+    val filtersCount: Rx[Int] = Rx(filters().length)
+
+    div(
+      FiltersStyle.render,
+      FiltersStyle.filters,
+
+      Rx {
+        div(
+          div(
+            FiltersStyle.showFiltersMenu,
+            onclick := (() => showFiltersMenu() = true),
+            "Filtrer",
+            if (filtersCount() > 0) span(FiltersStyle.filtersCount, filtersCount()) else span("")
+          ),
+
+          if (filters().isEmpty)
+            span("")
+          else
+            div(
+              FiltersStyle.values,
+
+              div(
+                FiltersStyle.clear,
+                onclick := (() => FilterUtils.removeAll(filters, detail, search)),
+                "Effacer les filtres"
+              ),
+
+              filters().sortBy(_.name).map { filter =>
+                div(
+                  FiltersStyle.filter,
+                  onclick := (() => FilterUtils.remove(filters, detail, search, filter)),
+                  span(FiltersStyle.name, filter.name.capitalize),
+                  Cross(15.px, C.gray.value)
+                )
+              }
+            )
+        )
+      }
+    )
+  }
+}
diff --git a/src/main/scala/reading/component/index/Header.scala b/src/main/scala/reading/component/index/Header.scala
index 50d520e..0809b0c 100644
--- a/src/main/scala/reading/component/index/Header.scala
+++ b/src/main/scala/reading/component/index/Header.scala
@@ -1,14 +1,12 @@
 package reading.component.index
 
 import rx._
-
-import scalatags.JsDom.all._
 import scalacss.Defaults._
 import scalacss.ScalatagsCss._
+import scalatags.JsDom.all._
 
 import reading.component.index.style.{ Header => HeaderStyle }
-import reading.component.widget.{ Cross, Input }
-import reading.component.style.{ Color => C }
+import reading.component.widget.Input
 import reading.models.{ Book, Filter }
 import reading.utils.RxUtils._
 
@@ -16,58 +14,27 @@ object Header {
   def apply(
     books: Rx[Seq[Book]],
     filters: Var[Seq[Filter]],
+    detail: Var[Option[Book]],
     search: Var[String],
     showFiltersMenu: Var[Boolean]
   )(
     implicit
     ctx: Ctx.Owner
   ): Frag = {
-    val filtersCount: Rx[Int] = Rx(filters().length)
     val booksCount: Rx[Int] = books.map(_.length)
 
     div(
       HeaderStyle.render,
       HeaderStyle.header,
 
-      Rx {
-        div(
-          div(
-            HeaderStyle.showFiltersMenu,
-            onclick := (() => showFiltersMenu() = true),
-            "Filtrer",
-            if (filtersCount() > 0) span(HeaderStyle.filtersCount, filtersCount()) else span("")
-          ),
-
-          if (filters().isEmpty)
-            span("")
-          else
-            div(
-              HeaderStyle.filters,
-
-              div(
-                HeaderStyle.clear,
-                onclick := (() => FilterUtils.removeAll(filters, search)),
-                "Effacer les filtres"
-              ),
-
-              filters().sortBy(_.name).map { filter =>
-                div(
-                  HeaderStyle.filter,
-                  onclick := (() => FilterUtils.remove(filters, search, filter)),
-                  span(HeaderStyle.name, filter.name.capitalize),
-                  Cross(15.px, C.black.value)
-                )
-              }
-            )
-        )
-      },
+      Filters(filters, detail, search, showFiltersMenu),
 
       div(
         HeaderStyle.searchAndCount,
-        Input(HeaderStyle.search, search, "Rechercher"),
+        Input(HeaderStyle.search, search, "Rechercher", maxLength = Some(25)),
         Rx {
           div(
-            HeaderStyle.booksCount,
+            HeaderStyle.count,
             span(s"${booksCount()} livre${if (booksCount() > 1) "s" else ""}")
           )
         }
diff --git a/src/main/scala/reading/component/index/Menu.scala b/src/main/scala/reading/component/index/Menu.scala
index 4c118bd..cfeb6d4 100644
--- a/src/main/scala/reading/component/index/Menu.scala
+++ b/src/main/scala/reading/component/index/Menu.scala
@@ -2,9 +2,9 @@ package reading.component.index
 
 import rx._
 
-import scalatags.JsDom.all._
 import scalacss.Defaults._
 import scalacss.ScalatagsCss._
+import scalatags.JsDom.all._
 
 import reading.component.index.style.{ Menu => MenuStyle }
 import reading.models._
@@ -14,6 +14,7 @@ object Menu {
   def apply(
     books: Rx[Seq[Book]],
     filters: Var[Seq[Filter]],
+    detail: Var[Option[Book]],
     search: Var[String],
     showFiltersMenu: Var[Boolean]
   )(
@@ -25,40 +26,34 @@ object Menu {
       Rx(if (showFiltersMenu()) MenuStyle.show else MenuStyle.empty),
       MenuStyle.menu,
 
-      div(MenuStyle.background),
+      Rx(header(showFiltersMenu, filters().length)),
 
       div(
-        MenuStyle.content,
-
-        Rx(header(showFiltersMenu, filters().length)),
-
-        div(
-          MenuStyle.groups,
-          Rx {
-            filters().find(_.kind == FilterKind.Grade) match {
-              case Some(grade) =>
-                val programs = Program.values.filter(p => Program.grade(p).toString() == grade.nonFormattedName)
-                group(books, filters, search, grade.name, programs.map(Filter.apply(_)), Some(grade))
-              case None =>
-                group(books, filters, search, "Classe", Grade.values.map(Filter.apply(_)))
-            }
-          },
-          Rx {
-            filters().find(_.kind == FilterKind.GroupedTheme) match {
-              case Some(groupedTheme) =>
-                val themes = Theme.values.filter(t => Theme.groupedTheme(t).toString() == groupedTheme.nonFormattedName)
-                group(books, filters, search, groupedTheme.name, themes.map(Filter.apply(_)), Some(groupedTheme))
-              case None =>
-                group(books, filters, search, "Theme", GroupedTheme.values.map(Filter.apply(_)))
-            }
-          },
-          group(books, filters, search, "Genre", Genre.values.sorted.map(Filter.apply(_))),
-          group(books, filters, search, "Niveau", Level.values.map(Filter.apply(_))),
-          group(books, filters, search, "Période", Period.values.map(Filter.apply(_)))
-        ),
+        MenuStyle.groups,
+        Rx {
+          filters().find(_.kind == FilterKind.Grade) match {
+            case Some(grade) =>
+              val programs = Program.values.filter(p => Program.grade(p).toString() == grade.nonFormattedName).sorted
+              group(books, filters, detail, search, grade.name, grade.name, programs.map(Filter.apply(_)), Some(grade))
+            case None =>
+              group(books, filters, detail, search, "classe", "classes", Grade.values.sorted.map(Filter.apply(_)))
+          }
+        },
+        Rx {
+          filters().find(_.kind == FilterKind.GroupedTheme) match {
+            case Some(groupedTheme) =>
+              val themes = Theme.values.filter(t => Theme.grouped(t).toString() == groupedTheme.nonFormattedName).sorted
+              group(books, filters, detail, search, groupedTheme.name, groupedTheme.name, themes.map(Filter.apply(_)), Some(groupedTheme))
+            case None =>
+              group(books, filters, detail, search, "thème", "thèmes", GroupedTheme.values.sorted.map(Filter.apply(_)))
+          }
+        },
+        group(books, filters, detail, search, "genre", "genres", Genre.values.sorted.map(Filter.apply(_))),
+        group(books, filters, detail, search, "niveau", "niveaux", Level.values.sorted.map(Filter.apply(_))),
+        group(books, filters, detail, search, "période", "périodes", Period.values.sorted.map(Filter.apply(_)))
+      ),
 
-        footer(books, filters, search, showFiltersMenu)
-      )
+      footer(books, filters, detail, search, showFiltersMenu)
     )
 
   def header(showFiltersMenu: Var[Boolean], count: Int): Frag =
@@ -71,8 +66,10 @@ object Menu {
   def group(
     books: Rx[Seq[Book]],
     filters: Var[Seq[Filter]],
+    detail: Var[Option[Book]],
     search: Var[String],
     name: String,
+    pluralName: String,
     groupFilters: Seq[Filter],
     parentFilter: Option[Filter] = None
   )(
@@ -84,19 +81,20 @@ object Menu {
         .map(filter => (filter, Filter.add(books(), filter).length))
         .filter(_._2 > 0)
     }
+    val filtersCount = filtersWithCount.map(_.length)
 
     Rx {
-      if (filtersWithCount().isEmpty)
+      if (filtersCount() == 0)
         span("")
       else
         div(
           div(
             MenuStyle.filterTitle,
             parentFilter.map { filter =>
-              onclick := (() => FilterUtils.remove(filters, search, filter))
+              onclick := (() => FilterUtils.remove(filters, detail, search, filter))
             }.getOrElse(""),
             if (parentFilter.isDefined) MenuStyle.activeFilter else "",
-            name,
+            if (filtersCount() > 1) pluralName else name,
             Rx {
               val count = filters().filter(f => groupFilters.exists(f == _)).length
               if (count > 0) span(MenuStyle.filterTitleCount, count) else span("")
@@ -112,9 +110,9 @@ object Menu {
                   if (isActive) MenuStyle.activeFilter else "",
                   onclick := (() =>
                     if (isActive)
-                      FilterUtils.remove(filters, search, filter)
+                      FilterUtils.remove(filters, detail, search, filter)
                     else
-                      FilterUtils.add(filters, search, filter)),
+                      FilterUtils.add(filters, detail, search, filter)),
                   span(
                     span(filter.name.capitalize),
                     span(MenuStyle.filterCount, count)
@@ -130,26 +128,32 @@ object Menu {
   def footer(
     books: Rx[Seq[Book]],
     filters: Var[Seq[Filter]],
+    detail: Var[Option[Book]],
     search: Var[String],
     showFiltersMenu: Var[Boolean]
   )(
     implicit
     ctx: Ctx.Owner
   ): Frag =
-    div(
-      MenuStyle.footer,
-      div(
-        MenuStyle.clear,
-        onclick := (() => FilterUtils.removeAll(filters, search)),
-        "Effacer"
-      ),
+    Rx {
       div(
-        MenuStyle.returnToBooks,
-        onclick := (() => showFiltersMenu() = false),
-        "Afficher",
-        Rx {
+        MenuStyle.footer,
+
+        if (filters().nonEmpty)
+          div(
+          MenuStyle.clear,
+          onclick := (() => if (filters.now.nonEmpty) FilterUtils.removeAll(filters, detail, search)),
+          "Effacer"
+        )
+        else
+          span(""),
+
+        div(
+          MenuStyle.returnToBooks,
+          onclick := (() => showFiltersMenu() = false),
+          "Afficher",
           span(MenuStyle.bookCount, books().length)
-        }
+        )
       )
-    )
+    }
 }
diff --git a/src/main/scala/reading/component/index/style/BookDetail.scala b/src/main/scala/reading/component/index/style/BookDetail.scala
index f432fda..2488a8f 100644
--- a/src/main/scala/reading/component/index/style/BookDetail.scala
+++ b/src/main/scala/reading/component/index/style/BookDetail.scala
@@ -2,15 +2,25 @@ package reading.component.index.style
 
 import scalacss.Defaults._
 
-import reading.component.style.{ Color => C }
+import reading.component.style.{ Color => C, Button }
+import reading.Media
 
 object BookDetail extends StyleSheet.Inline {
   import dsl._
 
   val detail = style(
+    position.fixed,
+    height(100.%%),
+    top(0.px),
+    right(0.px),
+    padding(30.px, 30.px, 0.px, 30.px),
+    overflowY.scroll,
     display.flex,
     flexWrap.wrap,
-    justifyContent.spaceAround
+    justifyContent.spaceAround,
+    Media.desktop(width :=! "calc(100% - 280px)"),
+    Media.mobile(width(100.%%)),
+    opacity(0)
   )
 
   val cover = style(
@@ -18,32 +28,47 @@ object BookDetail extends StyleSheet.Inline {
     marginBottom(30.px)
   )
 
-  val items = style(
-    marginBottom(25.px)
+  val presentation = style(
+    Media.desktop(width(50.%%))
   )
 
-  val item = style(
-    lineHeight(25.px),
-    margin(0.px, 15.px, 15.px),
-    &.lastChild(marginBottom(0.px))
+  val title = style(
+    color(C.congoBrown.value),
+    fontSize(26.px),
+    fontWeight.bold,
+    marginBottom(1.em),
+    lineHeight(1.4.em)
+  )
+
+  val author = style(
+    fontSize(20.px),
+    marginBottom(1.em)
+  )
+
+  val resume = style(
+    textAlign.justify,
+    lineHeight(1.4.em),
+    marginBottom(2.em)
+  )
+
+  val definitions = style(
+    marginBottom(2.5.em)
   )
 
-  val itemName = style(
+  val definitionTerm = style(
     fontWeight.bold,
-    textTransform.uppercase,
-    marginBottom(10.px)
+    float.left,
+    marginRight(5.px),
+    lineHeight(1.4.em)
   )
 
-  val itemValues = style(
-    marginLeft(15.px)
+  val definitionDescription = style(
+    marginBottom(1.em),
+    lineHeight(1.4.em)
   )
 
-  val itemValue = style(
-    marginBottom(5.px),
-    &.before(
-      content := "\"•\"",
-      color(C.stiletto.value),
-      marginRight(10.px)
-    )
+  val close = style(
+    Button.simple,
+    marginBottom(1.em)
   )
 }
diff --git a/src/main/scala/reading/component/index/style/Books.scala b/src/main/scala/reading/component/index/style/Books.scala
index ca52328..f7c1ffc 100644
--- a/src/main/scala/reading/component/index/style/Books.scala
+++ b/src/main/scala/reading/component/index/style/Books.scala
@@ -9,7 +9,22 @@ import reading.component.style.{ Color => C }
 object Books extends StyleSheet.Inline {
   import dsl._
 
+  val booksParent = style(
+    height(100.%%)
+  )
+
   val books = style(
+    display.flex,
+    flexDirection.column,
+    height(100.%%),
+    opacity(0)
+  )
+
+  val listParent = style(
+    overflowY.scroll
+  )
+
+  val list = style(
     display.flex,
     flexWrap.wrap,
     justifyContent.spaceAround
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..e4346a2
--- /dev/null
+++ b/src/main/scala/reading/component/index/style/Filters.scala
@@ -0,0 +1,69 @@
+package reading.component.index.style
+
+import scalacss.Defaults._
+
+import reading.Media
+import reading.component.style.{ Color => C, Button, Count }
+
+object Filters extends StyleSheet.Inline {
+  import dsl._
+
+  val filters = style(
+    Media.mobile(margin(0.px, 30.px, 10.px))
+  )
+
+  val showFiltersMenu = style(
+    Media.desktop(display.none),
+    Media.mobile(
+      Button.simple,
+      marginBottom(20.px)
+    )
+  )
+
+  val filtersCount = style(
+    Count.major,
+    marginLeft(20.px)
+  )
+
+  val values = style(
+    display.flex,
+    flexWrap.wrap,
+    alignItems.center,
+    Media.desktop(margin(0.px, 40.px, 10.px)),
+    Media.mobile(display.none)
+  )
+
+  private val box = style(
+    display.flex,
+    alignItems.center,
+    height(50.px),
+    marginRight(20.px),
+    marginBottom(10.px),
+    padding(15.px),
+    borderRadius(2.px),
+    border(1.px, solid, C.gray.lighten(60).value),
+    fontSize(18.px),
+    &.hover(cursor.pointer)
+  )
+
+  val clear = style(
+    box,
+    backgroundColor(C.mickado.value),
+    color(C.white.value),
+    &.hover(backgroundColor(C.mickado.lighten(30).value))
+  )
+
+  val filter = style(
+    box,
+    &.hover(borderColor(C.gray.lighten(80).value))
+  )
+
+  val name = style(
+    marginRight(10.px)
+  )
+
+  val cross = style(
+    width(15.px),
+    height(15.px)
+  )
+}
diff --git a/src/main/scala/reading/component/index/style/Header.scala b/src/main/scala/reading/component/index/style/Header.scala
index 2eb6eb2..10ce059 100644
--- a/src/main/scala/reading/component/index/style/Header.scala
+++ b/src/main/scala/reading/component/index/style/Header.scala
@@ -3,73 +3,20 @@ package reading.component.index.style
 import scalacss.Defaults._
 
 import reading.Media
-import reading.component.style.{ Color => C, Button, Count }
+import reading.component.style.{ Color => C }
 
 object Header extends StyleSheet.Inline {
   import dsl._
 
   val header = style(
-    Media.desktop(margin(40.px)),
-    Media.mobile(margin(30.px))
-  )
-
-  val showFiltersMenu = style(
-    Media.desktop(display.none),
-    Media.mobile(
-      Button.simple,
-      marginBottom(20.px)
-    )
-  )
-
-  val filtersCount = style(
-    Count.major,
-    marginLeft(20.px)
-  )
-
-  val filters = style(
-    display.flex,
-    flexWrap.wrap,
-    marginBottom(15.px),
-    Media.mobile(display.none)
-  )
-
-  private val box = style(
-    display.flex,
-    alignItems.center,
-    padding(15.px),
-    marginRight(20.px),
-    marginBottom(15.px),
-    borderRadius(2.px),
-    border(1.px, solid, C.gray.lighten(60).value),
-    fontSize(18.px),
-    &.hover(cursor.pointer)
-  )
-
-  val clear = style(
-    box,
-    backgroundColor(C.mickado.value),
-    color(C.white.value),
-    &.hover(backgroundColor(C.mickado.lighten(30).value))
-  )
-
-  val filter = style(
-    box,
-    &.hover(borderColor(C.gray.lighten(80).value))
-  )
-
-  val name = style(
-    marginRight(10.px)
-  )
-
-  val cross = style(
-    width(15.px),
-    height(15.px)
+    margin(30.px, 0.px)
   )
 
   val searchAndCount = style(
     display.flex,
     flexWrap.wrap,
     alignItems.center,
+    Media.desktop(paddingLeft(40.px)),
     Media.mobile(justifyContent.center)
   )
 
@@ -78,7 +25,7 @@ object Header extends StyleSheet.Inline {
     Media.desktop(marginRight(30.px))
   )
 
-  val booksCount = style(
+  val count = style(
     fontSize(20.px),
     color(C.gray.value),
     Media.mobile(textAlign.center)
diff --git a/src/main/scala/reading/component/index/style/Menu.scala b/src/main/scala/reading/component/index/style/Menu.scala
index dd74039..09f529c 100644
--- a/src/main/scala/reading/component/index/style/Menu.scala
+++ b/src/main/scala/reading/component/index/style/Menu.scala
@@ -9,28 +9,25 @@ object Menu extends StyleSheet.Inline {
   import dsl._
 
   val menu = style(
-    Media.mobile(display.none),
+    height(100.%%),
+    zIndex(1),
     Media.desktop(
+      backgroundColor(C.englishWalnut.value),
       color(C.white.value),
       position.relative,
       width(280.px)
-    )
-  )
-
-  val background = style(
-    Media.desktop(
+    ),
+    Media.mobile(
+      backgroundColor(C.white.value),
+      color(C.black.value),
       position.fixed,
-      width(280.px),
-      height(100.%%),
-      backgroundColor(C.englishWalnut.value),
-      boxShadow := "4px 0px 6px -1px rgba(0, 0, 0, 0.2)"
+      display.none,
+      width(100.%%)
     )
   )
 
-  val content = style(
-    position.relative,
-    width(100.%%),
-    height(100.%%)
+  val show = style(
+    Media.mobile(display.block)
   )
 
   val header = style(
@@ -44,7 +41,6 @@ object Menu extends StyleSheet.Inline {
     textTransform.uppercase,
     fontWeight.bold,
     letterSpacing(1.px),
-    Media.desktop(marginBottom(20.px)),
     Media.mobile(boxShadow := "0px 3px 5px -1px rgba(0, 0, 0, 0.2)")
   )
 
@@ -53,28 +49,13 @@ object Menu extends StyleSheet.Inline {
     marginLeft(20.px)
   )
 
-  val show = style(
-    Media.mobile(
-      display.block,
-      position.fixed,
-      top(0.px),
-      left(0.px),
-      width(100.%%),
-      height(100.%%),
-      zIndex(1),
-      overflowY.scroll,
-      backgroundColor(C.white.value),
-      color(C.black.value)
-    )
-  )
-
   val empty = style()
 
   val groups = style(
-    Media.mobile(
-      height :=! "calc(100% - 120px)",
-      overflowY.auto
-    )
+    overflowY.scroll,
+    paddingTop(20.px),
+    Media.desktop(height :=! "calc(100% - 60px)"),
+    Media.mobile(height :=! "calc(100% - 120px)")
   )
 
   val filterTitle = style(
@@ -155,9 +136,10 @@ object Commons extends StyleSheet.Inline {
 
   val footerButton = style(
     display.flex,
-    width(50.%%),
+    flexGrow(1),
     justifyContent.center,
     alignItems.center,
+    height(50.px),
     margin(5.px),
     textTransform.uppercase,
     fontSize(14.px),
diff --git a/src/main/scala/reading/component/style/Global.scala b/src/main/scala/reading/component/style/Global.scala
index 7501ec3..b7bf38c 100644
--- a/src/main/scala/reading/component/style/Global.scala
+++ b/src/main/scala/reading/component/style/Global.scala
@@ -14,8 +14,7 @@ object Global extends StyleSheet.Standalone {
   "body" - (
     position.absolute,
     width(100.%%),
-    height(100.%%),
-    overflowY.hidden
+    height(100.%%)
   )
 
   "a" - (
diff --git a/src/main/scala/reading/component/style/Index.scala b/src/main/scala/reading/component/style/Index.scala
index e02ebd9..a2be884 100644
--- a/src/main/scala/reading/component/style/Index.scala
+++ b/src/main/scala/reading/component/style/Index.scala
@@ -9,12 +9,12 @@ object Index extends StyleSheet.Inline {
 
   val page = style(
     display.flex,
-    overflowY.scroll,
     height(100.%%)
   )
 
   val main = style(
     Media.desktop(width :=! "calc(100% - 280px)"),
-    Media.mobile(width(100.%%))
+    Media.mobile(width(100.%%)),
+    overflow.hidden
   )
 }
diff --git a/src/main/scala/reading/component/widget/Animate.scala b/src/main/scala/reading/component/widget/Animate.scala
index 0e848aa..6328177 100644
--- a/src/main/scala/reading/component/widget/Animate.scala
+++ b/src/main/scala/reading/component/widget/Animate.scala
@@ -14,16 +14,16 @@ object Animate {
     transition: (Double, Double) => Double,
     animate: (Double, HTMLElement) => Unit,
     onEnd: => Unit = ()
-  ): Unit = {
+  ): Unit =
     animationFrames.get(id) match {
-      case Some(animationFrame) => window.cancelAnimationFrame(animationFrame)
-      case None => ()
+      case Some(animationFrame) =>
+        ()
+      case None =>
+        val animationFrame = window.requestAnimationFrame(ts =>
+          frame(id, ts, duration, transition, animate, onEnd)(ts))
+        animationFrames.put(id, animationFrame)
+        ()
     }
-    val animationFrame = window.requestAnimationFrame(ts =>
-      frame(id, ts, duration, transition, animate, onEnd)(ts))
-    animationFrames.put(id, animationFrame)
-    ()
-  }
 
   private def frame(
     id: String,
@@ -44,6 +44,7 @@ object Animate {
           animationFrames.put(id, animationFrame)
         } else {
           animate(1, element)
+          animationFrames.remove(id)
           onEnd
         }
       case _ =>
diff --git a/src/main/scala/reading/component/widget/AnimateMethod.scala b/src/main/scala/reading/component/widget/AnimateMethod.scala
new file mode 100644
index 0000000..dfe3e46
--- /dev/null
+++ b/src/main/scala/reading/component/widget/AnimateMethod.scala
@@ -0,0 +1,29 @@
+package reading.component.widget
+
+object AnimateMethod {
+  def fadeOut(id: String, onEnd: => Unit = ()): Unit =
+    Animate(
+      id = id,
+      duration = 100,
+      transition = Transition.linear,
+      animate =
+      (progress, element) => {
+        element.style.opacity = s"${1 - progress}"
+        element.style.transform = s"translateX(${20 * progress}px)"
+      },
+      onEnd = onEnd
+    )
+
+  def fadeIn(id: String, onEnd: => Unit = ()): Unit =
+    Animate(
+      id = id,
+      duration = 100,
+      transition = Transition.easeIn,
+      animate =
+      (progress, element) => {
+        element.style.opacity = s"${progress}"
+        element.style.transform = s"translateX(${20 * (1 - progress)}px)"
+      },
+      onEnd = onEnd
+    )
+}
diff --git a/src/main/scala/reading/component/widget/Input.scala b/src/main/scala/reading/component/widget/Input.scala
index 7dac47a..1a1157e 100644
--- a/src/main/scala/reading/component/widget/Input.scala
+++ b/src/main/scala/reading/component/widget/Input.scala
@@ -2,14 +2,15 @@ package reading.component.widget
 
 import scalatags.JsDom.all._
 
-import org.scalajs.dom.KeyboardEvent
 import org.scalajs.dom.html.Input
+import org.scalajs.dom.KeyboardEvent
 
 import scalacss.Defaults._
 import scalacss.ScalatagsCss._
 
 import rx._
 
+import reading.component.style.{ Color => C }
 import reading.component.widget.style.{ Input => InputStyle }
 
 object Input {
@@ -17,28 +18,38 @@ object Input {
     style: StyleA,
     query: Var[String],
     label: String = "",
-    onEnter: => Unit = ()
+    onEnter: => Unit = (),
+    maxLength: Option[Int] = None
   )(
     implicit
     ctx: Ctx.Owner
   ): Frag = {
     val inputBox = input(
-      InputStyle.render,
       InputStyle.input,
-      style,
       placeholder := label,
       onkeyup := { (e: KeyboardEvent) =>
         val input = e.target.asInstanceOf[Input]
         query() = input.value
         input.value = input.value
         if (e.keyCode == 13) onEnter
-      }
+      },
+      maxlength := maxLength.map(_.toString).getOrElse("")
     ).render
 
     query.trigger {
       inputBox.value = query.now
     }
 
-    inputBox
+    div(
+      InputStyle.render,
+      InputStyle.parent,
+      style,
+      inputBox,
+      span(
+        InputStyle.clear,
+        onclick := (() => query() = ""),
+        Cross(15.px, C.gray.value)
+      )
+    )
   }
 }
diff --git a/src/main/scala/reading/component/widget/Modal.scala b/src/main/scala/reading/component/widget/Modal.scala
deleted file mode 100644
index db1f7e6..0000000
--- a/src/main/scala/reading/component/widget/Modal.scala
+++ /dev/null
@@ -1,59 +0,0 @@
-package reading.component.widget
-
-import scala.util.Random
-
-import org.scalajs.dom.raw.HTMLElement
-import scalacss.Defaults._
-import scalacss.ScalatagsCss._
-import scalatags.JsDom.all._
-
-import reading.component.widget.style.{ Modal => ModalStyle }
-
-object Modal {
-  def apply(onClose: => Unit)(content: Frag): Frag = {
-    val modalId = s"modal${Random.nextInt}"
-
-    Animate(
-      id = modalId,
-      duration = 250,
-      transition = Transition.easeOut,
-      animate =
-      (progress, element) => {
-        element.style.opacity = s"$progress"
-        element.childNodes(2) match {
-          case e: HTMLElement => e.style.transform = s"scale(${0.85 + 0.15 * progress})"
-        }
-      }
-    )
-
-    div(
-      ModalStyle.render,
-      ModalStyle.modal,
-      id := modalId,
-
-      div(
-        ModalStyle.curtain,
-        onclick := (() => close(modalId, onClose))
-      ),
-
-      div(
-        ModalStyle.content,
-        content,
-        button(
-          ModalStyle.close,
-          onclick := (() => close(modalId, onClose)),
-          "Fermer"
-        )
-      )
-    )
-  }
-
-  private def close(modalId: String, onClose: => Unit): Unit =
-    Animate(
-      id = modalId,
-      duration = 300,
-      transition = Transition.linear,
-      onEnd = onClose,
-      animate = (progress, element) => element.style.opacity = s"${1 - progress}"
-    )
-}
diff --git a/src/main/scala/reading/component/widget/style/Input.scala b/src/main/scala/reading/component/widget/style/Input.scala
index 967393b..9453640 100644
--- a/src/main/scala/reading/component/widget/style/Input.scala
+++ b/src/main/scala/reading/component/widget/style/Input.scala
@@ -7,10 +7,25 @@ import reading.component.style.{ Color => C }
 object Input extends StyleSheet.Inline {
   import dsl._
 
+  val parent = style(
+    position.relative
+  )
+
   val input = style(
+    height(45.px),
     border(1.px, solid, C.mickado.value),
     borderRadius(2.px),
     padding(10.px),
     &.hover(borderColor(C.gray.value))
   )
+
+  val clear = style(
+    position.absolute,
+    top(0.px),
+    right(10.px),
+    display.flex,
+    height(100.%%),
+    alignItems.center,
+    cursor.pointer
+  )
 }
diff --git a/src/main/scala/reading/component/widget/style/Modal.scala b/src/main/scala/reading/component/widget/style/Modal.scala
deleted file mode 100644
index faf325d..0000000
--- a/src/main/scala/reading/component/widget/style/Modal.scala
+++ /dev/null
@@ -1,65 +0,0 @@
-package reading.component.widget.style
-
-import scalacss.Defaults._
-
-import reading.Media
-import reading.component.style.{ Color => C, Button }
-
-object Modal extends StyleSheet.Inline {
-  import dsl._
-
-  val modal = style(
-    display.flex,
-    justifyContent.center,
-    position.fixed,
-    width(100.%%),
-    height(100.%%),
-    top(0.px),
-    right(0.px),
-    bottom(0.px),
-    left(0.px),
-    opacity(0),
-    overflowY.scroll
-  )
-
-  val curtain = style(
-    Media.desktop(
-      width(100.%%),
-      height(100.%%),
-      position.fixed,
-      top(0.px),
-      left(0.px),
-      backgroundColor(C.black.value),
-      opacity(0.7),
-      cursor.pointer
-    ),
-    Media.mobile(
-      display.none
-    )
-  )
-
-  val content = style(
-    position.relative,
-    backgroundColor(C.white.value),
-    margin.auto,
-    Media.desktop(
-      width(50.%%),
-      borderRadius(5.px)
-    ),
-    Media.mobile(
-      width(100.%%),
-      height(100.%%),
-      overflowY.auto
-    ),
-    padding(30.px, 30.px, 0.px, 30.px)
-  )
-
-  val close = style(
-    Media.desktop(display.none),
-    Media.mobile(
-      Button.simple,
-      marginTop(20.px),
-      marginBottom(30.px)
-    )
-  )
-}
diff --git a/src/main/scala/reading/models/Book.scala b/src/main/scala/reading/models/Book.scala
index 6f4d8dd..923f2ed 100644
--- a/src/main/scala/reading/models/Book.scala
+++ b/src/main/scala/reading/models/Book.scala
@@ -9,7 +9,8 @@ case class Book(
     genres: Seq[Genre],
     themes: Seq[Theme],
     programs: Seq[Program],
-    level: Level
+    level: Level,
+    resume: Option[String] = None
 ) extends Ordered[Book] {
   def compare(that: Book) =
     Compare.format(this.title).compare(Compare.format(that.title))
diff --git a/src/main/scala/reading/models/Books.scala b/src/main/scala/reading/models/Books.scala
index 43ed2b8..69f9ee6 100644
--- a/src/main/scala/reading/models/Books.scala
+++ b/src/main/scala/reading/models/Books.scala
@@ -89,7 +89,7 @@ object Books {
     ),
 
     Book(
-      title = "Lou !",
+      title = "Lou !",
       author = "Julien NEEL",
       year = "2004-2016",
       parts = 7,
@@ -176,7 +176,34 @@ object Books {
       genres = Seq(RomanAventure),
       themes = Seq(Amitie, Aventure),
       programs = Seq(VoyageEtAventure),
-      level = Moyen
+      level = Moyen,
+      resume = Some("""
+        Pour Tom Sawyer, il y a des choses vraiment plus importantes que
+        l’école! Aller à la pêche, par exemple, se battre avec les nouveaux
+        venus au village ou, plus important encore, retrouver son grand ami
+        Huckleberry, qui mène une vie de bohème à l’image de son vagabond de
+        père… Mais à force de se prendre pour des bandits et de faire des
+        expériences de sorcellerie à la nuit tombée, Tom et Huck vont être
+        mêlés à un véritable crime, avec de vrais assassins et un authentique
+        trésor… Un chef-d’œuvre de la littérature américaine.
+        
+        Mark Twain nous avertit : « La plupart des aventures relatées dans ce
+        livre sont vécues. » En effet, Tom Sawyer, chenapan de stature
+        internationale, lui fut inspiré par deux ou trois de ses camarades et
+        Huckleberry Finn est "décrit d’après nature". Plus intéressés par
+        l’aventure que par l’école, les deux garçons jouent aux brigands et aux
+        sorciers. Jusqu’au jour où ils se retrouvent embarqués dans une
+        véritable affaire criminelle…
+        
+        Tom Sawyer est un garçon chez qui l’amour du jeu l’emporte sur tout
+        autre sentiment. Sa famille éplorée le cherche-t-elle partout ? Il joue
+        au pirate et il ne rentrera que pour assister à son propre service
+        funèbre. Le punit-on ? Il transformera la corvée en jeu et la fera
+        exécuter par ses camarades. Son cerveau infatigable s’envole hors de la
+        réalité, entraînant le lecteur vers toutes sortes d’aventures
+        inattendues.
+        Source : Bibliothèque verte, Hachette
+      """)
     ),
 
     Book(
@@ -323,7 +350,18 @@ object Books {
       genres = Seq(Comique),
       themes = Seq(Humour, Amitie, Aventure),
       programs = Seq(Monstre, Autrui, Heros),
-      level = Moyen
+      level = Moyen,
+      resume = Some("""
+        Sophie ne rêve pas, cette nuit-là, quand elle aperçoit de la fenêtre de
+        l’orphelinat une silhouette immense vêtue d’une longue cape et munie
+        d’une curieuse trompette. Une main énorme s’approche et saisit la
+        petite fille terrifiée pour l’emmener au pays des géants. Mais
+        heureusement, Sophie est tombée entre les mains d’un géant peu
+        ordinaire : le Bon Gros Géant, qui se nourrit de légumes et souffle des
+        rêves dans les chambres des enfants… Avec la jeune Sophie, devenez
+        l’ami du géant au grand cœur et apprenez son langage pour le moins
+        loufoque ! Un chef-d’œuvre d’imagination signé Roald Dahl !
+      """)
     ),
 
     Book(
@@ -539,7 +577,19 @@ object Books {
       genres = Seq(Fantasy),
       themes = Seq(Aventure),
       programs = Seq(),
-      level = Moyen
+      level = Moyen,
+      resume = Some("""
+        Bilbo, comme tous les hobbits, est un petit être paisible et sans
+        histoire. Son quotidien est bouleversé un beau jour, lorsque Grandalf
+        le magicien et treize nains barbus l’entraînent dans un voyage
+        périlleux. C’est le début d’une grande aventure, d’une fantastique
+        quête au trésor semée d’embûches et d’épreuves, qui mènera Bilbo
+        jusqu’à la Montagne Solitaire gardée par le dragon Smaug…
+        
+        Prélude au Seigneur des anneaux, Bilbo le Hobbit a été vendu à des
+        millions d’exemplaires depuis sa publication en 1937, s’imposant comme
+        l’un des livres les plus aimés et les plus influents du XXᵉ siècle.
+      """)
     ),
 
     Book(
@@ -974,7 +1024,16 @@ object Books {
       genres = Seq(Fantasy),
       themes = Seq(Conflit, Initiation),
       programs = Seq(),
-      level = Moyen
+      level = Moyen,
+      resume = Some("""
+        Will rêve de devenir chevalier, comme son père, mort en héros au
+        combat. Mais c’est un tout autre destin qui lui est réservé ! Il sera
+        l’apprenti du sombre Halt, un Rôdeur aux pouvoirs troublants, défenseur
+        secret du royaume d’Araluen. Pour maintenir la paix du domaine, Will
+        doit apprendre la magie de la dissimulation et devenir une ombre parmi
+        les ombres. Mais il lui faut faire vite, car le seigneur Morgarath
+        menace de reprendre le pouvoir par le feu et le sang.
+      """)
     ),
 
     Book(
@@ -1059,11 +1118,26 @@ object Books {
       title = "Bichon",
       author = "David GILSON",
       year = "2013-2015",
+      parts = 2,
       period = None,
       genres = Seq(BD),
       themes = Seq(Humour, Ecole, Amitie, Famille, Amour, Homosexualite),
       programs = Seq(),
-      level = Facile
+      level = Facile,
+      resume = Some("""
+        Se déguiser en princesse pour un goûter d’anniversaire, jouer à
+        l’élastique, entamer une chorégraphie en pleine cour de récré… Un peu
+        compliqué quand on aime faire toutes ces choses et qu’on est un petit
+        garçon de 8 ans. Mais pas pour Bichon : il transgresse les règles de la
+        société sans même s’en rendre compte ! Heureusement, sa famille et ses
+        amis l’aiment tel qu’il est. Même que parfois Jean-Marc, le beau garçon
+        du CM2, prend sa défense quand on se moque de lui… David Gilson réussit
+        l’ambitieux pari de raconter avec tendresse et humour la vie
+        quotidienne d’un petit garçon « pas comme les autres ». Bichon ne fait
+        qu’être lui-même et se soucie peu du regard des autres, et cette
+        personnalité déjà si affirmée et si naturelle est un joyeux exemple
+        pour les petits et grands lecteurs !
+      """)
     ),
 
     Book(
@@ -1097,7 +1171,16 @@ object Books {
       genres = Seq(Roman),
       themes = Seq(Enquete, Enfants),
       programs = Seq(),
-      level = Moyen
+      level = Moyen,
+      resume = Some("""
+        Caïus est un âne.
+        La phrase inscrite par Rufus sur sa tablette remporte un grand succès
+        en classe. mais Caïus rougit de colère. Comment Rufus ose-t-il
+        l’insulter, lui, le fils d’un richissime sénateur ? Pourtant, le
+        lendemain, plus personne n’a envie de rire. La même phrase est tracée
+        en lettres rouges sur la façade du temple de Minerve. Or, dans la Rome
+        impériale, le sacrilège est terrible.
+      """)
     ),
 
     Book(
@@ -1253,7 +1336,22 @@ object Books {
       genres = Seq(BD, Fantasy, RomanAventure),
       themes = Seq(Combat, Initiation),
       programs = Seq(Heros),
-      level = Facile
+      level = Facile,
+      resume = Some("""
+        La neige est méchante en cet hiver 1065, elle a décidé de s’en prendre
+        aux hommes. Elle envoie ses légions de flocons de la taille d’un roc
+        sur le Fizzland, avec pour mission d’engloutir les villages vikings et
+        tous leurs habitants. Afin d’échapper à la Démone blanche, Bjorn et sa
+        famille se claquemurent dans la salle commune de la maison de son père,
+        Erik, le colosse sans peur. Tous se préparent à supporter un siège qui
+        risque de durer de longs mois. Lors de cette épreuve exceptionnelle,
+        chacun va dévoiler son cœur et son courage. À l’exception de Bjorn. Lui
+        ne se révèle pas, il se métamorphose. Ce jeune garçon timide et
+        craintif, dont le nez coule comme une source, maigre comme un oisillon
+        et pas très doué pour les armes va brusquement se transformer en un
+        combattant redoutable. Par quel miracle ? Bjorn serait-il un morphir ?
+        Lui-même en doute.
+      """)
     ),
 
     Book(
@@ -1364,7 +1462,18 @@ object Books {
       genres = Seq(Roman),
       themes = Seq(Adolescence, Deuil, Famille),
       programs = Seq(Autrui),
-      level = Facile
+      level = Facile,
+      resume = Some("""
+        Il fait beau, ce jour-là, à la terrasse de l’hôtel. La famille est
+        attablée. On discute d’un temple à visiter. Mais avec cette mer
+        turquoise… Maxime n’a aucune envie de bouger. Il va rester ici,
+        tranquille, à profiter de la plage avec Jade, sa sœur jumelle. Quelques
+        minutes plus tard, une vague apparaît. Une vague qui n’en finit pas de
+        grossir. Une vague qui engloutit tout. Dans leur course folle, Jade
+        lâche la main de son frère. Pour Max, il n’ y a plus de mots. Plus de
+        larmes. Plus de présent. Plus d’avenir. Pourra-t-il survivre à ce
+        drame ?
+      """)
     ),
 
     Book(
@@ -1551,7 +1660,23 @@ object Books {
       genres = Seq(Fantastique),
       themes = Seq(Adolescence, Deuil),
       programs = Seq(Valeurs),
-      level = Moyen
+      level = Moyen,
+      resume = Some("""
+        En Louisiane, tout le monde croit aux esprits. Lanesha, elle, a le don
+        de les voir. « Tu es comme moi, ma chérie, tu as un don de
+        double-vue », lui a expliqué Mama Ya-Ya, la sage-femme qui l’a
+        recueillie à sa naissance. Mama Ya-Ya, savait qu’un ouragan approchait,
+        bien avant que la radio et la télévision n’en parlent. Les dégâts
+        seront incommensurables, répète le présentateur. Tous les habitants de
+        la Nouvelle-Orléans doivent quitter la ville. Mama Ya-Ya est très âgée,
+        et ne possède pas de voiture, alors Lanesha a fait des provisions d’eau
+        et de nourriture, et a cloué des planches sur les fenêtres. Elle ne
+        sait pas ce qui l’attend, mais elle se prépare de toutes ses forces à
+        survivre. Avec TaShlon, le fils des voisins, avec le chien Spot qu’ils
+        viennent d’adopter ensemble. Avec le fantôme silencieux de sa mère, qui
+        est venu pour l’aider. Avec l’amour de Mama Ya-Ya, qui est
+        incommensurable.
+      """)
     ),
 
     Book(
@@ -1573,7 +1698,23 @@ object Books {
       genres = Seq(Fantastique),
       themes = Seq(Adolescence, Viol, Danse),
       programs = Seq(SeRaconter),
-      level = Facile
+      level = Facile,
+      resume = Some("""
+        Lucie a été trouvée, bébé, au pied d’un arbre dans la forêt. Recueillie
+        et adoptée par des parents aimants, elle grandit comme tous les autres
+        enfants. Passionnée de danse, elle rêve d’incarner une sylphide, ces
+        esprits de l’air, à mi-chemin entre les anges et les elfes. Inscrite au
+        conservatoire de Lyon, elle remporte le rôle pour un spectacle et se
+        lance à corps perdu dans les répétitions, ignorant les conseils de son
+        professeur qui lui demande de prendre soin d’elle, refusant de voir ces
+        bosses qui jaillissent de temps en temps dans son dos.
+        
+        Fruit des amours d’un humain et d’une sylphide, elle est un être à
+        part. Au cours d’une promenade dans les bois, elle va faire la
+        connaissance de ses soeurs. Elle n’a pas encore leur légèreté, mais au
+        contact de la nature, elle parvient à déployer ses ailes. Bien sûr,
+        elle doit garder secrète sa métamorphose…
+      """)
     ),
 
     Book(
@@ -1585,7 +1726,20 @@ object Books {
       genres = Seq(Manga, SF),
       themes = Seq(Adolescence, Immortalite, Conflit, Mutant),
       programs = Seq(Reel),
-      level = Moyen
+      level = Moyen,
+      resume = Some("""
+        Renversé par un camion en rentrant de l’école, le jeune Kei meurt sur
+        le coup. Mais quelques instant plus tard, il ressuscite
+        mystérieusement. Dès lors, sa vie de lycéen bascule. Une étrange
+        organisation gouvernementale tente par tous les moyens de le capturer
+        afin de mener des expériences scientifiques sur lui. Rapidement, il
+        apprend qu’il n’est pas le seul être dans cette situation périlleuse,
+        et qu’il semble être ce que certains nomment un Ajin. Personne ne sait
+        exactement comment ils sont apparus ni pourquoi ils existent. Mais les
+        services spéciaux du gouvernement sont prêts à user de tous les moyens
+        pour le découvrir, car rien à leurs yeux n’est plus dangereux pour
+        l’humanité… qu’un être immortel !
+      """)
     ),
 
     Book(
@@ -1609,7 +1763,13 @@ object Books {
       genres = Seq(BD),
       themes = Seq(Amitie, Famille, Humour, Ecole),
       programs = Seq(Resister, Autrui),
-      level = Facile
+      level = Facile,
+      resume = Some("""
+        Ariol est un petit âne qui n’aime pas se lever le matin pour aller à
+        l’école, surtout l’hiver. Dehors, il fait nuit et froid, comme dans le
+        frigo quand la petite lumière est en panne. Mais bon, à l’école, il y a
+        la jolie Pétula ! Alors, Ariol se lève et il y va.
+      """)
     ),
 
     Book(
@@ -1669,7 +1829,12 @@ object Books {
       genres = Seq(Nouvelle, Fantastique, Thriller),
       themes = Seq(Vieillesse),
       programs = Seq(SeRaconter),
-      level = Difficile
+      level = Difficile,
+      resume = Some("""
+        Un recueil de nouvelles auscultant les paradoxes de l’Amérique et
+        abordant des thèmes tels que les souffrances individuelles et
+        collectives, la vieillesse et la mort, la culpabilité, etc.
+      """)
     ),
 
     Book(
@@ -1686,7 +1851,7 @@ object Books {
 
     Book(
       title = "Le Monde Secret de Sombreterre",
-      author = "Cassandra O'DONNELL",
+      author = "Cassandra O’DONNELL",
       year = "2016",
       parts = 2,
       period = None,
@@ -1698,7 +1863,7 @@ object Books {
 
     Book(
       title = "Malenfer",
-      author = "Cassandra O'DONNELL",
+      author = "Cassandra O’DONNELL",
       year = "2014-2015",
       parts = 3,
       period = None,
@@ -1956,7 +2121,15 @@ object Books {
       genres = Seq(Roman),
       themes = Seq(Adolescence, Famille, Homosexualite, Apprentissage),
       programs = Seq(),
-      level = Moyen
+      level = Moyen,
+      resume = Some("""
+        Dante attend les résultats de ses examens. Le courrier qui lui ouvrira
+        les portes de l’université. De sa future vie. Celle dont il a toujours
+        rêvé. Mais quand on sonne enfin à la porte, ce n’est pas le facteur,
+        c’est Mélanie. Son ex-copine, dont il n’a plus entendu parler depuis
+        des mois. Avec un bébé. Le sien. Le leur. Etre père à 17 ans ? Il y a
+        de quoi pleurer. Mais les garçons ne pleurent jamais…
+      """)
     ),
 
     Book(
@@ -2183,7 +2356,7 @@ object Books {
 
     Book(
       title = "Emmett TILL, derniers jours d’une courte vie",
-      author = "Arnaud FLOC'H",
+      author = "Arnaud FLOC’H",
       year = "2015",
       period = Some(Siecle20),
       genres = Seq(BD),
diff --git a/src/main/scala/reading/models/FilterFactory.scala b/src/main/scala/reading/models/FilterFactory.scala
index c7b9fbf..d900af5 100644
--- a/src/main/scala/reading/models/FilterFactory.scala
+++ b/src/main/scala/reading/models/FilterFactory.scala
@@ -18,7 +18,7 @@ object FilterFactory {
   implicit object GroupedTheme extends FilterFactory[GroupedTheme] {
     def create(groupedTheme: GroupedTheme): Filter =
       new Filter {
-        def filter(book: Book): Boolean = book.themes.map(Theme.groupedTheme).contains(groupedTheme)
+        def filter(book: Book): Boolean = book.themes.map(Theme.grouped).contains(groupedTheme)
         val kind: FilterKind = FilterKind.GroupedTheme
         val nonFormattedName: String = groupedTheme.toString()
         val name: String = groupedTheme.prettyPrint()
diff --git a/src/main/scala/reading/models/Genre.scala b/src/main/scala/reading/models/Genre.scala
index 2d55e60..a3a3165 100644
--- a/src/main/scala/reading/models/Genre.scala
+++ b/src/main/scala/reading/models/Genre.scala
@@ -25,8 +25,8 @@ sealed trait Genre extends EnumEntry with Ordered[Genre] {
     case Uchronie => "uchronie"
     case Manga => "manga"
     case Thriller => "thriller"
-    case Epistolaire => "Epistolaire"
-    case Nouvelle => "Nouvelle"
+    case Epistolaire => "epistolaire"
+    case Nouvelle => "nouvelle"
   }
 }
 
diff --git a/src/main/scala/reading/models/Grade.scala b/src/main/scala/reading/models/Grade.scala
index 32c5b72..f54211d 100644
--- a/src/main/scala/reading/models/Grade.scala
+++ b/src/main/scala/reading/models/Grade.scala
@@ -6,7 +6,7 @@ sealed trait Grade extends EnumEntry with Ordered[Grade] {
   import Grade._
 
   def compare(that: Grade): Int = {
-    values.indexOf(that) - values.indexOf(this)
+    values.indexOf(this) - values.indexOf(that)
   }
 
   def prettyPrint(): String = this match {
diff --git a/src/main/scala/reading/models/GroupedTheme.scala b/src/main/scala/reading/models/GroupedTheme.scala
index 9ece7db..61a5281 100644
--- a/src/main/scala/reading/models/GroupedTheme.scala
+++ b/src/main/scala/reading/models/GroupedTheme.scala
@@ -5,9 +5,8 @@ import enumeratum._
 sealed trait GroupedTheme extends EnumEntry with Ordered[GroupedTheme] {
   import GroupedTheme._
 
-  def compare(that: GroupedTheme): Int = {
-    values.indexOf(that) - values.indexOf(this)
-  }
+  def compare(that: GroupedTheme): Int =
+    Compare.format(this.prettyPrint).compare(Compare.format(that.prettyPrint))
 
   def prettyPrint(): String = this match {
     case Culture => "culture"
diff --git a/src/main/scala/reading/models/Level.scala b/src/main/scala/reading/models/Level.scala
index c06776e..9f25165 100644
--- a/src/main/scala/reading/models/Level.scala
+++ b/src/main/scala/reading/models/Level.scala
@@ -6,7 +6,7 @@ sealed trait Level extends EnumEntry with Ordered[Level] {
   import Level._
 
   def compare(that: Level): Int = {
-    values.indexOf(that) - values.indexOf(this)
+    values.indexOf(this) - values.indexOf(that)
   }
 
   def prettyPrint(): String = this match {
diff --git a/src/main/scala/reading/models/Period.scala b/src/main/scala/reading/models/Period.scala
index f148b4c..c0b663e 100644
--- a/src/main/scala/reading/models/Period.scala
+++ b/src/main/scala/reading/models/Period.scala
@@ -6,20 +6,20 @@ sealed trait Period extends EnumEntry with Ordered[Period] {
   import Period._
 
   def compare(that: Period): Int =
-    values.indexOf(that) - values.indexOf(this)
+    values.indexOf(this) - values.indexOf(that)
 
   def prettyPrint(): String = this match {
-    case Antiquite => "Antiquité"
-    case MoyenAge => "Moyen âge"
-    case Renaissance => "Renaissance"
-    case Lumieres => "Lumières"
-    case Louis14 => "Louis XIV"
+    case Antiquite => "antiquité"
+    case MoyenAge => "moyen âge"
+    case Renaissance => "renaissance"
+    case Lumieres => "lumières"
+    case Louis14 => "louis XIV"
     case Siecle18 => "18ème siècle"
     case Siecle19 => "19ème siècle"
     case Siecle20 => "20ème siècle"
     case Annees50 => "années 50"
     case Contemporain => "contemporain"
-    case Futur => "Futur"
+    case Futur => "futur"
   }
 }
 
diff --git a/src/main/scala/reading/models/Theme.scala b/src/main/scala/reading/models/Theme.scala
index 6ab4b9f..66870a4 100644
--- a/src/main/scala/reading/models/Theme.scala
+++ b/src/main/scala/reading/models/Theme.scala
@@ -45,7 +45,7 @@ sealed trait Theme extends EnumEntry with Ordered[Theme] {
     case Guerre => "guerre"
     case Handicap => "handicap"
     case Harcelement => "harcelement"
-    case Homosexualite => "Homosexualité"
+    case Homosexualite => "homosexualité"
     case Humour => "humour"
     case Immortalite => "immortalité"
     case Initiation => "initiation"
@@ -174,7 +174,7 @@ object Theme extends Enum[Theme] {
   case object Viol extends Theme
   case object Voyage extends Theme
 
-  def groupedTheme(theme: Theme): GroupedTheme = {
+  def grouped(theme: Theme): GroupedTheme = {
     import GroupedTheme._
 
     theme match {
-- 
cgit v1.2.3