diff --git a/apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte b/apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte index 2e3af3500..f822d34fd 100644 --- a/apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte +++ b/apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte @@ -1,7 +1,10 @@ -{#each topLevel as block (block.id)} +{#snippet renderBlock(block: WebsiteBlock)} {@const spec = getBlockSpec(block.type)} {#if spec} + {@const children = (byParent.get(block.id) ?? []).map(asRegistryBlock)} {#if mode === 'edit'}
onSelect?.(block.id)} + onclick={(e) => { + e.stopPropagation(); + onSelect?.(block.id); + }} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); + e.stopPropagation(); onSelect?.(block.id); } }} > - +
{:else}
- +
{/if} {:else if mode === 'edit'} @@ -58,6 +92,17 @@ Unbekannter Block-Typ: {block.type} {/if} +{/snippet} + +{#snippet renderInnerChild(child: BlockType)} + {@const fullBlock = blocks.find((b) => b.id === child.id)} + {#if fullBlock} + {@render renderBlock(fullBlock)} + {/if} +{/snippet} + +{#each topLevel as block (block.id)} + {@render renderBlock(block)} {/each} diff --git a/apps/mana/apps/web/src/lib/modules/website/components/ImageInspector.svelte b/apps/mana/apps/web/src/lib/modules/website/components/ImageInspector.svelte new file mode 100644 index 000000000..d5337c373 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/website/components/ImageInspector.svelte @@ -0,0 +1,217 @@ + + +
+
e.preventDefault()} + ondrop={onDrop} + onclick={() => fileInput?.click()} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + fileInput?.click(); + } + }} + > + {#if uploading} + Lade hoch… + {:else if block.props.url} + {block.props.altText} + Klicken / ziehen, um zu ersetzen + {:else} + Bild hier hinziehen oder klicken + {/if} +
+ + + {#if uploadError} +

{uploadError}

+ {/if} + + + + + + + +
+ + + +
+ + +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/website/components/SiteSettingsDialog.svelte b/apps/mana/apps/web/src/lib/modules/website/components/SiteSettingsDialog.svelte new file mode 100644 index 000000000..13f53dd26 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/website/components/SiteSettingsDialog.svelte @@ -0,0 +1,361 @@ + + +
e.key === 'Escape' && onClose()} + role="button" + tabindex="-1" + aria-label="Schließen" +>
+ + + + diff --git a/apps/mana/apps/web/src/lib/modules/website/upload.ts b/apps/mana/apps/web/src/lib/modules/website/upload.ts new file mode 100644 index 000000000..ce539de5b --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/website/upload.ts @@ -0,0 +1,78 @@ +/** + * Client-side upload helper for the website builder. + * + * Forwards the file to mana-media under `app=website` so uploads are + * scoped + listable in admin. Returns `{ mediaId, url }` — the block + * stores the full URL (simpler for the public renderer, which doesn't + * have auth and can't re-resolve a mediaId on every render). + * + * The `url` is the CDN-friendly `/file/large` variant by default. If a + * block needs a different size (gallery thumbnails), call + * `mediaFileUrl(mediaId, variant)` with 'small' / 'medium' / 'large' / + * 'original'. + */ + +import { browser } from '$app/environment'; + +function getMediaUrl(): string { + if (browser && typeof window !== 'undefined') { + const injected = (window as unknown as { __PUBLIC_MANA_MEDIA_URL__?: string }) + .__PUBLIC_MANA_MEDIA_URL__; + if (injected) return injected; + } + return ( + (import.meta as unknown as { env?: Record }).env?.PUBLIC_MANA_MEDIA_URL ?? + process.env.PUBLIC_MANA_MEDIA_URL ?? + 'http://localhost:3015' + ); +} + +export type MediaVariant = 'small' | 'medium' | 'large' | 'original'; + +export function mediaFileUrl(mediaId: string, variant: MediaVariant = 'large'): string { + return `${getMediaUrl()}/api/v1/media/${mediaId}/file/${variant}`; +} + +export interface UploadResult { + mediaId: string; + url: string; +} + +export class UploadError extends Error { + readonly status: number; + constructor(message: string, status: number) { + super(message); + this.name = 'UploadError'; + this.status = status; + } +} + +/** + * Upload an image file. Throws `UploadError` on non-2xx responses or + * non-image content. Caller is responsible for rendering the error. + */ +export async function uploadImage(file: File): Promise { + if (!file.type.startsWith('image/')) { + throw new UploadError('Bitte wähle ein Bild (PNG, JPG, WEBP, GIF).', 400); + } + if (file.size > 25 * 1024 * 1024) { + throw new UploadError('Datei zu groß (max 25 MB).', 400); + } + + const formData = new FormData(); + formData.append('file', file); + formData.append('app', 'website'); + + const res = await fetch(`${getMediaUrl()}/api/v1/media/upload`, { + method: 'POST', + body: formData, + }); + if (!res.ok) { + throw new UploadError(`Upload fehlgeschlagen (${res.status})`, res.status); + } + + const data = (await res.json()) as { id?: string }; + if (!data.id) throw new UploadError('Upload-Antwort ohne Media-ID', 500); + + return { mediaId: data.id, url: mediaFileUrl(data.id, 'large') }; +} diff --git a/apps/mana/apps/web/src/lib/modules/website/views/EditorView.svelte b/apps/mana/apps/web/src/lib/modules/website/views/EditorView.svelte index 3eeca26e5..7381fa3ca 100644 --- a/apps/mana/apps/web/src/lib/modules/website/views/EditorView.svelte +++ b/apps/mana/apps/web/src/lib/modules/website/views/EditorView.svelte @@ -13,6 +13,7 @@ import InsertPalette from '../components/InsertPalette.svelte'; import PageList from '../components/PageList.svelte'; import PublishBar from '../components/PublishBar.svelte'; + import SiteSettingsDialog from '../components/SiteSettingsDialog.svelte'; interface Props { siteId: string; @@ -30,6 +31,7 @@ const pageBlocks = $derived(blocksForPage(blocks.value, props.pageId)); let selectedBlockId = $state(null); + let showSettings = $state(false); const selectedBlock = $derived( selectedBlockId ? (pageBlocks.find((b) => b.id === selectedBlockId) ?? null) : null @@ -57,8 +59,19 @@