feat: add notes app

This commit is contained in:
2026-10-01 19:39:20 +02:00
parent ccb8dd3d15
commit ddbb6f37b5
35 changed files with 706 additions and 52 deletions
+55 -14
View File
@@ -2,14 +2,23 @@
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.
picked up automatically. The root only holds what apps share:
- the shell (`app/app.vue`, global CSS, theme) and an empty `/`
- the Baserow client `server/utils/baserow.ts` (`listRows`, `getRow`, `createRow`, `updateRow`, `deleteRow`,
`listFields`, each taking a table ID). `NUXT_BASEROW_URL` and `NUXT_BASEROW_TOKEN` are shared; each app reads its
own table IDs.
- the login. One password (`NUXT_DIARY_PASSWORD`) unlocks every protected app: `server/middleware/auth.ts` returns
401 for the API prefixes in `PROTECTED_PREFIXES` until `/api/auth/login` has set the session cookie
(signed with `NUXT_SESSION_SECRET`, http-only, 30 days), and a page wraps itself in `<PasswordGate>` to show the
password prompt. To protect a new app, add its API prefix and use the gate.
- `readValidBody(event, zodSchema)` and the client util `errorMessage(error)`
| Path | App |
|---|---|
| `/diary` | [Diary](#diary) — `layers/diary/` (password protected) |
| `/wishlist` | [Wishlist](#wishlist) — `layers/wishlist/` |
| `/notes` | [Notes](#notes) — `layers/notes/` (password protected) |
```bash
pnpm install
@@ -26,6 +35,42 @@ pnpm dev
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.
# Notes
Minimal notes: a collapsible list of notes on the left, a WYSIWYG Markdown editor (Nuxt UI `UEditor`) on the right.
No folders. A note is named by its title, or by when it was created if the title is empty. Changes save themselves
0.8 s after you stop typing, and when you leave the note or close the tab. Select text for the formatting toolbar;
Markdown shortcuts (`# `, `- `, `**bold**`, …) also work.
Search at the top of the list matches title and text (`/api/notes/docs?q=`, case-insensitive, parts of words too).
It uses Baserow's search; Baserow only searches the fields a request includes, so the search includes `content`.
Shortcuts (Ctrl instead of ⌘ outside macOS): **⌘ ⌥ N** new note, **⌘ E** show/hide the note list (so the editor's
⌘ E for inline code is turned off; use backticks or the toolbar). Plain ⌘ N can't be used, the browser keeps it.
**Table** (`NUXT_BASEROW_NOTES_APP_TABLE_ID`): `title` (single line text), `content` (long text without rich text; holds the
Markdown), `created` and `updated` (dates with time, set by the
server when a note is created or saved). The list is sorted by `created`, newest first. Rows added in Baserow directly have no dates and show as
"Untitled" at the bottom. The token needs read, create, update and delete.
```
layers/notes/
app/
pages/notes.vue password gate, sidebar + the open note
pages/notes/index.vue nothing selected
pages/notes/[id].vue title, editor, delete
components/NotesList.vue the note list (open, delete)
components/NotesSidebar.vue desktop: the list on the left (open/closed is remembered in a cookie)
components/NotesDrawer.vue mobile: the list, pulled down from the top
components/NotesActionBar.vue "New", bottom right
components/NotesSidebarHandle.vue desktop: chevron on the sidebar edge, on hover, shows/hides the list
composables/
useNotes.ts the list, create/delete, note names
useNotesAutosave.ts debounced saving
server/api/notes/docs/ GET list, POST create, GET/PATCH/DELETE [id]
shared/ NotesDoc type, update schema
```
# Wishlist
A public shelf of the books in the Baserow wishlist table (`NUXT_BASEROW_WISHLIST_TABLE_ID`): cover, German title
@@ -77,7 +122,8 @@ self-hosted Baserow 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_BASEROW_NOTES_APP_TABLE_ID` | Table of the notes app (see [Notes](#notes)), not the diary's notes |
| `NUXT_DIARY_PASSWORD` | Password that unlocks the diary and notes |
| `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.
@@ -90,9 +136,8 @@ In `layers/diary/app/app.config.ts`, `diary.showCovers: true` shows a generated
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.
- Until the password is entered, only a password prompt is shown and every `/api/diary/*` route returns 401
(the shared login, see the top of this file). Changing `NUXT_SESSION_SECRET` logs out every browser.
## Code layout
@@ -107,22 +152,18 @@ app/
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
utils/ dates, icons, search highlighting
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)
api/diary/ 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
@@ -137,7 +178,7 @@ shared/
**Books:** `title`, `title_german`, `authors` (link) and `read_date` (date).
**Notes:** `Datum` (date) and `Notes` (long text).
**Notes** (the diary's, now named `diary` in Baserow): `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.