144 lines
7.0 KiB
Markdown
144 lines
7.0 KiB
Markdown
# Website
|
|
|
|
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
|
|
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 |
|
|
|---|---|
|
|
| `NUXT_BASEROW_URL` | Baserow instance, no trailing slash |
|
|
| `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 `layers/diary/app/app.config.ts`, `diary.showCovers: true` shows a generated title tile next to each review.
|
|
|
|
## What it does
|
|
|
|
- `/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. 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.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
|
|
PasswordModal.vue
|
|
CoverTile.vue generated cover, used when showCovers is on
|
|
composables/
|
|
useDiary.ts loads reviews + notes, searches, sorts and groups them by year
|
|
useSaveEntry.ts shared submit logic of the forms
|
|
useLog.ts open/close state of the Log modal
|
|
utils/ dates, icons, search highlighting, error messages
|
|
types/entry.ts a timeline entry (review or note)
|
|
server/
|
|
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/
|
|
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/
|
|
types/ Review, Note
|
|
utils/medium.ts the review media: label and creator label
|
|
utils/schemas.ts zod schemas used by both forms and API
|
|
```
|
|
|
|
## Baserow tables
|
|
|
|
**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).
|
|
|
|
Rows without a date are skipped. Text is plain text: line breaks are kept and it is never rendered as HTML.
|
|
|
|
### 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 `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`.
|