aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoris2023-02-09 10:03:11 +0100
committerJoris2023-02-09 10:03:11 +0100
commit4e2339ddd1d95c07e7b54dee8565cd07e9e7dc34 (patch)
tree069ad8a9f99473290d49215de7598af5f6788fa7
parent0aa9ef160fe3362a85a6e9b678d1b65756c8e3a0 (diff)
Improve filtering on read status
-rwxr-xr-xbin/get-books2
-rwxr-xr-xbin/migrate/1-read-status19
-rw-r--r--public/main.css69
-rw-r--r--src/main.ts132
4 files changed, 186 insertions, 36 deletions
diff --git a/bin/get-books b/bin/get-books
index 0fbbd2f..d3ac0f6 100755
--- a/bin/get-books
+++ b/bin/get-books
@@ -9,7 +9,7 @@ else
exit 1
fi
-echo "const books = ["
+echo "const bookLibrary = ["
for METADATA in $(find "$BOOK_DIR" -name 'metadata.json'); do
DIR=$(dirname "$METADATA")
diff --git a/bin/migrate/1-read-status b/bin/migrate/1-read-status
new file mode 100755
index 0000000..5f4b61d
--- /dev/null
+++ b/bin/migrate/1-read-status
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [ "$#" == 1 ]; then
+ BOOK_DIR="$1"
+else
+ echo "usage: $0 path-to-book-directory"
+ exit 1
+fi
+
+for FILE in $(find "$BOOK_DIR" -name 'metadata.json'); do
+ METADATA=$(cat "$FILE")
+ READ=$(echo "$METADATA" | jq .read)
+ if [ "$READ" == "true" ]; then
+ echo "$METADATA" | jq '.read = "Read"' > "$FILE"
+ else
+ echo "$METADATA" | jq '.read = "Unread"' > "$FILE"
+ fi
+done
diff --git a/public/main.css b/public/main.css
index 7e7f297..1beec21 100644
--- a/public/main.css
+++ b/public/main.css
@@ -1,19 +1,75 @@
+html {
+ height: 100%;
+}
+
body {
margin: 0;
display: flex;
+ height: 100%;
+ font-family: sans-serif;
}
-.g-Aside {
- padding: 1rem;
+/* Filters */
+
+aside {
background-color: #333;
+ color: white;
+ width: 200px;
+ overflow-y: auto;
+}
+
+ul {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.g-FilterTitle {
+ padding: 0.5rem 1rem;
+ background-color: #555;
+ border-left: 8px solid transparent;
+ font-weight: bold;
+}
+
+.g-Filter--Unselected {
+ border-left: 8px solid #555;
+}
+
+.g-Filter--Selected {
+ border-left: 8px solid #883333;
}
-.g-Main {
+.g-Filter button {
+ border: none;
+ background-color: transparent;
+ color: inherit;
+ cursor: pointer;
+ padding: 0.5rem 1rem;
+ width: 100%;
+ text-align: left;
+}
+
+.g-Filter button:hover {
+ background-color: #888833;
+}
+
+/* Books */
+
+main {
+ width: 100%;
padding: 1rem;
+ overflow-y: auto;
+}
+
+header {
+ font-size: 120%;
+ margin-bottom: 1rem;
+}
+
+.g-Books {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
grid-gap: 1rem;
-
}
.g-Book {
@@ -21,8 +77,3 @@ body {
align-self: center;
border: 1px solid #DDDDDD;
}
-
-.g-Book:hover {
- transform: scale(1.1);
- transition: transform 0.1s linear;
-}
diff --git a/src/main.ts b/src/main.ts
index 7033690..0a3fd30 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,6 @@
-import { h, withVar, mount } from 'lib/rx'
+import { h, withVar, mount, Rx } from 'lib/rx'
+
+// Model
interface Book {
title: string
@@ -6,43 +8,121 @@ interface Book {
authorsSort: string
genres: Array<string>
date: string
- read: boolean
+ read: ReadStatus,
cover: string
}
+type ReadStatus = 'Read' | 'Unread' | 'Reading' | 'Stopped'
+
+const readStatuses: Array<ReadStatus> = ['Read', 'Unread', 'Reading', 'Stopped' ]
+
+// Books
+
// @ts-ignore
-const sortedBooks: Array<Book> = (books as Array<Book>).sort((a, b) =>
+const sortedBookLibrary: Array<Book> = (bookLibrary as Array<Book>).sort((a, b) =>
a.authorsSort == b.authorsSort
? a.date > b.date
: a.authorsSort > b.authorsSort)
-enum Filter {
- All,
- Read,
- Unread
+// Filters
+
+interface Filters {
+ read?: ReadStatus
}
-const view = withVar(Filter.All, (filter, updateFilter) => [
- h('aside',
- { className: 'g-Aside' },
- h('select',
- { name: 'filter',
- id: 'filter',
- onchange: (event: Event) => updateFilter(_ => (event.target as HTMLSelectElement).value as any)
- },
- h('option', { value: Filter.All }, 'Tous les livres'),
- h('option', { value: Filter.Read }, 'Livres lus'),
- h('option', { value: Filter.Unread }, 'Livres non lus')
+// View
+
+const view = withVar<Filters>({}, (filters, updateFilters) => {
+ const filtredBooks = filters.map(f =>
+ sortedBookLibrary.filter(book =>
+ f.read === undefined || book.read === f.read
)
- ),
- h('main',
- { className: 'g-Main' },
- filter.map(f =>
- sortedBooks
- .filter(b => f == Filter.All || b.read == (f == Filter.Read))
- .map(book => h('img', { className: 'g-Book', src: book.cover }))
+ )
+
+ return [
+ h('aside', viewFilters({ filtredBooks, filters, updateFilters })),
+ h('main',
+ h('header', filtredBooks.map(fb => `${fb.length} livres`)),
+ h('div',
+ { className: 'g-Books' },
+ filtredBooks.map(fb =>
+ fb.map(book => h('img', { className: 'g-Book', src: book.cover }))
+ )
+ )
)
+ ]
+})
+
+// Filters view
+
+interface ViewFiltersParams {
+ filtredBooks: Rx<Array<Book>>
+ filters: Rx<Filters>
+ updateFilters: (f: (filters: Filters) => Filters) => void
+}
+
+function viewFilters({ filtredBooks, filters, updateFilters }: ViewFiltersParams) {
+ return h('ul',
+ {},
+ h('li', [
+ h('div', { className: 'g-FilterTitle' }, 'Lecture'),
+ readFilter({
+ filtredBooks,
+ readStatus: filters.map(fs => fs.read),
+ update: (status?: ReadStatus) => updateFilters(fs => {
+ fs.read = status
+ return fs
+ })
+ })
+ ])
+ )
+}
+
+interface ReadFilterParams {
+ filtredBooks: Rx<Array<Book>>
+ readStatus: Rx<ReadStatus | undefined>
+ update: (status?: ReadStatus) => void
+}
+
+function readFilter({ filtredBooks, readStatus, update }: ReadFilterParams) {
+ return h('ul',
+ { className: 'g-Filters' },
+ readStatus.map(currentStatus => {
+ if (currentStatus !== undefined) {
+ return h('li',
+ { className: 'g-Filter g-Filter--Selected' },
+ h('button',
+ { onclick: () => update(undefined) },
+ readStatusLabel(currentStatus)
+ )
+ )
+ } else {
+ return readStatuses.map(status => {
+ const count = filtredBooks.map(xs => xs.filter(b => b.read === status).length)
+
+ return h('li',
+ { className: 'g-Filter g-Filter--Unselected' },
+ h('button',
+ { onclick: () => update(status) },
+ count.map(c => `${readStatusLabel(status)} (${c})`)
+ )
+ )
+ })
+ }
+ })
)
-])
+}
+
+function readStatusLabel(status: ReadStatus): string {
+ if (status === 'Read') {
+ return 'lus'
+ } else if (status === 'Unread') {
+ return 'non lus'
+ } else if (status === 'Reading') {
+ return 'en cours'
+ } else {
+ return 'arrêtés'
+ }
+}
mount(view)