From 1a999f8cca5ca3d89d3e1a5ab75190553bab1b06 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 31 Mar 2026 21:23:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(guides):=20Phase=202=20=E2=80=94=20step=20?= =?UTF-8?q?editor,=20edit=20mode,=20collection=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guide detail page: - Edit mode toggle (✏ Bearbeiten / βœ“ Fertig) - StepEditorModal: 5 step types, title, markdown content, checkable toggle - Per-step controls in edit mode: edit, delete, reorder ↑↓ (on hover) - Section management: add / delete sections in edit mode - Unsectioned + sectioned steps both editable - Empty state with CTA to enter edit mode - Guide delete with confirmation dialog - Run history shows mode icon (πŸ“œ scroll / 🎯 focus) Collections page: - CollectionEditModal: emoji, color, path vs library type - Create collection from empty state and header button - Edit/delete existing collections Co-Authored-By: Claude Sonnet 4.6 --- .claude/plans/mana-guides.md | 10 +- .../lib/components/CollectionEditModal.svelte | 142 ++++++ .../src/lib/components/StepEditorModal.svelte | 171 +++++++ .../src/routes/(app)/collections/+page.svelte | 44 +- .../src/routes/(app)/guide/[id]/+page.svelte | 432 +++++++++++++----- 5 files changed, 688 insertions(+), 111 deletions(-) create mode 100644 apps/guides/apps/web/src/lib/components/CollectionEditModal.svelte create mode 100644 apps/guides/apps/web/src/lib/components/StepEditorModal.svelte diff --git a/.claude/plans/mana-guides.md b/.claude/plans/mana-guides.md index 6443faae3..0847c60b8 100644 --- a/.claude/plans/mana-guides.md +++ b/.claude/plans/mana-guides.md @@ -151,7 +151,15 @@ apps/guides/apps/web/src/routes/ - [x] GuideEditModal - [x] Registrierung in mana-apps.ts + app-icons.ts -### Phase 2 β€” Web-Import & Sharing +### Phase 2 β€” Content-Editing βœ… Abgeschlossen +- [x] StepEditorModal (Typ, Titel, Content, Checkable-Toggle) +- [x] Guide-Detail Edit-Mode (Steps hinzufΓΌgen, bearbeiten, lΓΆschen, sortieren ↑↓) +- [x] Abschnitt-Verwaltung im Edit-Mode (hinzufΓΌgen, lΓΆschen) +- [x] Guide lΓΆschen (mit BestΓ€tigungs-Dialog) +- [x] CollectionEditModal (Emoji, Farbe, Typ path/library) +- [x] Collections erstellen/bearbeiten aus Collections-View + +### Phase 3 β€” Web-Import & Sharing - [ ] Hono/Bun-Server (apps/guides/apps/server/) - [ ] Web-Import: URL β†’ Guide via mana-search - [ ] Guide-Export: JSON / Markdown diff --git a/apps/guides/apps/web/src/lib/components/CollectionEditModal.svelte b/apps/guides/apps/web/src/lib/components/CollectionEditModal.svelte new file mode 100644 index 000000000..60370b1c9 --- /dev/null +++ b/apps/guides/apps/web/src/lib/components/CollectionEditModal.svelte @@ -0,0 +1,142 @@ + + +{#if open} + +
+ + +
+{/if} diff --git a/apps/guides/apps/web/src/lib/components/StepEditorModal.svelte b/apps/guides/apps/web/src/lib/components/StepEditorModal.svelte new file mode 100644 index 000000000..c130ced22 --- /dev/null +++ b/apps/guides/apps/web/src/lib/components/StepEditorModal.svelte @@ -0,0 +1,171 @@ + + +{#if open} + +
+ + +
+{/if} diff --git a/apps/guides/apps/web/src/routes/(app)/collections/+page.svelte b/apps/guides/apps/web/src/routes/(app)/collections/+page.svelte index 9efc1cccd..a9d03d6a5 100644 --- a/apps/guides/apps/web/src/routes/(app)/collections/+page.svelte +++ b/apps/guides/apps/web/src/routes/(app)/collections/+page.svelte @@ -2,6 +2,11 @@ import { liveQuery } from 'dexie'; import { collectionCollection, guideCollection, runCollection } from '$lib/data/local-store.js'; import type { LocalCollection, LocalGuide, LocalRun } from '$lib/data/local-store.js'; + import { guidesStore } from '$lib/stores/guides.svelte'; + import CollectionEditModal from '$lib/components/CollectionEditModal.svelte'; + + let showCreateModal = $state(false); + let editingCollection = $state(undefined); let collections = $state([]); let allGuides = $state([]); @@ -43,18 +48,32 @@
-
-

Sammlungen

-

Lernpfade und thematische Anleitungs-Sets

+
+
+

Sammlungen

+

Lernpfade und thematische Anleitungs-Sets

+
+
{#if collections.length === 0}
πŸ“‚

Noch keine Sammlungen

-

+

Sammlungen gruppieren Anleitungen zu Lernpfaden oder thematischen Bibliotheken.

+
{:else}
@@ -105,3 +124,20 @@
{/if}
+ +{#if showCreateModal} + (showCreateModal = false)} + onSave={async (data) => { + if (editingCollection) await guidesStore.updateCollection(editingCollection.id, data); + else await guidesStore.createCollection(data); + showCreateModal = false; + }} + onDelete={editingCollection ? async (id) => { + await collectionCollection.delete(id); + showCreateModal = false; + } : undefined} + /> +{/if} 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 cd62fcdf3..8a70e8c9c 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 @@ -14,15 +14,18 @@ } from '$lib/data/local-store.js'; import { runsStore } from '$lib/stores/runs.svelte'; import { guidesStore } from '$lib/stores/guides.svelte'; + import GuideEditModal from '$lib/components/GuideEditModal.svelte'; + import StepEditorModal from '$lib/components/StepEditorModal.svelte'; + import type { BaseRecord } from '@manacore/local-store'; let guideId = $derived($page.params.id); + // ── Data ──────────────────────────────────────────────── + let guide = $state(null); let sections = $state([]); let steps = $state([]); let runs = $state([]); - let showEditModal = $state(false); - let showDeleteConfirm = $state(false); $effect(() => { const id = guideId; @@ -43,22 +46,94 @@ return () => sub.unsubscribe(); }); + // ── Derived ───────────────────────────────────────────── + let completedRuns = $derived(runs.filter((r) => r.completedAt)); let activeRun = $derived(runs.find((r) => !r.completedAt)); - let totalSteps = $derived(steps.filter((s) => s.checkable).length); + let checkableSteps = $derived(steps.filter((s) => s.checkable)); function getStepsForSection(sectionId: string) { - return steps.filter((s) => s.sectionId === sectionId); + return steps.filter((s) => s.sectionId === sectionId).sort((a, b) => a.order - b.order); } - function getUnsectionedSteps() { - return steps.filter((s) => !s.sectionId); + return steps.filter((s) => !s.sectionId).sort((a, b) => a.order - b.order); + } + function getActiveRunProgress() { + if (!activeRun || !checkableSteps.length) return 0; + const done = Object.values(activeRun.stepStates).filter((s) => s.done).length; + return Math.round((done / checkableSteps.length) * 100); } - function getActiveRunProgress() { - if (!activeRun) return 0; - const done = Object.values(activeRun.stepStates).filter((s) => s.done).length; - return totalSteps > 0 ? Math.round((done / totalSteps) * 100) : 0; + // ── UI state ──────────────────────────────────────────── + + let editMode = $state(false); + let showGuideEditModal = $state(false); + let showDeleteConfirm = $state(false); + let showAddSection = $state(false); + let newSectionTitle = $state(''); + + // StepEditorModal state + let stepModalOpen = $state(false); + let editingStep = $state(undefined); + let stepModalSectionId = $state(undefined); + + function openAddStep(sectionId?: string) { + editingStep = undefined; + stepModalSectionId = sectionId; + stepModalOpen = true; + } + function openEditStep(step: LocalStep) { + editingStep = step; + stepModalSectionId = step.sectionId; + stepModalOpen = true; + } + + // ── Actions ───────────────────────────────────────────── + + async function handleSaveStep(data: Omit) { + if (editingStep) { + await guidesStore.updateStep(editingStep.id, data); + } else { + const targetSteps = data.sectionId + ? getStepsForSection(data.sectionId) + : getUnsectionedSteps(); + await guidesStore.createStep({ ...data, order: targetSteps.length }); + } + stepModalOpen = false; + } + + async function deleteStep(stepId: string) { + await guidesStore.deleteStep(stepId); + } + + async function moveStep(step: LocalStep, direction: 'up' | 'down') { + const siblings = step.sectionId + ? getStepsForSection(step.sectionId) + : getUnsectionedSteps(); + const idx = siblings.findIndex((s) => s.id === step.id); + const swapIdx = direction === 'up' ? idx - 1 : idx + 1; + if (swapIdx < 0 || swapIdx >= siblings.length) return; + const other = siblings[swapIdx]; + await Promise.all([ + guidesStore.updateStep(step.id, { order: other.order }), + guidesStore.updateStep(other.id, { order: step.order }), + ]); + } + + async function addSection() { + if (!newSectionTitle.trim()) return; + await guidesStore.createSection({ guideId, title: newSectionTitle.trim(), order: sections.length }); + newSectionTitle = ''; + showAddSection = false; + } + + async function deleteSection(sectionId: string) { + await guidesStore.deleteSection(sectionId); + } + + async function handleDeleteGuide() { + await guidesStore.deleteGuide(guideId); + goto('/'); } async function startRun(mode: 'scroll' | 'focus') { @@ -70,97 +145,115 @@ if (activeRun) goto(`/guide/${guideId}/run?runId=${activeRun.id}&mode=${activeRun.mode}`); } + // ── Display config ─────────────────────────────────────── + const difficultyConfig = { - easy: { label: 'Einfach', color: 'text-green-600 bg-green-50' }, - medium: { label: 'Mittel', color: 'text-amber-600 bg-amber-50' }, - hard: { label: 'Schwer', color: 'text-red-600 bg-red-50' }, + easy: { label: 'Einfach', color: 'text-green-600 bg-green-50 dark:bg-green-950/30' }, + medium: { label: 'Mittel', color: 'text-amber-600 bg-amber-50 dark:bg-amber-950/30' }, + hard: { label: 'Schwer', color: 'text-red-600 bg-red-50 dark:bg-red-950/30' }, }; - const stepTypeConfig = { - instruction: { icon: 'β†’', color: 'border-l-primary' }, - warning: { icon: '⚠', color: 'border-l-orange-400' }, - tip: { icon: 'πŸ’‘', color: 'border-l-violet-400' }, - checkpoint: { icon: 'βœ“', color: 'border-l-blue-400' }, - code: { icon: '', color: 'border-l-slate-400' }, + const stepTypeConfig: Record = { + instruction: { icon: 'β†’', border: 'border-l-primary', bg: '' }, + warning: { icon: '⚠', border: 'border-l-orange-400', bg: 'bg-orange-50/50 dark:bg-orange-950/20' }, + tip: { icon: 'πŸ’‘', border: 'border-l-violet-400', bg: 'bg-violet-50/50 dark:bg-violet-950/20' }, + checkpoint: { icon: 'βœ“', border: 'border-l-blue-400', bg: 'bg-blue-50/50 dark:bg-blue-950/20' }, + code: { icon: '', border: 'border-l-slate-400', bg: 'bg-slate-50/50 dark:bg-slate-950/20' }, }; {#if !guide} -
+

Anleitung nicht gefunden.

{:else}
- - - ← Bibliothek - + + +
+ + ← Bibliothek + +
+ +
+
- {guide.coverEmoji ?? 'πŸ“–'} -
-

{guide.title}

- {#if guide.description} -

{guide.description}

- {/if} -
- - {difficultyConfig[guide.difficulty].label} - - {#if guide.estimatedMinutes} - ⏱ {guide.estimatedMinutes} min - {/if} - {totalSteps} Schritte - {#if completedRuns.length > 0} - βœ“ {completedRuns.length}Γ— abgeschlossen +
+ {guide.coverEmoji ?? 'πŸ“–'} +
+

{guide.title}

+ {#if guide.description} +

{guide.description}

{/if} +
+ + {difficultyConfig[guide.difficulty].label} + + {#if guide.estimatedMinutes} + ⏱ {guide.estimatedMinutes}min + {/if} + {steps.length} Schritte + {#if completedRuns.length > 0} + βœ“ {completedRuns.length}Γ— abgeschlossen + {/if} +
-
-
- + {#if editMode} +
+ + +
+ {/if}
- {#if activeRun} + {#if activeRun && !editMode}
Aktiver Durchlauf - {getActiveRunProgress()}% abgeschlossen + {getActiveRunProgress()}%
-
+
-
- {:else} - + {:else if !editMode}
{/if} - +
+ + {#if sections.length > 0} {#each sections as section (section.id)} + {@const sectionSteps = getStepsForSection(section.id)}
-

- {section.title} -

+
+

+ {section.title} +

+ {#if editMode} + + {/if} +
+
- {#each getStepsForSection(section.id) as step (step.id)} + {#each sectionSteps as step, i (step.id)} {@const cfg = stepTypeConfig[step.type]} -
-
- {cfg.icon} -
-

{step.title}

- {#if step.content} -

{step.content}

- {/if} -
+
+ {cfg.icon} +
+

{step.title}

+ {#if step.content} +

{step.content}

+ {/if}
+ {#if editMode} +
+ + + + +
+ {/if}
{/each} + + {#if editMode} + + {/if}
{/each} + {/if} - {#if getUnsectionedSteps().length > 0} -
- {#each getUnsectionedSteps() as step (step.id)} - {@const cfg = stepTypeConfig[step.type]} -
-

{step.title}

-
- {/each} -
- {/if} - {:else} + + {#if getUnsectionedSteps().length > 0 || (sections.length === 0)} + {@const unsectioned = getUnsectionedSteps()}
- {#each steps as step (step.id)} + {#each unsectioned as step, i (step.id)} {@const cfg = stepTypeConfig[step.type]} -
-
- {cfg.icon} -
-

{step.title}

- {#if step.content} -

{step.content}

- {/if} -
+
+ {cfg.icon} +
+

{step.title}

+ {#if step.content} +

{step.content}

+ {/if}
+ {#if editMode} +
+ + + + +
+ {/if}
{/each} + + {#if editMode} + + {/if} +
+ {/if} + + + {#if steps.length === 0 && !editMode} +
+

Noch keine Schritte

+ +
+ {/if} + + + {#if editMode} +
+ {#if showAddSection} +
+ { if (e.key === 'Enter') addSection(); if (e.key === 'Escape') { showAddSection = false; newSectionTitle = ''; } }} + class="flex-1 rounded-xl border border-border bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30" + /> + + +
+ {:else} + + {/if}
{/if}
- {#if completedRuns.length > 0} -
+ {#if completedRuns.length > 0 && !editMode} +

Verlauf

{#each completedRuns.slice(0, 5) as run (run.id)} + {@const doneCount = Object.values(run.stepStates).filter((s) => s.done).length}
βœ“ - {new Date(run.startedAt).toLocaleDateString('de-DE', { - day: '2-digit', month: '2-digit', year: 'numeric' - })} + {new Date(run.startedAt).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
- - {Object.values(run.stepStates).filter((s) => s.done).length}/{totalSteps} Schritte - + {doneCount}/{checkableSteps.length} Schritte Β· {run.mode === 'focus' ? '🎯' : 'πŸ“œ'}
{/each}
@@ -250,3 +419,54 @@ {/if}
{/if} + + +{#if showGuideEditModal && guide} + (showGuideEditModal = false)} + onSave={async (data) => { + await guidesStore.updateGuide(guide!.id, data); + showGuideEditModal = false; + }} + onDelete={async (id) => { + showGuideEditModal = false; + await guidesStore.deleteGuide(id); + goto('/'); + }} + /> +{/if} + +{#if stepModalOpen} + (stepModalOpen = false)} + onSave={handleSaveStep} + /> +{/if} + + +{#if showDeleteConfirm} + +
e.target === e.currentTarget && (showDeleteConfirm = false)}> +
+

Anleitung lΓΆschen?

+

+ β€ž{guide?.title}" und alle Schritte werden unwiderruflich gelΓΆscht. +

+
+ + +
+
+
+{/if}