mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(guides): Phase 2 — step editor, edit mode, collection management
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 <noreply@anthropic.com>
This commit is contained in:
parent
8e496ff417
commit
1a999f8cca
5 changed files with 688 additions and 111 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
<script lang="ts">
|
||||
import type { LocalCollection } from '$lib/data/local-store.js';
|
||||
import type { BaseRecord } from '@manacore/local-store';
|
||||
|
||||
type CollectionInput = Omit<LocalCollection, keyof BaseRecord>;
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
collection?: LocalCollection;
|
||||
onClose: () => void;
|
||||
onSave: (data: CollectionInput) => Promise<void>;
|
||||
onDelete?: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
let { open, collection, onClose, onSave, onDelete }: Props = $props();
|
||||
|
||||
let title = $state(collection?.title ?? '');
|
||||
let description = $state(collection?.description ?? '');
|
||||
let coverEmoji = $state(collection?.coverEmoji ?? '📚');
|
||||
let coverColor = $state(collection?.coverColor ?? '#0d9488');
|
||||
let type = $state<'path' | 'library'>(collection?.type ?? 'path');
|
||||
let saving = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (collection) {
|
||||
title = collection.title;
|
||||
description = collection.description ?? '';
|
||||
coverEmoji = collection.coverEmoji ?? '📚';
|
||||
coverColor = collection.coverColor ?? '#0d9488';
|
||||
type = collection.type;
|
||||
}
|
||||
});
|
||||
|
||||
const COVER_COLORS = ['#0d9488', '#3b82f6', '#8b5cf6', '#f59e0b', '#ef4444', '#ec4899', '#10b981', '#f97316'];
|
||||
|
||||
async function handleSave() {
|
||||
if (!title.trim()) return;
|
||||
saving = true;
|
||||
try {
|
||||
await onSave({
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
coverEmoji,
|
||||
coverColor,
|
||||
type,
|
||||
guideOrder: collection?.guideOrder ?? [],
|
||||
});
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') onClose();
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') handleSave();
|
||||
}
|
||||
function handleBackdrop(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div class="fixed inset-0 z-50 flex items-end justify-center bg-black/50 sm:items-center sm:p-4" onmousedown={handleBackdrop}>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div role="dialog" aria-modal="true" class="w-full max-w-md overflow-hidden rounded-t-2xl bg-background shadow-xl sm:rounded-2xl" onkeydown={handleKeydown}>
|
||||
<div class="flex items-center justify-between border-b border-border px-5 py-4">
|
||||
<h2 class="font-semibold text-foreground">{collection ? 'Sammlung bearbeiten' : 'Neue Sammlung'}</h2>
|
||||
<button onclick={onClose} class="text-muted-foreground hover:text-foreground">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<!-- Emoji + Color -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div>
|
||||
<p class="mb-1 text-xs text-muted-foreground">Emoji</p>
|
||||
<input type="text" bind:value={coverEmoji} maxlength="2"
|
||||
class="h-12 w-16 rounded-xl border border-border bg-surface text-center text-2xl focus:outline-none focus:ring-2 focus:ring-primary/30" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="mb-1 text-xs text-muted-foreground">Farbe</p>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{#each COVER_COLORS as color}
|
||||
<button onclick={() => (coverColor = color)}
|
||||
class="h-7 w-7 rounded-full transition-transform hover:scale-110 {coverColor === color ? 'ring-2 ring-offset-2 ring-foreground' : ''}"
|
||||
style="background-color: {color}"></button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-muted-foreground">Titel *</label>
|
||||
<input type="text" bind:value={title} placeholder="z.B. Developer Starter Kit" autofocus
|
||||
class="w-full rounded-xl border border-border bg-surface px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30" />
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-muted-foreground">Beschreibung</label>
|
||||
<textarea bind:value={description} rows="2" placeholder="Kurze Beschreibung..."
|
||||
class="w-full resize-none rounded-xl border border-border bg-surface px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Type -->
|
||||
<div>
|
||||
<label class="mb-2 block text-xs font-medium text-muted-foreground">Typ</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button onclick={() => (type = 'path')}
|
||||
class="rounded-xl border p-3 text-left transition-colors {type === 'path' ? 'border-primary bg-primary/10' : 'border-border hover:bg-accent'}">
|
||||
<div class="text-lg mb-1">🗺</div>
|
||||
<div class="text-sm font-medium text-foreground">Lernpfad</div>
|
||||
<div class="text-xs text-muted-foreground">Geordnete Sequenz</div>
|
||||
</button>
|
||||
<button onclick={() => (type = 'library')}
|
||||
class="rounded-xl border p-3 text-left transition-colors {type === 'library' ? 'border-primary bg-primary/10' : 'border-border hover:bg-accent'}">
|
||||
<div class="text-lg mb-1">📚</div>
|
||||
<div class="text-sm font-medium text-foreground">Bibliothek</div>
|
||||
<div class="text-xs text-muted-foreground">Thematische Gruppe</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border-t border-border px-5 py-4">
|
||||
<div>
|
||||
{#if collection && onDelete}
|
||||
<button onclick={() => onDelete(collection!.id)} class="text-sm text-red-500 hover:text-red-600">Löschen</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button onclick={onClose} class="rounded-xl border border-border px-4 py-2 text-sm hover:bg-accent">Abbrechen</button>
|
||||
<button onclick={handleSave} disabled={!title.trim() || saving}
|
||||
class="rounded-xl bg-primary px-5 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50">
|
||||
{saving ? 'Speichern...' : collection ? 'Speichern' : 'Erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
171
apps/guides/apps/web/src/lib/components/StepEditorModal.svelte
Normal file
171
apps/guides/apps/web/src/lib/components/StepEditorModal.svelte
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<script lang="ts">
|
||||
import type { LocalStep, StepType } from '$lib/data/local-store.js';
|
||||
import type { BaseRecord } from '@manacore/local-store';
|
||||
|
||||
type StepInput = Omit<LocalStep, keyof BaseRecord>;
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
step?: LocalStep;
|
||||
guideId: string;
|
||||
sectionId?: string;
|
||||
order: number;
|
||||
onClose: () => void;
|
||||
onSave: (data: StepInput) => Promise<void>;
|
||||
}
|
||||
|
||||
let { open, step, guideId, sectionId, order, onClose, onSave }: Props = $props();
|
||||
|
||||
let title = $state(step?.title ?? '');
|
||||
let content = $state(step?.content ?? '');
|
||||
let type = $state<StepType>(step?.type ?? 'instruction');
|
||||
let checkable = $state(step?.checkable ?? true);
|
||||
let saving = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (step) {
|
||||
title = step.title;
|
||||
content = step.content ?? '';
|
||||
type = step.type;
|
||||
checkable = step.checkable;
|
||||
} else {
|
||||
title = '';
|
||||
content = '';
|
||||
type = 'instruction';
|
||||
checkable = true;
|
||||
}
|
||||
});
|
||||
|
||||
const STEP_TYPES: { value: StepType; label: string; icon: string; description: string }[] = [
|
||||
{ value: 'instruction', label: 'Anweisung', icon: '→', description: 'Normaler Schritt' },
|
||||
{ value: 'warning', label: 'Warnung', icon: '⚠', description: 'Wichtiger Hinweis' },
|
||||
{ value: 'tip', label: 'Tipp', icon: '💡', description: 'Hilfreicher Hinweis' },
|
||||
{ value: 'checkpoint', label: 'Checkpoint', icon: '✓', description: 'Überprüfungspunkt' },
|
||||
{ value: 'code', label: 'Code', icon: '</>', description: 'Code-Block (Markdown)' },
|
||||
];
|
||||
|
||||
async function handleSave() {
|
||||
if (!title.trim()) return;
|
||||
saving = true;
|
||||
try {
|
||||
await onSave({ title: title.trim(), content: content.trim() || undefined, type, checkable, guideId, sectionId, order });
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') onClose();
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') handleSave();
|
||||
}
|
||||
|
||||
function handleBackdrop(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-end justify-center bg-black/50 sm:items-center sm:p-4"
|
||||
onmousedown={handleBackdrop}
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={step ? 'Schritt bearbeiten' : 'Neuer Schritt'}
|
||||
class="w-full max-w-lg overflow-hidden rounded-t-2xl bg-background shadow-xl sm:rounded-2xl"
|
||||
onkeydown={handleKeydown}
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-border px-5 py-4">
|
||||
<h2 class="font-semibold text-foreground">{step ? 'Schritt bearbeiten' : 'Neuer Schritt'}</h2>
|
||||
<button onclick={onClose} class="text-muted-foreground hover:text-foreground">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="max-h-[75vh] overflow-y-auto p-5 space-y-4">
|
||||
<!-- Step type selector -->
|
||||
<div>
|
||||
<label class="mb-2 block text-xs font-medium text-muted-foreground">Typ</label>
|
||||
<div class="grid grid-cols-5 gap-1.5">
|
||||
{#each STEP_TYPES as t}
|
||||
<button
|
||||
onclick={() => (type = t.value)}
|
||||
title={t.description}
|
||||
class="flex flex-col items-center gap-1 rounded-xl border py-2.5 text-center transition-colors
|
||||
{type === t.value
|
||||
? 'border-primary bg-primary/10 text-primary'
|
||||
: 'border-border text-muted-foreground hover:bg-accent'}"
|
||||
>
|
||||
<span class="text-base">{t.icon}</span>
|
||||
<span class="text-[10px] leading-tight">{t.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-muted-foreground">Schritt-Titel *</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={title}
|
||||
placeholder="z.B. npm install ausführen"
|
||||
autofocus
|
||||
class="w-full rounded-xl border border-border bg-surface px-3 py-2.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-muted-foreground">
|
||||
Beschreibung / Inhalt
|
||||
{#if type === 'code'}
|
||||
<span class="font-normal">(Markdown · ```bash ... ``` für Code-Blöcke)</span>
|
||||
{/if}
|
||||
</label>
|
||||
<textarea
|
||||
bind:value={content}
|
||||
placeholder={type === 'code'
|
||||
? '```bash\nnpm install\nnpm run dev\n```'
|
||||
: 'Optionale Beschreibung oder Details...'}
|
||||
rows={type === 'code' ? 5 : 3}
|
||||
class="w-full resize-y rounded-xl border border-border bg-surface px-3 py-2.5 font-mono text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Checkable toggle -->
|
||||
<label class="flex cursor-pointer items-center justify-between rounded-xl border border-border bg-surface px-4 py-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Abhakbar</p>
|
||||
<p class="text-xs text-muted-foreground">Schritt kann im Run-Modus abgehakt werden</p>
|
||||
</div>
|
||||
<div
|
||||
class="relative h-5 w-9 rounded-full transition-colors {checkable ? 'bg-primary' : 'bg-muted'}"
|
||||
onclick={() => (checkable = !checkable)}
|
||||
role="switch"
|
||||
aria-checked={checkable}
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="absolute top-0.5 h-4 w-4 rounded-full bg-white shadow transition-transform {checkable ? 'translate-x-4' : 'translate-x-0.5'}"
|
||||
></span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 border-t border-border px-5 py-4">
|
||||
<button onclick={onClose} class="rounded-xl border border-border px-4 py-2 text-sm text-foreground hover:bg-accent">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onclick={handleSave}
|
||||
disabled={!title.trim() || saving}
|
||||
class="rounded-xl bg-primary px-5 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Speichern...' : step ? 'Speichern' : 'Hinzufügen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -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<LocalCollection | undefined>(undefined);
|
||||
|
||||
let collections = $state<LocalCollection[]>([]);
|
||||
let allGuides = $state<LocalGuide[]>([]);
|
||||
|
|
@ -43,18 +48,32 @@
|
|||
</script>
|
||||
|
||||
<div class="p-4 md:p-6">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-foreground">Sammlungen</h1>
|
||||
<p class="text-sm text-muted-foreground">Lernpfade und thematische Anleitungs-Sets</p>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-foreground">Sammlungen</h1>
|
||||
<p class="text-sm text-muted-foreground">Lernpfade und thematische Anleitungs-Sets</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => { editingCollection = undefined; showCreateModal = true; }}
|
||||
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
|
||||
>
|
||||
+ Neu
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if collections.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-24 text-center">
|
||||
<span class="mb-4 text-6xl">📂</span>
|
||||
<h2 class="mb-2 text-lg font-semibold">Noch keine Sammlungen</h2>
|
||||
<p class="text-sm text-muted-foreground max-w-sm">
|
||||
<p class="mb-6 text-sm text-muted-foreground max-w-sm">
|
||||
Sammlungen gruppieren Anleitungen zu Lernpfaden oder thematischen Bibliotheken.
|
||||
</p>
|
||||
<button
|
||||
onclick={() => { editingCollection = undefined; showCreateModal = true; }}
|
||||
class="rounded-lg bg-primary px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-hover"
|
||||
>
|
||||
Erste Sammlung erstellen
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
|
@ -105,3 +124,20 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if showCreateModal}
|
||||
<CollectionEditModal
|
||||
open={true}
|
||||
collection={editingCollection}
|
||||
onClose={() => (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}
|
||||
|
|
|
|||
|
|
@ -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<LocalGuide | null>(null);
|
||||
let sections = $state<LocalSection[]>([]);
|
||||
let steps = $state<LocalStep[]>([]);
|
||||
let runs = $state<LocalRun[]>([]);
|
||||
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<LocalStep | undefined>(undefined);
|
||||
let stepModalSectionId = $state<string | undefined>(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<LocalStep, keyof BaseRecord>) {
|
||||
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<string, { icon: string; border: string; bg: string }> = {
|
||||
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' },
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if !guide}
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<p class="text-muted-foreground">Anleitung nicht gefunden.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mx-auto max-w-3xl p-4 md:p-8">
|
||||
<!-- Back -->
|
||||
<a href="/" class="mb-6 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
||||
← Bibliothek
|
||||
</a>
|
||||
|
||||
<!-- Back + Edit toggle -->
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<a href="/" class="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
||||
← Bibliothek
|
||||
</a>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={() => (editMode = !editMode)}
|
||||
class="rounded-lg px-3 py-1.5 text-xs font-medium transition-colors
|
||||
{editMode ? 'bg-primary text-white' : 'border border-border text-muted-foreground hover:bg-accent'}"
|
||||
>
|
||||
{editMode ? '✓ Fertig' : '✏ Bearbeiten'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cover header -->
|
||||
<div
|
||||
class="mb-6 flex items-center gap-4 rounded-2xl p-6"
|
||||
style="background-color: {guide.coverColor ?? '#0d9488'}20"
|
||||
class="mb-6 rounded-2xl p-5"
|
||||
style="background-color: {guide.coverColor ?? '#0d9488'}18"
|
||||
>
|
||||
<span class="text-5xl">{guide.coverEmoji ?? '📖'}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-2xl font-bold text-foreground">{guide.title}</h1>
|
||||
{#if guide.description}
|
||||
<p class="mt-1 text-sm text-muted-foreground">{guide.description}</p>
|
||||
{/if}
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span class="rounded-full px-2 py-0.5 text-xs font-medium {difficultyConfig[guide.difficulty].color}">
|
||||
{difficultyConfig[guide.difficulty].label}
|
||||
</span>
|
||||
{#if guide.estimatedMinutes}
|
||||
<span class="text-xs text-muted-foreground">⏱ {guide.estimatedMinutes} min</span>
|
||||
{/if}
|
||||
<span class="text-xs text-muted-foreground">{totalSteps} Schritte</span>
|
||||
{#if completedRuns.length > 0}
|
||||
<span class="text-xs text-muted-foreground">✓ {completedRuns.length}× abgeschlossen</span>
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="flex-shrink-0 text-5xl">{guide.coverEmoji ?? '📖'}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-xl font-bold text-foreground leading-tight">{guide.title}</h1>
|
||||
{#if guide.description}
|
||||
<p class="mt-1 text-sm text-muted-foreground">{guide.description}</p>
|
||||
{/if}
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2">
|
||||
<span class="rounded-full px-2 py-0.5 text-xs font-medium {difficultyConfig[guide.difficulty].color}">
|
||||
{difficultyConfig[guide.difficulty].label}
|
||||
</span>
|
||||
{#if guide.estimatedMinutes}
|
||||
<span class="text-xs text-muted-foreground">⏱ {guide.estimatedMinutes}min</span>
|
||||
{/if}
|
||||
<span class="text-xs text-muted-foreground">{steps.length} Schritte</span>
|
||||
{#if completedRuns.length > 0}
|
||||
<span class="text-xs text-muted-foreground">✓ {completedRuns.length}× abgeschlossen</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
onclick={() => (showEditModal = true)}
|
||||
class="rounded-lg p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
aria-label="Bearbeiten"
|
||||
>✏️</button>
|
||||
{#if editMode}
|
||||
<div class="flex flex-shrink-0 gap-1">
|
||||
<button
|
||||
onclick={() => (showGuideEditModal = true)}
|
||||
class="rounded-lg p-2 text-sm text-muted-foreground hover:bg-surface"
|
||||
title="Guide-Einstellungen"
|
||||
>✏️</button>
|
||||
<button
|
||||
onclick={() => (showDeleteConfirm = true)}
|
||||
class="rounded-lg p-2 text-sm text-muted-foreground hover:bg-red-50 hover:text-red-600"
|
||||
title="Anleitung löschen"
|
||||
>🗑</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active run banner -->
|
||||
{#if activeRun}
|
||||
{#if activeRun && !editMode}
|
||||
<div class="mb-6 rounded-xl border border-primary/30 bg-primary/5 p-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-primary">Aktiver Durchlauf</span>
|
||||
<span class="text-xs text-muted-foreground">{getActiveRunProgress()}% abgeschlossen</span>
|
||||
<span class="text-xs text-muted-foreground">{getActiveRunProgress()}%</span>
|
||||
</div>
|
||||
<div class="mb-3 h-1.5 overflow-hidden rounded-full bg-primary/20">
|
||||
<div
|
||||
class="h-full rounded-full bg-primary transition-all"
|
||||
style="width: {getActiveRunProgress()}%"
|
||||
></div>
|
||||
<div class="h-full rounded-full bg-primary transition-all" style="width: {getActiveRunProgress()}%"></div>
|
||||
</div>
|
||||
<button
|
||||
onclick={continueRun}
|
||||
class="w-full rounded-lg bg-primary px-4 py-2.5 text-sm font-medium text-white hover:bg-primary-hover"
|
||||
>
|
||||
<button onclick={continueRun} class="w-full rounded-lg bg-primary px-4 py-2.5 text-sm font-medium text-white hover:bg-primary-hover">
|
||||
Fortsetzen →
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Start run buttons -->
|
||||
{:else if !editMode}
|
||||
<div class="mb-8 flex gap-3">
|
||||
<button
|
||||
onclick={() => startRun('scroll')}
|
||||
class="flex-1 rounded-xl bg-primary px-4 py-3 text-sm font-semibold text-white hover:bg-primary-hover"
|
||||
disabled={steps.length === 0}
|
||||
class="flex-1 rounded-xl bg-primary px-4 py-3 text-sm font-semibold text-white hover:bg-primary-hover disabled:opacity-40"
|
||||
>
|
||||
▶ Durchlauf starten
|
||||
</button>
|
||||
<button
|
||||
onclick={() => startRun('focus')}
|
||||
class="rounded-xl border border-border px-4 py-3 text-sm font-medium text-foreground hover:bg-accent"
|
||||
disabled={steps.length === 0}
|
||||
class="rounded-xl border border-border px-4 py-3 text-sm font-medium text-foreground hover:bg-accent disabled:opacity-40"
|
||||
title="Fokus-Modus: ein Schritt auf einmal"
|
||||
>
|
||||
🎯 Fokus
|
||||
|
|
@ -168,81 +261,157 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Steps -->
|
||||
<!-- Steps + Edit controls -->
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Sections with steps -->
|
||||
{#if sections.length > 0}
|
||||
{#each sections as section (section.id)}
|
||||
{@const sectionSteps = getStepsForSection(section.id)}
|
||||
<div>
|
||||
<h2 class="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{section.title}
|
||||
</h2>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{section.title}
|
||||
</h2>
|
||||
{#if editMode}
|
||||
<button
|
||||
onclick={() => deleteSection(section.id)}
|
||||
class="ml-auto text-xs text-red-400 hover:text-red-600"
|
||||
title="Abschnitt löschen"
|
||||
>✕</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
{#each getStepsForSection(section.id) as step (step.id)}
|
||||
{#each sectionSteps as step, i (step.id)}
|
||||
{@const cfg = stepTypeConfig[step.type]}
|
||||
<div class="rounded-lg border-l-2 bg-surface px-4 py-3 {cfg.color}">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="mt-0.5 text-xs font-mono text-muted-foreground">{cfg.icon}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-foreground">{step.title}</p>
|
||||
{#if step.content}
|
||||
<p class="mt-1 text-xs text-muted-foreground whitespace-pre-wrap">{step.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="group flex items-start gap-2 rounded-xl border-l-2 px-4 py-3 {cfg.border} {cfg.bg} bg-surface">
|
||||
<span class="mt-0.5 w-5 flex-shrink-0 text-center text-xs font-mono text-muted-foreground">{cfg.icon}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-foreground">{step.title}</p>
|
||||
{#if step.content}
|
||||
<p class="mt-1 text-xs text-muted-foreground whitespace-pre-wrap line-clamp-2">{step.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if editMode}
|
||||
<div class="flex flex-shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button onclick={() => moveStep(step, 'up')} disabled={i === 0} class="p-1 text-muted-foreground hover:text-foreground disabled:opacity-20">↑</button>
|
||||
<button onclick={() => moveStep(step, 'down')} disabled={i === sectionSteps.length - 1} class="p-1 text-muted-foreground hover:text-foreground disabled:opacity-20">↓</button>
|
||||
<button onclick={() => openEditStep(step)} class="p-1 text-muted-foreground hover:text-foreground">✏</button>
|
||||
<button onclick={() => deleteStep(step.id)} class="p-1 text-red-400 hover:text-red-600">✕</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if editMode}
|
||||
<button
|
||||
onclick={() => openAddStep(section.id)}
|
||||
class="flex w-full items-center gap-2 rounded-xl border border-dashed border-border px-4 py-3 text-sm text-muted-foreground transition-colors hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
<span class="text-lg leading-none">+</span>
|
||||
Schritt hinzufügen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if getUnsectionedSteps().length > 0}
|
||||
<div class="space-y-2">
|
||||
{#each getUnsectionedSteps() as step (step.id)}
|
||||
{@const cfg = stepTypeConfig[step.type]}
|
||||
<div class="rounded-lg border-l-2 bg-surface px-4 py-3 {cfg.color}">
|
||||
<p class="text-sm font-medium text-foreground">{step.title}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Unsectioned steps -->
|
||||
{#if getUnsectionedSteps().length > 0 || (sections.length === 0)}
|
||||
{@const unsectioned = getUnsectionedSteps()}
|
||||
<div class="space-y-2">
|
||||
{#each steps as step (step.id)}
|
||||
{#each unsectioned as step, i (step.id)}
|
||||
{@const cfg = stepTypeConfig[step.type]}
|
||||
<div class="rounded-lg border-l-2 bg-surface px-4 py-3 {cfg.color}">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="mt-0.5 text-xs font-mono text-muted-foreground">{cfg.icon}</span>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-foreground">{step.title}</p>
|
||||
{#if step.content}
|
||||
<p class="mt-1 text-xs text-muted-foreground whitespace-pre-wrap">{step.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="group flex items-start gap-2 rounded-xl border-l-2 px-4 py-3 {cfg.border} {cfg.bg} bg-surface">
|
||||
<span class="mt-0.5 w-5 flex-shrink-0 text-center text-xs font-mono text-muted-foreground">{cfg.icon}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-foreground">{step.title}</p>
|
||||
{#if step.content}
|
||||
<p class="mt-1 text-xs text-muted-foreground whitespace-pre-wrap line-clamp-2">{step.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if editMode}
|
||||
<div class="flex flex-shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button onclick={() => moveStep(step, 'up')} disabled={i === 0} class="p-1 text-muted-foreground hover:text-foreground disabled:opacity-20">↑</button>
|
||||
<button onclick={() => moveStep(step, 'down')} disabled={i === unsectioned.length - 1} class="p-1 text-muted-foreground hover:text-foreground disabled:opacity-20">↓</button>
|
||||
<button onclick={() => openEditStep(step)} class="p-1 text-muted-foreground hover:text-foreground">✏</button>
|
||||
<button onclick={() => deleteStep(step.id)} class="p-1 text-red-400 hover:text-red-600">✕</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if editMode}
|
||||
<button
|
||||
onclick={() => openAddStep()}
|
||||
class="flex w-full items-center gap-2 rounded-xl border border-dashed border-border px-4 py-3 text-sm text-muted-foreground transition-colors hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
<span class="text-lg leading-none">+</span>
|
||||
Schritt hinzufügen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Empty state -->
|
||||
{#if steps.length === 0 && !editMode}
|
||||
<div class="rounded-xl border border-dashed border-border py-12 text-center">
|
||||
<p class="mb-3 text-sm text-muted-foreground">Noch keine Schritte</p>
|
||||
<button
|
||||
onclick={() => (editMode = true)}
|
||||
class="text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
Bearbeiten um Schritte hinzuzufügen →
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Edit mode: add section / add step (top-level) -->
|
||||
{#if editMode}
|
||||
<div class="mt-2 space-y-2 border-t border-border pt-4">
|
||||
{#if showAddSection}
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newSectionTitle}
|
||||
placeholder="Abschnitt-Titel"
|
||||
autofocus
|
||||
onkeydown={(e) => { 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"
|
||||
/>
|
||||
<button onclick={addSection} class="rounded-xl bg-primary px-4 py-2 text-sm text-white hover:bg-primary-hover">OK</button>
|
||||
<button onclick={() => { showAddSection = false; newSectionTitle = ''; }} class="rounded-xl border border-border px-3 py-2 text-sm text-muted-foreground">✕</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => (showAddSection = true)}
|
||||
class="flex w-full items-center gap-2 rounded-xl border border-dashed border-border px-4 py-3 text-sm text-muted-foreground transition-colors hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
<span class="text-lg leading-none">⊞</span>
|
||||
Abschnitt hinzufügen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Run history -->
|
||||
{#if completedRuns.length > 0}
|
||||
<div class="mt-10">
|
||||
{#if completedRuns.length > 0 && !editMode}
|
||||
<div class="mt-10 border-t border-border pt-6">
|
||||
<h2 class="mb-3 text-sm font-semibold text-foreground">Verlauf</h2>
|
||||
<div class="space-y-2">
|
||||
{#each completedRuns.slice(0, 5) as run (run.id)}
|
||||
{@const doneCount = Object.values(run.stepStates).filter((s) => s.done).length}
|
||||
<div class="flex items-center justify-between rounded-lg bg-surface px-4 py-2.5 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-green-500">✓</span>
|
||||
<span class="text-foreground">
|
||||
{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' })}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{Object.values(run.stepStates).filter((s) => s.done).length}/{totalSteps} Schritte
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">{doneCount}/{checkableSteps.length} Schritte · {run.mode === 'focus' ? '🎯' : '📜'}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -250,3 +419,54 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modals -->
|
||||
{#if showGuideEditModal && guide}
|
||||
<GuideEditModal
|
||||
open={true}
|
||||
{guide}
|
||||
onClose={() => (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}
|
||||
<StepEditorModal
|
||||
open={true}
|
||||
step={editingStep}
|
||||
{guideId}
|
||||
sectionId={stepModalSectionId}
|
||||
order={0}
|
||||
onClose={() => (stepModalOpen = false)}
|
||||
onSave={handleSaveStep}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Delete confirm -->
|
||||
{#if showDeleteConfirm}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" onmousedown={(e) => e.target === e.currentTarget && (showDeleteConfirm = false)}>
|
||||
<div class="w-full max-w-sm rounded-2xl bg-background p-6 shadow-xl">
|
||||
<h3 class="mb-2 text-lg font-semibold text-foreground">Anleitung löschen?</h3>
|
||||
<p class="mb-6 text-sm text-muted-foreground">
|
||||
<strong>„{guide?.title}"</strong> und alle Schritte werden unwiderruflich gelöscht.
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<button onclick={() => (showDeleteConfirm = false)} class="flex-1 rounded-xl border border-border py-2.5 text-sm text-foreground hover:bg-accent">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button onclick={handleDeleteGuide} class="flex-1 rounded-xl bg-red-500 py-2.5 text-sm font-medium text-white hover:bg-red-600">
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue