Reading Log
index.jsx 274 lines
Files · 8
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274import "./style.css";
import React from "react";
import { Button } from "../../shared/ui.jsx";
import { createRoot } from "react-dom/client";
import { RecordApp, RecordActions } from "../../shared/records.jsx";
import { displayDate, readingDate } from "../../shared/format.js";
import { store } from "./store.js";
const shelves = {
"to-read": "Want to read",
"currently-reading": "Currently reading",
read: "Read",
};
const colors = ["#726088", "#8d654a", "#4b7773", "#536b92", "#9a7040"];
const config = {
slug: "reading",
singular: "book",
plural: "books",
navigationLabel: "Bookshelves",
subtitle: "Books, shelves, and reviews.",
listLabel: "Library",
searchPlaceholder: "Search title, author, or shelf…",
emptyTitle: "No books yet",
emptyDescription: "Add a book of your own, or try a small sample library.",
title: (row) => row.title,
search: (row) =>
`${row.title} ${row.author} ${row.bookshelves} ${row.my_review}`,
sort: (a, b) =>
b.date_added.localeCompare(a.date_added) || b.book_id - a.book_id,
filters: (rows) => [
{ id: "all", label: "All books", count: rows.length },
...Object.entries(shelves).map(([id, label]) => ({
id,
label,
count: rows.filter((row) => row.exclusive_shelf === id).length,
})),
],
matches: (row, filter) => row.exclusive_shelf === filter,
stats: (rows) => [
{
label: "Currently reading",
value: rows.filter((row) => row.exclusive_shelf === "currently-reading")
.length,
},
{
label: "Books read",
value: rows.filter((row) => row.exclusive_shelf === "read").length,
},
{
label: "Want to read",
value: rows.filter((row) => row.exclusive_shelf === "to-read").length,
},
],
fields: [
{ key: "title", label: "Title", required: true, wide: true },
{ key: "author", label: "Author", required: true },
{
key: "book_id",
label: "Goodreads book ID",
required: true,
numeric: true,
help: "The edition identifier from Goodreads.",
},
{
key: "exclusive_shelf",
label: "Shelf",
options: Object.entries(shelves).map(([value, label]) => ({
value,
label,
})),
default: () => "to-read",
},
{
key: "my_rating",
label: "Your rating",
numeric: true,
options: [0, 1, 2, 3, 4, 5].map((value) => ({
value,
label: value ? `${value} ${value === 1 ? "star" : "stars"}` : "Unrated",
})),
default: () => 0,
},
{
key: "date_read",
label: "Date finished",
placeholder: "YYYY/MM/DD",
help: "Filled with today when you move a book to Read.",
},
{
key: "date_added",
label: "Date added",
placeholder: "YYYY/MM/DD",
default: readingDate,
},
{ key: "my_review", label: "Your review", multiline: true, wide: true },
{
key: "bookshelves",
label: "Additional shelves",
placeholder: "fiction, favorites",
advanced: true,
wide: true,
help: "Comma-separated shelf names.",
},
{
key: "isbn",
label: "ISBN",
advanced: true,
placeholder: "=“0140328726”",
},
{ key: "isbn13", label: "ISBN13", advanced: true },
{
key: "average_rating",
label: "Average reader rating",
numeric: true,
default: () => 0,
advanced: true,
},
{ key: "number_of_pages", label: "Pages", numeric: true, advanced: true },
{
key: "read_count",
label: "Read count",
numeric: true,
default: () => 0,
advanced: true,
},
],
prepare: (data, previous) => {
if (
!previous &&
store.getSnapshot().rows.some((book) => book.book_id === data.book_id)
) {
throw new Error(
"A book with this Goodreads ID is already in your library.",
);
}
if (
data.exclusive_shelf === "read" &&
previous?.exclusive_shelf !== "read"
) {
if (!data.date_read) data.date_read = readingDate();
if (data.read_count === (previous?.read_count ?? 0)) data.read_count++;
}
return data;
},
renderCard: (row, actions) => (
<article className="ui-panel ui-record-card ra-book-card">
<div
className="ra-cover"
style={{
"--cover-color": colors[Math.abs(row.book_id) % colors.length],
}}
aria-hidden="true"
>
<span className="ra-cover-title">{row.title}</span>
<small>{row.author}</small>
</div>
<div className="ra-book-body">
<div className="ra-card-top">
<div>
<h3 className="ra-card-title">{row.title}</h3>
<p className="ra-book-author">{row.author}</p>
</div>
<RecordActions row={row} actions={actions} title={row.title} />
</div>
<span
className={`ui-badge ${row.exclusive_shelf === "currently-reading" ? "ui-badge-accent" : row.exclusive_shelf === "read" ? "ui-badge-success" : ""}`}
>
{shelves[row.exclusive_shelf]}
</span>
<div
className="ra-stars"
aria-label={
row.my_rating ? `${row.my_rating} out of 5 stars` : "Unrated"
}
>
<span aria-hidden="true">
{"★".repeat(row.my_rating)}
{"☆".repeat(5 - row.my_rating)}
</span>
<small>
{row.number_of_pages !== null
? `${row.number_of_pages} pages`
: "Pages unknown"}
</small>
</div>
<div className="ra-book-bottom">
{row.exclusive_shelf === "read" && row.date_read
? `Finished ${displayDate(row.date_read)}`
: row.bookshelves ||
(row.date_added
? `Added ${displayDate(row.date_added)}`
: "No date added")}
{row.read_count > 1 ? ` · Read ${row.read_count} times` : ""}
</div>
{row.my_review && <p className="ra-feed-preview">{row.my_review}</p>}
{row.exclusive_shelf === "to-read" && (
<Button
variant="ghost"
className="ra-inline-action"
disabled={actions.busy}
onClick={() =>
actions.act(row, { exclusive_shelf: "currently-reading" })
}
>
Start reading →
</Button>
)}
{row.exclusive_shelf === "currently-reading" && (
<Button
variant="ghost"
className="ra-inline-action"
disabled={actions.busy}
onClick={() =>
actions.act(row, {
exclusive_shelf: "read",
date_read: readingDate(),
read_count: row.read_count + 1,
})
}
>
Mark as finished →
</Button>
)}
</div>
</article>
),
samples: [
{
book_id: 18423,
title: "The Left Hand of Darkness",
author: "Ursula K. Le Guin",
exclusive_shelf: "currently-reading",
my_rating: 0,
number_of_pages: 304,
date_added: "2026/10/12",
bookshelves: "science-fiction",
isbn: '="0441478123"',
isbn13: '="9780441478125"',
},
{
book_id: 3869,
title: "A Brief History of Time",
author: "Stephen Hawking",
exclusive_shelf: "to-read",
number_of_pages: 212,
date_added: "2026/10/10",
bookshelves: "science",
},
{
book_id: 18144590,
title: "The Ocean at the End of the Lane",
author: "Neil Gaiman",
exclusive_shelf: "read",
my_rating: 4,
number_of_pages: 181,
date_added: "2026/09/28",
date_read: "2026/10/09",
read_count: 1,
my_review: "A short, strange story about memory and childhood.",
},
{
book_id: 44767458,
title: "Dune",
author: "Frank Herbert",
exclusive_shelf: "to-read",
number_of_pages: 688,
date_added: "2026/10/07",
bookshelves: "science-fiction",
},
],
};
createRoot(document.getElementById("root")).render(
<RecordApp store={store} config={config} />,
);