2026-09-30 19:15:57 +02:00
2026-09-30 19:15:57 +02:00
2026-09-30 19:15:57 +02:00
2026-09-30 19:15:57 +02:00
2026-09-30 19:15:57 +02:00
2026-09-23 11:44:36 +02:00
2026-09-30 19:15:57 +02:00
2026-09-30 19:15:57 +02:00
2026-09-23 11:44:36 +02:00
2026-09-23 11:44:36 +02:00
2026-09-30 19:15:57 +02:00
2026-09-23 11:44:36 +02:00

Website

One Nuxt 4 + Nuxt UI deployment made of self-contained apps. Each app is a Nuxt layer 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 — layers/diary/ (password protected)
/wishlist Wishlist — layers/wishlist/
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)
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.
S
Description
No description provided
Readme
293 KiB
Languages
TypeScript 53.9%
Vue 45.9%
CSS 0.2%