feat: add wishlist
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
# Diary
|
||||
# Website
|
||||
|
||||
A password-protected diary of reviews (books, movies, albums) and notes, built with Nuxt 4 + Nuxt UI on top of two
|
||||
self-hosted Baserow tables.
|
||||
One Nuxt 4 + Nuxt UI deployment made of self-contained apps. Each app is a [Nuxt layer](https://nuxt.com/docs/guide/going-further/layers)
|
||||
in `layers/<name>/` with its own pages, components, composables, server routes and config; layers in that folder are
|
||||
picked up automatically. The root only holds what apps share: the shell (`app/app.vue`, global CSS, theme), an empty `/`, and the Baserow
|
||||
client `server/utils/baserow.ts` (`listRows`, `createRow`, `updateRow`, `listFields`, each taking a table ID).
|
||||
`NUXT_BASEROW_URL` and `NUXT_BASEROW_TOKEN` are shared; each app reads its own table IDs.
|
||||
|
||||
| Path | App |
|
||||
|---|---|
|
||||
| `/diary` | [Diary](#diary) — `layers/diary/` (password protected) |
|
||||
| `/wishlist` | [Wishlist](#wishlist) — `layers/wishlist/` |
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
@@ -9,6 +17,50 @@ cp .env.example .env
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Adding an app
|
||||
|
||||
1. Create `layers/<name>/` with `app/pages/<name>.vue` (or `app/pages/<name>/index.vue`), and whatever else it needs:
|
||||
`app/components`, `app/composables`, `server/api/<name>/…`, `shared/`, and a `nuxt.config.ts` for its runtime config.
|
||||
2. Keep it contained: serve its API under `/api/<name>/`, and prefix `useState` / `useFetch` keys and app config
|
||||
with the app name. All layers share one auto-import namespace, so give components and exported utils
|
||||
names that won't clash with another app's (e.g. `DiaryEntry`, not `Entry`).
|
||||
3. Don't use `~/` inside a layer — it points to the root `app/`. Use relative imports.
|
||||
|
||||
# Wishlist
|
||||
|
||||
A public shelf of the books in the Baserow wishlist table (`NUXT_BASEROW_WISHLIST_TABLE_ID`): cover, German title
|
||||
and author per card. A card opens its Thalia link. Search matches title, author, ISBN and Thalia link;
|
||||
sort by author (last name) or title.
|
||||
|
||||
**Table:** `isbn` (text), `thalia` (URL), `cover` (URL), `books` (link to the books table) and the lookups
|
||||
`title` (German title) and `authors`.
|
||||
|
||||
**Covers:** `cover` is used if set. Otherwise the browser looks the ISBN up on Open Library
|
||||
(`openlibrary.org/isbn/<isbn>.json`) and posts the cover URL to `/api/wishlist/books/<id>/cover`, which saves it to the
|
||||
row, so each book is looked up only until it has a cover. That route is public but only accepts
|
||||
`https://covers.openlibrary.org/b/id/<n>-M.jpg` and never overwrites a cover. Books Open Library has no cover for
|
||||
get a generated tile; paste any image URL into `cover` to give them one.
|
||||
|
||||
```
|
||||
layers/wishlist/
|
||||
nuxt.config.ts baserowWishlistTableId
|
||||
app/
|
||||
pages/wishlist.vue search, sort, grid
|
||||
components/WishlistCard.vue cover + title + author, links to Thalia
|
||||
composables/
|
||||
useWishlist.ts loads, searches and sorts the books
|
||||
useWishlistCover.ts cover from the table or Open Library, saves it back
|
||||
server/
|
||||
api/wishlist/books/ GET list, POST [id]/cover
|
||||
utils/wishlist.ts Baserow row -> WishlistBook
|
||||
shared/types/wishlist.ts
|
||||
```
|
||||
|
||||
# Diary
|
||||
|
||||
A password-protected diary of reviews (books, movies, albums) and notes, built with Nuxt 4 + Nuxt UI on top of two
|
||||
self-hosted Baserow tables.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Purpose |
|
||||
@@ -17,29 +69,34 @@ pnpm dev
|
||||
| `NUXT_BASEROW_TOKEN` | Database token with read and create access to both tables |
|
||||
| `NUXT_BASEROW_REVIEWS_TABLE_ID` | Reviews table |
|
||||
| `NUXT_BASEROW_NOTES_TABLE_ID` | Notes table |
|
||||
| `NUXT_BASEROW_WISHLIST_TABLE_ID` | Wishlist table (see [Wishlist](#wishlist)) |
|
||||
| `NUXT_DIARY_PASSWORD` | Password that unlocks the diary |
|
||||
| `NUXT_SESSION_SECRET` | Signs the login cookie, at least 32 characters (`openssl rand -hex 32`) |
|
||||
|
||||
In `app/app.config.ts`, `showCovers: true` shows a generated title tile next to each review.
|
||||
In `layers/diary/app/app.config.ts`, `diary.showCovers: true` shows a generated title tile next to each review.
|
||||
|
||||
## What it does
|
||||
|
||||
- `/` is the only page: reviews and notes on one timeline, newest first, grouped by year.
|
||||
- `/diary`: reviews and notes on one timeline, newest first, grouped by year.
|
||||
- Only entries with text are shown. Long text is collapsed behind "Read more".
|
||||
- Search matches every word against the kind (`review`, `note`), the medium (`book`, `movie`, `album`),
|
||||
the date (`aug 2025`, `2025-08`), the title, the author and the text. Matches in the text are highlighted.
|
||||
- "Log" opens a form with a Note / Review switch.
|
||||
- Until the password is entered, only a password prompt is shown and every API route except
|
||||
`/api/session` and `/api/login` returns 401. A correct password sets a signed, http-only cookie valid for 30 days.
|
||||
- "Log" opens a form with a Note / Review switch. A review is written for a book picked from the books table;
|
||||
its "Read on" date is saved as that book's `read_date`.
|
||||
- Until the password is entered, only a password prompt is shown and every `/api/diary/*` route except
|
||||
`/api/diary/session` and `/api/diary/login` returns 401. A correct password sets a signed, http-only cookie valid for 30 days.
|
||||
Changing `NUXT_SESSION_SECRET` logs out every browser.
|
||||
|
||||
## Code layout
|
||||
|
||||
```
|
||||
layers/diary/
|
||||
nuxt.config.ts runtime config (env vars above)
|
||||
app/
|
||||
app.vue password gate around the page
|
||||
pages/index.vue the diary page
|
||||
app.config.ts diary.showCovers
|
||||
pages/diary.vue password gate around the timeline
|
||||
components/
|
||||
DiaryTimeline.vue the diary: search, Log button, entries grouped by year
|
||||
DiaryEntry.vue one timeline entry (date, title, text, read more)
|
||||
LogModal.vue Note / Review switch around the two forms
|
||||
NoteForm.vue, ReviewForm.vue
|
||||
@@ -52,11 +109,11 @@ app/
|
||||
utils/ dates, icons, search highlighting, error messages
|
||||
types/entry.ts a timeline entry (review or note)
|
||||
server/
|
||||
middleware/auth.ts locks /api/* until unlocked
|
||||
api/ session, login, reviews and notes (GET list, POST create)
|
||||
middleware/diary-auth.ts locks /api/diary/* until unlocked
|
||||
api/diary/ session, login, reviews and notes (GET list, POST create), books (for the review form)
|
||||
utils/
|
||||
baserow.ts Baserow client: listRows, createRow, listFields
|
||||
review.ts, note.ts Baserow row -> app type
|
||||
diaryTable.ts the diary's table IDs (the Baserow client itself is in the root server/utils)
|
||||
review.ts, note.ts, book.ts Baserow row -> app type
|
||||
auth.ts session cookie and password check
|
||||
validation.ts readValidBody(event, zodSchema)
|
||||
shared/
|
||||
@@ -67,8 +124,11 @@ shared/
|
||||
|
||||
## Baserow tables
|
||||
|
||||
**Reviews:** `title`, `date` (ISO date), `author_or_director`, `review` (long text) and `medium`
|
||||
(single select: `Book` / `Movie` / `Album`).
|
||||
**Reviews:** `books` (link to the books table), `review` (long text), `medium` (single select: `Book` / `Movie` /
|
||||
`Album`) and the lookups through `books`: `title` (`title_german`), `author_or_director` (`authors`) and
|
||||
`date` (`read_date`). The books table is found through the `books` link, so it needs no table ID.
|
||||
|
||||
**Books:** `title`, `title_german`, `authors` (link) and `read_date` (date).
|
||||
|
||||
**Notes:** `Datum` (date) and `Notes` (long text).
|
||||
|
||||
@@ -76,6 +136,8 @@ Rows without a date are skipped. Text is plain text: line breaks are kept and it
|
||||
|
||||
### Adding a medium
|
||||
|
||||
Reviews currently link only to books, so the form always saves `Book`. Other media need their own link field.
|
||||
|
||||
1. Add the option to the `medium` single select in Baserow.
|
||||
2. Add it to `Medium` in `shared/types/review.ts`, to `MEDIUMS` in `shared/utils/medium.ts` (its `label` must match
|
||||
the Baserow option) and to `MEDIUM_ICONS` in `app/utils/icons.ts`.
|
||||
2. Add it to `Medium` in `layers/diary/shared/types/review.ts`, to `MEDIUMS` in `layers/diary/shared/utils/medium.ts`
|
||||
(its `label` must match the Baserow option) and to `MEDIUM_ICONS` in `layers/diary/app/utils/icons.ts`.
|
||||
|
||||
Reference in New Issue
Block a user