diff --git a/README.md b/README.md index 3b897c6..2a27b25 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,14 @@ 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: +picked up automatically. The UI is German (`lang="de"` and Nuxt UI's German locale in `app/app.vue`; only the CV +has an English version too), and so are the error messages of the API. Every app has a house button back to the hub +and a light / dark switch in its header, both from `` (one size and style everywhere) (Notizen: in the title row); the +choice of light or dark applies to the whole site. The root only holds what apps share: -- the shell (`app/app.vue`, global CSS, theme) and an empty `/` +- the shell (`app/app.vue`, global CSS, theme) and the hub on `/`: a tile per app (icon, title, a lock + for the protected ones), sorted by title. Each app registers its tile in its own `app/app.config.ts` under + `hub.` (`HubApp` in `shared/types/hub.ts`). - 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. @@ -13,7 +18,7 @@ picked up automatically. The root only holds what apps share: (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)`, the client util `errorMessage(error)` and `usePdfExport(fileName)` ("Save as PDF" - through the browser's print dialog: names the PDF and prints light also in dark mode; used by CV and Packer, with the `` under the content) + through the browser's print dialog: names the PDF and prints light also in dark mode; used by CV and Packliste, with the `` under the content) | Path | App | |---|---| @@ -21,7 +26,7 @@ picked up automatically. The root only holds what apps share: | `/wishlist` | [Wishlist](#wishlist) — `layers/wishlist/` | | `/notes` | [Notes](#notes) — `layers/notes/` (password protected) | | `/cv` | [CV](#cv) — `layers/cv/` | -| `/packer` | [Packer](#packer) — `layers/packer/` | +| `/packer` | [Packliste](#packliste) — `layers/packer/` | ```bash pnpm install @@ -36,9 +41,13 @@ pnpm dev 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. +3. Add its tile to the hub: `hub: { : { title, icon, to: '/' } }` in its + `app/app.config.ts` (`locked: true` if it is behind the password). +4. Don't use `~/` inside a layer — it points to the root `app/`. Use relative imports. -# Packer +# Packliste + +The app is named Packliste everywhere; only its URL, folder and code keep `packer`. A public packing list built from the Baserow packing table (`NUXT_BASEROW_PACKER_TABLE_ID`). At the top you set the trip: number of days and people, where you sleep, weather, season and activities. The list below is grouped by category in table order, items sorted alphabetically. @@ -77,7 +86,7 @@ The settings are kept in the URL, so a list can be bookmarked or sent. tab's `sessionStorage`, survives reloads and is gone when the tab is closed. - **Als PDF speichern** (under the list, as on the CV) prints the list as a checklist (with the browser's "Save as PDF"): the trip in one line under the title (days, people, season, stay, activities, total weight), the categories in two columns, ticked items ticked. The - settings and the buttons are left out; the file is named `packer__tage`. + settings and the buttons are left out; the file is named `packliste__tage`. - **Zurücksetzen** (shown once anything differs from a fresh list) clears all of that and puts the settings back to their defaults (weights changed on the page go back to the table's too): 1 day, 1 person, today's season, Übernachtung and the automatic activities. @@ -113,13 +122,17 @@ The header (name, description per language, photo, email, LinkedIn link) is set under `cv`. LinkedIn blocks scraping, so the photo and description are not fetched from there; put the photo in `layers/cv/public/`. +Next to it a light / dark switch (Nuxt UI's color mode, so it applies to the whole site; the PDF is always light). +The accent color `cv.accent` is used in light mode and in the PDF; dark mode uses the site's amber like the other apps. + A DE / EN switch (remembered in the `cv-lang` cookie, German by default) translates the page's own text: headings, dates, durations, types, the description. `Area` comes from `area_german` on the German page; the rest of the table's text (`Institution`) is shown as stored. **Table:** `From`, `To` (dates), `Type` (single select), `Institution`, `Area` (text, English), `area_german` (text, used on the German page, falls back to `Area`), `Place` (single select), `Grade` (number), `relevant` (boolean), `file` (file; each file is listed under its entry as a download, e.g. a thesis), `urls` (long text: one link per line, a blank line between groups such as projects; GitHub links show -`owner/repo` with a GitHub icon, others their address; write `Label https://…` for custom text). Rows without `From` +`owner/repo` with a GitHub icon, others their address; write `Label https://…` for custom text). On phones the +links (also email, LinkedIn and files) show only their icon, with the text as tooltip. Rows without `From` are skipped. Baserow returns file URLs as `http://`, so they are rewritten to `NUXT_BASEROW_URL`. @@ -127,8 +140,8 @@ are skipped. Baserow returns file URLs as `http://`, so they are rewritten to layers/cv/ nuxt.config.ts baserowCvTableId app/ - app.config.ts cv: name, description, photo, email, linkedin - pages/cv.vue language switch, header, Experience and Education + app.config.ts cv: name, description, photo, email, linkedin, accent + pages/cv.vue light / dark and language switch, header, Experience and Education composables/useCvLang.ts current language and the translated texts components/CvSection.vue one section: period, duration, area, institution, type, grade utils/cvDate.ts period and duration, open-ended until today @@ -154,7 +167,7 @@ Shortcuts (Ctrl instead of ⌘ outside macOS): **⌘ ⌥ N** new note, **⌘ E** **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. +"Ohne Titel" at the bottom. The token needs read, create, update and delete. ``` layers/notes/ @@ -165,7 +178,7 @@ app/ 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/NotesActionBar.vue "Neu", 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 @@ -226,7 +239,7 @@ self-hosted Baserow tables. | `NUXT_BASEROW_NOTES_TABLE_ID` | Notes table | | `NUXT_BASEROW_WISHLIST_TABLE_ID` | Wishlist table (see [Wishlist](#wishlist)) | | `NUXT_BASEROW_CV_TABLE_ID` | CV table (see [CV](#cv)) | -| `NUXT_BASEROW_PACKER_TABLE_ID` | Packing table (see [Packer](#packer)) | +| `NUXT_BASEROW_PACKER_TABLE_ID` | Packing table (see [Packliste](#packliste)) | | `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`) | @@ -236,10 +249,10 @@ In `layers/diary/app/app.config.ts`, `diary.showCovers: true` shows a generated ## 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; +- Only entries with text are shown. Long text is collapsed behind "Weiterlesen". +- Search matches every word against the kind (`rezension`, `notiz`), the medium (`buch`, `film`, `album`), + the date (`okt 2025`, `2025-08`), the title, the author and the text. Matches in the text are highlighted. +- "Eintragen" opens a form with a Notiz / Rezension 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 returns 401 (the shared login, see the top of this file). Changing `NUXT_SESSION_SECRET` logs out every browser. @@ -253,9 +266,9 @@ 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 + DiaryTimeline.vue the diary: search, Eintragen button, entries grouped by year DiaryEntry.vue one timeline entry (date, title, text, read more) - LogModal.vue Note / Review switch around the two forms + LogModal.vue Notiz / Rezension switch around the two forms NoteForm.vue, ReviewForm.vue CoverTile.vue generated cover, used when showCovers is on composables/ diff --git a/app/app.config.ts b/app/app.config.ts index 4a20171..78a3105 100644 --- a/app/app.config.ts +++ b/app/app.config.ts @@ -4,5 +4,7 @@ export default defineAppConfig({ primary: 'amber', neutral: 'stone' } - } + }, + /** Tiles of the hub on `/`; each app adds its own in its layer's app.config.ts, keyed by the app name */ + hub: {} as Record }) diff --git a/app/app.vue b/app/app.vue index 8dd09d3..5ad3f1e 100644 --- a/app/app.vue +++ b/app/app.vue @@ -1,5 +1,9 @@ + + diff --git a/app/components/AppHeaderActions.vue b/app/components/AppHeaderActions.vue new file mode 100644 index 0000000..3a0765b --- /dev/null +++ b/app/components/AppHeaderActions.vue @@ -0,0 +1,11 @@ + + + diff --git a/app/components/PasswordModal.vue b/app/components/PasswordModal.vue index 8395ec3..59bf8cd 100644 --- a/app/components/PasswordModal.vue +++ b/app/components/PasswordModal.vue @@ -14,7 +14,7 @@ async function unlock() { await $fetch('/api/auth/login', { method: 'POST', body: { password: password.value } }) emit('unlocked') } catch (e) { - error.value = errorMessage(e) ?? 'Couldn\'t unlock' + error.value = errorMessage(e) ?? 'Entsperren fehlgeschlagen' password.value = '' } finally { unlocking.value = false @@ -23,13 +23,13 @@ async function unlock() {