From a02dceb51cabdfdcc1b37c268b2a0563de8c6132 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 31 Mar 2026 21:42:26 +0200 Subject: [PATCH] feat(guides): ImportModal, share button, CLAUDE.md, server dev scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ImportModal: 3-tab (URL/Text/AI) import UI with preview before saving - Guide detail: share button → generates 7-day shareable link with copy-to-clipboard - App layout: Import button in sidebar + dynamic ImportModal mount - Library page: Import button in header (desktop), openImportGuide context - Port corrected to 3027 (was 3025, conflict with CityCorners) - CLAUDE.md: full project docs (routes, collections, env vars, phase status) - Root package.json: dev:guides:server, updated dev:guides:app/local/full Co-Authored-By: Claude Sonnet 4.6 --- apps/guides/CLAUDE.md | 108 ++++++ apps/guides/apps/server/src/index.ts | 2 +- .../web/src/lib/components/ImportModal.svelte | 338 ++++++++++++++++++ .../apps/web/src/routes/(app)/+layout.svelte | 23 +- .../apps/web/src/routes/(app)/+page.svelte | 21 +- .../src/routes/(app)/guide/[id]/+page.svelte | 55 +++ package.json | 7 +- 7 files changed, 543 insertions(+), 11 deletions(-) create mode 100644 apps/guides/CLAUDE.md create mode 100644 apps/guides/apps/web/src/lib/components/ImportModal.svelte diff --git a/apps/guides/CLAUDE.md b/apps/guides/CLAUDE.md new file mode 100644 index 000000000..5373a37c5 --- /dev/null +++ b/apps/guides/CLAUDE.md @@ -0,0 +1,108 @@ +# Guides — CLAUDE.md + +Mana Guides is a local-first step-by-step guide app (SOPs, recipes, tutorials, learning paths). +Port: **5200** (web), **3027** (server) +Theme: Teal `#0d9488` +Tier: `beta` + +## Apps + +| App | Package | Port | Description | +|-----|---------|------|-------------| +| `apps/web` | `@guides/web` | 5200 | SvelteKit 5 local-first UI | +| `apps/server` | `@guides/server` | 3025 | Hono/Bun compute server (import, share) | + +## Dev Commands + +```bash +pnpm dev:guides:web # Web only +pnpm dev:guides:server # Server only +pnpm dev:guides:app # Server + web +pnpm dev:guides:local # Sync + server + web (no auth) +pnpm dev:guides:full # Auth + sync + server + web +``` + +## Architecture + +**Local-first**: All CRUD goes through `guidesStore` (Dexie.js IndexedDB), synced via mana-sync. +**Server (port 3025)**: Compute-only — web import via mana-search + mana-llm, shareable links. + +### Data Model + +``` +LocalGuide → has many LocalSection, LocalStep +LocalSection → has many LocalStep (order field) +LocalStep → belongs to LocalGuide, optional LocalSection +LocalCollection → has many LocalGuide (ordered list) +LocalRun → belongs to LocalGuide, stepStates: Record +``` + +### Collections + +| Collection | Index | Description | +|------------|-------|-------------| +| `guides` | category, difficulty, collectionId | Guide library | +| `sections` | guideId, order | Optional sections within a guide | +| `steps` | guideId, sectionId, order | Steps (instruction/warning/tip/checkpoint/code) | +| `collections` | type | Path or Library groupings | +| `runs` | guideId, startedAt | Execution history | + +## Routes + +``` +/ Library (guide grid, search, filters) +/guide/[id] Guide detail + edit mode + run history +/guide/[id]/run Run mode (?mode=scroll|focus) +/collections Collections grid +/collections/[id] Collection detail with progress +/history All run history +/shared/[token] Public shared guide (no auth needed) — Phase 4 +/(auth)/login Login page +``` + +## Server Routes (port 3025) + +``` +POST /api/v1/import/url → fetch URL → mana-search extract → mana-llm → { guide, sections } +POST /api/v1/import/text → raw text/markdown → mana-llm → { guide, sections } +POST /api/v1/import/ai → AI prompt → mana-llm → { guide, sections } +POST /api/v1/share → create shareable link (7-day TTL) → { token, url, expiresAt } +GET /api/v1/share/:token → retrieve shared guide snapshot +GET /health → service health check +``` + +## Environment Variables + +```env +# Web +PUBLIC_SYNC_SERVER_URL=ws://localhost:3050 +PUBLIC_GUIDES_SERVER_URL=http://localhost:3027 + +# Server +PORT=3027 +CORS_ORIGINS=http://localhost:5200 +MANA_SEARCH_URL=http://localhost:3021 +MANA_LLM_URL=http://localhost:3030 +PUBLIC_BASE_URL=http://localhost:5200 +``` + +## Key Files + +| File | Purpose | +|------|---------| +| `apps/web/src/lib/data/local-store.ts` | 5 Dexie collections with TypeScript types | +| `apps/web/src/lib/data/guest-seed.ts` | 3 demo guides, 1 collection for onboarding | +| `apps/web/src/lib/stores/guides.svelte.ts` | Guide/section/step/collection mutations | +| `apps/web/src/lib/stores/runs.svelte.ts` | Run start/step state/complete mutations | +| `apps/web/src/routes/(app)/guide/[id]/run/+page.svelte` | Scroll + focus run modes | +| `apps/server/src/routes/import.ts` | URL/text/AI import via mana-llm | +| `apps/server/src/routes/share.ts` | Shareable guide links (in-memory MVP) | + +## Phase Status + +| Phase | Status | Description | +|-------|--------|-------------| +| 1 | Done | Core CRUD, local-first, guest seed, library/detail/run views | +| 2 | Done | Collections, StepEditorModal, CollectionEditModal, inline step add | +| 3 | In progress | Hono server (import + share done), ImportModal frontend, share button | +| 4 | Planned | DB persistence for shares, /shared/[token] public route, XP/gamification | diff --git a/apps/guides/apps/server/src/index.ts b/apps/guides/apps/server/src/index.ts index c5235ecb3..7fe65ee5e 100644 --- a/apps/guides/apps/server/src/index.ts +++ b/apps/guides/apps/server/src/index.ts @@ -43,7 +43,7 @@ app.get('/health', (c) => }) ); -const port = Number(process.env.PORT ?? 3025); +const port = Number(process.env.PORT ?? 3027); console.log(`🚀 Guides server (Hono + Bun) starting on port ${port}`); export default { port, fetch: app.fetch }; diff --git a/apps/guides/apps/web/src/lib/components/ImportModal.svelte b/apps/guides/apps/web/src/lib/components/ImportModal.svelte new file mode 100644 index 000000000..d3d06da90 --- /dev/null +++ b/apps/guides/apps/web/src/lib/components/ImportModal.svelte @@ -0,0 +1,338 @@ + + +{#if open} + + +{/if} diff --git a/apps/guides/apps/web/src/routes/(app)/+layout.svelte b/apps/guides/apps/web/src/routes/(app)/+layout.svelte index 39591a589..4c5a969b9 100644 --- a/apps/guides/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/guides/apps/web/src/routes/(app)/+layout.svelte @@ -12,7 +12,9 @@ // Context for child pages let showCreateModal = $state(false); + let showImportModal = $state(false); setContext('openCreateGuide', () => { showCreateModal = true; }); + setContext('openImportGuide', () => { showImportModal = true; }); // Nav items const navItems = [ @@ -57,7 +59,7 @@ {/each} -
+
+
@@ -115,3 +123,16 @@ /> {/await} {/if} + +{#if showImportModal} + {#await import('$lib/components/ImportModal.svelte') then { default: ImportModal }} + (showImportModal = false)} + onImported={(id) => { + showImportModal = false; + goto(`/guide/${id}`); + }} + /> + {/await} +{/if} diff --git a/apps/guides/apps/web/src/routes/(app)/+page.svelte b/apps/guides/apps/web/src/routes/(app)/+page.svelte index cba3f5697..8179cea7b 100644 --- a/apps/guides/apps/web/src/routes/(app)/+page.svelte +++ b/apps/guides/apps/web/src/routes/(app)/+page.svelte @@ -48,6 +48,7 @@ ); const openCreateGuide = getContext<() => void>('openCreateGuide'); + const openImportGuide = getContext<() => void>('openImportGuide'); const difficultyLabels = { easy: 'Einfach', medium: 'Mittel', hard: 'Schwer' }; @@ -59,12 +60,20 @@

Bibliothek

{allGuides.length} Anleitungen

- + diff --git a/apps/guides/apps/web/src/routes/(app)/guide/[id]/+page.svelte b/apps/guides/apps/web/src/routes/(app)/guide/[id]/+page.svelte index d6205c7cc..40230b76a 100644 --- a/apps/guides/apps/web/src/routes/(app)/guide/[id]/+page.svelte +++ b/apps/guides/apps/web/src/routes/(app)/guide/[id]/+page.svelte @@ -164,6 +164,40 @@ if (activeRun) goto(`/guide/${guideId}/run?runId=${activeRun.id}&mode=${activeRun.mode}`); } + // ── Share ──────────────────────────────────────────────── + + const SERVER_URL = import.meta.env.PUBLIC_GUIDES_SERVER_URL || 'http://localhost:3027'; + let shareUrl = $state(null); + let shareLoading = $state(false); + let shareCopied = $state(false); + + async function shareGuide() { + if (!guide) return; + shareLoading = true; + try { + const res = await fetch(`${SERVER_URL}/api/v1/share`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ guide, sections: steps }), + }); + if (res.ok) { + const data = await res.json<{ url: string }>(); + shareUrl = data.url; + } + } catch { + // silently fail — share is optional + } finally { + shareLoading = false; + } + } + + async function copyShareUrl() { + if (!shareUrl) return; + await navigator.clipboard.writeText(shareUrl); + shareCopied = true; + setTimeout(() => { shareCopied = false; }, 2000); + } + // ── Display config ─────────────────────────────────────── const difficultyConfig = { @@ -194,6 +228,14 @@ ← Bibliothek
+
+ + {#if shareUrl} +
+ {shareUrl} + +
+ {/if} + {#if activeRun && !editMode}
diff --git a/package.json b/package.json index 49e4ea0bb..10405a616 100644 --- a/package.json +++ b/package.json @@ -141,9 +141,10 @@ "dev:calc:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:calc:web\"", "guides:dev": "turbo run dev --filter=guides...", "dev:guides:web": "pnpm --filter @guides/web dev", - "dev:guides:app": "pnpm dev:guides:web", - "dev:guides:local": "concurrently -n sync,web -c magenta,cyan \"pnpm dev:sync\" \"pnpm dev:guides:web\"", - "dev:guides:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:guides:web\"", + "dev:guides:server": "pnpm --filter @guides/server dev", + "dev:guides:app": "concurrently -n server,web -c yellow,cyan \"pnpm dev:guides:server\" \"pnpm dev:guides:web\"", + "dev:guides:local": "concurrently -n sync,server,web -c magenta,yellow,cyan \"pnpm dev:sync\" \"pnpm dev:guides:server\" \"pnpm dev:guides:web\"", + "dev:guides:full": "concurrently -n auth,sync,server,web -c blue,magenta,yellow,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:guides:server\" \"pnpm dev:guides:web\"", "moodlit:dev": "turbo run dev --filter=moodlit...", "dev:moodlit:mobile": "pnpm --filter @moodlit/mobile dev", "dev:moodlit:app": "concurrently -n server,web -c yellow,cyan \"pnpm dev:moodlit:server\" \"pnpm dev:moodlit:web\"",