# 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//` 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//` with `app/pages/.vue` (or `app/pages//index.vue`), and whatever else it needs: `app/components`, `app/composables`, `server/api//…`, `shared/`, and a `nuxt.config.ts` for its runtime config. 2. Keep it contained: serve its API under `/api//`, 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/.json`) and posts the cover URL to `/api/wishlist/books//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/-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`.