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} />,
);