From ddbb6f37b59796a47da1b24b002a0aa452fd43c9 Mon Sep 17 00:00:00 2001 From: huaqo Date: Thu, 1 Oct 2026 19:39:20 +0200 Subject: [PATCH] feat: add notes app --- .env.example | 1 + README.md | 69 +++++++-- app/components/PasswordGate.vue | 11 ++ .../app => app}/components/PasswordModal.vue | 7 +- {layers/diary/app => app}/utils/errors.ts | 0 layers/diary/app/pages/diary.vue | 9 +- layers/diary/nuxt.config.ts | 4 +- layers/diary/server/middleware/diary-auth.ts | 11 -- layers/diary/shared/utils/schemas.ts | 4 - .../notes/app/components/NotesActionBar.vue | 20 +++ layers/notes/app/components/NotesDrawer.vue | 42 ++++++ layers/notes/app/components/NotesList.vue | 59 ++++++++ layers/notes/app/components/NotesSidebar.vue | 11 ++ .../app/components/NotesSidebarHandle.vue | 25 ++++ layers/notes/app/composables/useNotes.ts | 137 ++++++++++++++++++ .../notes/app/composables/useNotesAutosave.ts | 51 +++++++ layers/notes/app/pages/notes.vue | 29 ++++ layers/notes/app/pages/notes/[id].vue | 112 ++++++++++++++ layers/notes/app/pages/notes/index.vue | 14 ++ layers/notes/nuxt.config.ts | 5 + .../server/api/notes/docs/[id].delete.ts | 4 + .../notes/server/api/notes/docs/[id].get.ts | 3 + .../notes/server/api/notes/docs/[id].patch.ts | 5 + .../notes/server/api/notes/docs/index.get.ts | 11 ++ .../notes/server/api/notes/docs/index.post.ts | 4 + layers/notes/server/utils/notes.ts | 40 +++++ layers/notes/shared/types/notes.ts | 12 ++ layers/notes/shared/utils/notesSchemas.ts | 8 + nuxt.config.ts | 5 +- .../diary => server/api/auth}/login.post.ts | 8 +- .../diary => server/api/auth}/session.get.ts | 2 +- server/middleware/auth.ts | 11 ++ {layers/diary/server => server}/utils/auth.ts | 8 +- server/utils/baserow.ts | 16 +- .../server => server}/utils/validation.ts | 0 35 files changed, 706 insertions(+), 52 deletions(-) create mode 100644 app/components/PasswordGate.vue rename {layers/diary/app => app}/components/PasswordModal.vue (73%) rename {layers/diary/app => app}/utils/errors.ts (100%) delete mode 100644 layers/diary/server/middleware/diary-auth.ts create mode 100644 layers/notes/app/components/NotesActionBar.vue create mode 100644 layers/notes/app/components/NotesDrawer.vue create mode 100644 layers/notes/app/components/NotesList.vue create mode 100644 layers/notes/app/components/NotesSidebar.vue create mode 100644 layers/notes/app/components/NotesSidebarHandle.vue create mode 100644 layers/notes/app/composables/useNotes.ts create mode 100644 layers/notes/app/composables/useNotesAutosave.ts create mode 100644 layers/notes/app/pages/notes.vue create mode 100644 layers/notes/app/pages/notes/[id].vue create mode 100644 layers/notes/app/pages/notes/index.vue create mode 100644 layers/notes/nuxt.config.ts create mode 100644 layers/notes/server/api/notes/docs/[id].delete.ts create mode 100644 layers/notes/server/api/notes/docs/[id].get.ts create mode 100644 layers/notes/server/api/notes/docs/[id].patch.ts create mode 100644 layers/notes/server/api/notes/docs/index.get.ts create mode 100644 layers/notes/server/api/notes/docs/index.post.ts create mode 100644 layers/notes/server/utils/notes.ts create mode 100644 layers/notes/shared/types/notes.ts create mode 100644 layers/notes/shared/utils/notesSchemas.ts rename {layers/diary/server/api/diary => server/api/auth}/login.post.ts (76%) rename {layers/diary/server/api/diary => server/api/auth}/session.get.ts (70%) create mode 100644 server/middleware/auth.ts rename {layers/diary/server => server}/utils/auth.ts (78%) rename {layers/diary/server => server}/utils/validation.ts (100%) diff --git a/.env.example b/.env.example index 331c23e..e73f9ea 100644 --- a/.env.example +++ b/.env.example @@ -3,5 +3,6 @@ NUXT_BASEROW_TOKEN= NUXT_BASEROW_REVIEWS_TABLE_ID=866 NUXT_BASEROW_NOTES_TABLE_ID=864 NUXT_BASEROW_WISHLIST_TABLE_ID=870 +NUXT_BASEROW_NOTES_APP_TABLE_ID= NUXT_DIARY_PASSWORD= NUXT_SESSION_SECRET= diff --git a/README.md b/README.md index 9503d2c..cd49fb2 100644 --- a/README.md +++ b/README.md @@ -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//` 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 `` 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. diff --git a/app/components/PasswordGate.vue b/app/components/PasswordGate.vue new file mode 100644 index 0000000..d6cd426 --- /dev/null +++ b/app/components/PasswordGate.vue @@ -0,0 +1,11 @@ + + + diff --git a/layers/diary/app/components/PasswordModal.vue b/app/components/PasswordModal.vue similarity index 73% rename from layers/diary/app/components/PasswordModal.vue rename to app/components/PasswordModal.vue index 51c75b6..8395ec3 100644 --- a/layers/diary/app/components/PasswordModal.vue +++ b/app/components/PasswordModal.vue @@ -1,4 +1,5 @@