diff options
-rwxr-xr-x | bin/get-books | 2 | ||||
-rwxr-xr-x | bin/migrate/1-read-status | 19 | ||||
-rw-r--r-- | public/main.css | 69 | ||||
-rw-r--r-- | src/main.ts | 132 |
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) |