feat: add FilesBlock interface and component integration
Deploy / verify (push) Successful in 46s
Deploy / deploy (push) Successful in 1m8s

- Introduced FilesBlockItem and FilesBlockData interfaces to support file handling in the CMS.
- Integrated FilesBlock component into ContentRows.svelte for rendering file blocks.
- Updated block type handling to include support for files, enhancing content versatility.
This commit is contained in:
Peter Meier
2026-04-22 10:23:20 +02:00
parent bedfe597ab
commit 5b5ec7c659
6 changed files with 481 additions and 0 deletions
+4
View File
@@ -5,6 +5,7 @@
import IframeBlock from "./blocks/IframeBlock.svelte";
import ImageBlock from "./blocks/ImageBlock.svelte";
import ImageGalleryBlock from "./blocks/ImageGalleryBlock.svelte";
import FilesBlock from "./blocks/FilesBlock.svelte";
import YoutubeVideoBlock from "./blocks/YoutubeVideoBlock.svelte";
import QuoteBlock from "./blocks/QuoteBlock.svelte";
import QuoteCarouselBlock from "./blocks/QuoteCarouselBlock.svelte";
@@ -27,6 +28,7 @@
IframeBlockData,
ImageBlockData,
ImageGalleryBlockData,
FilesBlockData,
YoutubeVideoBlockData,
QuoteBlockData,
QuoteCarouselBlockData,
@@ -112,6 +114,8 @@
<ImageBlock block={item as ImageBlockData} />
{:else if blockType(item) === "image_gallery"}
<ImageGalleryBlock block={item as ImageGalleryBlockData} />
{:else if blockType(item) === "files"}
<FilesBlock block={item as FilesBlockData} />
{:else if blockType(item) === "youtube_video"}
<YoutubeVideoBlock block={item as YoutubeVideoBlockData} />
{:else if blockType(item) === "quote"}
+126
View File
@@ -0,0 +1,126 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import { getBlockLayoutClasses } from "$lib/block-layout";
import type { FilesBlockData } from "$lib/block-types";
import {
extractCmsFileField,
fileExtFromUrl,
formatFileSize,
getCmsFileUrl,
} from "$lib/rusty-file";
let { block }: { block: FilesBlockData } = $props();
const layoutClasses = $derived(
block.layout ? getBlockLayoutClasses(block.layout) : "col-span-12 mb-4",
);
type Resolved = {
href: string;
label: string;
description?: string;
sizeLabel: string | null;
ext: string | null;
iconName: string;
};
function iconForExt(ext: string | null): string {
switch (ext) {
case "pdf":
return "mdi:file-pdf-box";
case "doc":
case "docx":
case "rtf":
case "odt":
return "mdi:file-word-box";
case "xls":
case "xlsx":
case "ods":
case "csv":
return "mdi:file-excel-box";
case "ppt":
case "pptx":
return "mdi:file-powerpoint-box";
case "zip":
case "tar":
case "gz":
case "7z":
return "mdi:folder-zip";
case "jpg":
case "jpeg":
case "png":
case "webp":
case "gif":
case "svg":
return "mdi:file-image";
case "txt":
case "md":
return "mdi:file-document-outline";
default:
return "mdi:file-outline";
}
}
const resolved = $derived(
((block.items ?? []) as unknown[])
.map((raw): Resolved | null => {
const field = extractCmsFileField(raw);
if (!field) return null;
const filename = field.filename ?? field.url.split("/").pop();
const href = getCmsFileUrl(field.url, {
filename,
download: !!block.forceDownload,
});
const ext = fileExtFromUrl(field.url);
const label = field.title ?? filename ?? field.url;
return {
href,
label,
description: field.description,
sizeLabel: formatFileSize(field.size),
ext,
iconName: iconForExt(ext),
};
})
.filter((r): r is Resolved => r !== null),
);
</script>
<div class="files-block {layoutClasses}" data-block-type="files" data-block-slug={block._slug}>
{#if block.headline}
<h3 class="mb-3 text-lg font-semibold">{block.headline}</h3>
{/if}
{#if block.description}
<p class="mb-3 text-sm text-zinc-700">{block.description}</p>
{/if}
{#if resolved.length === 0}
<p class="text-sm text-zinc-500">Keine Dateien verfügbar.</p>
{:else}
<ul class="flex flex-col gap-2">
{#each resolved as f}
<li>
<a
href={f.href}
class="group flex items-start gap-3 rounded-md border border-zinc-200 bg-white px-3 py-2 hover:border-zinc-400 hover:bg-zinc-50 transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={f.iconName} class="size-6 shrink-0 text-zinc-600" aria-hidden="true" />
<span class="flex flex-col min-w-0">
<span class="font-medium text-zinc-900 group-hover:underline truncate">{f.label}</span>
{#if f.description}
<span class="text-sm text-zinc-600">{f.description}</span>
{/if}
<span class="text-xs text-zinc-500">
{#if f.ext}<span class="uppercase">{f.ext}</span>{/if}
{#if f.ext && f.sizeLabel} · {/if}
{#if f.sizeLabel}{f.sizeLabel}{/if}
</span>
</span>
</a>
</li>
{/each}
</ul>
{/if}
</div>