feat(deadline-banner): auto mode pulls all calendar_items globally
Deploy / verify (push) Successful in 40s
Deploy / deploy (push) Successful in 59s

Auto-mode deadline_banner now resolves to the next future event across
all calendar_items in the CMS instead of requiring a manually-curated
items[] pool. Server load hooks (+page.server.ts for /, /[...slug],
/post/[slug]) call resolveDeadlineBannerBlocks which fetches the
global calendar_item list once and attaches it as items[] when mode
is "auto" and items[] is not already resolved. Component logic
unchanged — it still picks the soonest future entry.

Also:
- cms.ts: getCalendarItems() bulk list fetcher
- PostOverviewBlock.svelte: design defaults to "cards" when value is
  empty string (not just null/undefined) so existing entries with
  design: "" render

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-04-19 11:02:50 +02:00
parent ffd4f599cd
commit 62d25202a6
9 changed files with 242 additions and 1 deletions
+4
View File
@@ -14,6 +14,7 @@
import CalendarBlock from "./blocks/CalendarBlock.svelte";
import OrganisationsBlock from "./blocks/OrganisationsBlock.svelte";
import OpnFormBlock from "./blocks/OpnFormBlock.svelte";
import DeadlineBannerBlock from "./blocks/DeadlineBannerBlock.svelte";
import { isBlockLayoutBreakout, getSpaceBottomClass } from "$lib/block-layout";
import type { BlockLayout } from "$lib/block-layout";
import type { Translations } from "$lib/translations";
@@ -35,6 +36,7 @@
CalendarBlockData,
OrganisationsBlockData,
OpnFormBlockData,
DeadlineBannerBlockData,
} from "$lib/block-types";
let { layout, class: className = "", translations = {} }: { layout: RowContentLayout; class?: string; translations?: Translations | null } = $props();
@@ -128,6 +130,8 @@
<OrganisationsBlock block={item as OrganisationsBlockData} />
{:else if blockType(item) === "opnform"}
<OpnFormBlock block={item as OpnFormBlockData} />
{:else if blockType(item) === "deadline_banner"}
<DeadlineBannerBlock block={item as DeadlineBannerBlockData} />
{:else}
<div class="col-span-12 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
Unbekannter Block: <code>{blockType(item)}</code>
@@ -0,0 +1,135 @@
<script lang="ts">
import { getBlockLayoutClasses } from "$lib/block-layout";
import type { DeadlineBannerBlockData } from "$lib/block-types";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
let { block }: { block: DeadlineBannerBlockData } = $props();
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
const variant = $derived(block.variant ?? "accent");
const showCountdown = $derived(block.showCountdown !== false);
type ResolvedItem = {
title?: string;
terminZeit?: string;
description?: string;
link?: DeadlineBannerBlockData["link"];
};
const autoItem = $derived.by((): ResolvedItem | null => {
if (block.mode !== "auto") return null;
const now = Date.now();
const items = (block.items ?? [])
.filter(
(x): x is ResolvedItem =>
typeof x === "object" && x !== null && "terminZeit" in x,
)
.map((x) => ({ x, ts: Date.parse(x.terminZeit ?? "") }))
.filter(({ ts }) => !isNaN(ts) && ts >= now)
.sort((a, b) => a.ts - b.ts);
return items[0]?.x ?? null;
});
const text = $derived(
block.mode === "auto" ? autoItem?.title ?? "" : block.text ?? "",
);
const dateIso = $derived(
block.mode === "auto" ? autoItem?.terminZeit ?? "" : block.date ?? "",
);
const linkValue = $derived(
block.mode === "auto" ? autoItem?.link : block.link,
);
const dateObj = $derived.by(() => {
if (!dateIso) return null;
const d = new Date(dateIso);
return isNaN(d.getTime()) ? null : d;
});
const dateStr = $derived.by(() => {
if (!dateObj) return "";
return dateObj.toLocaleDateString("de-DE", {
day: "numeric",
month: "long",
});
});
const daysLeft = $derived.by(() => {
if (!dateObj) return null;
const ms = dateObj.getTime() - Date.now();
if (ms < 0) return null;
return Math.ceil(ms / (1000 * 60 * 60 * 24));
});
const countdownStr = $derived.by(() => {
if (!showCountdown || daysLeft == null) return "";
if (daysLeft === 0) return "Heute";
if (daysLeft === 1) return "Morgen";
return `Noch ${daysLeft} Tage`;
});
function getHref(link: DeadlineBannerBlockData["link"]): string {
if (typeof link === "string" && link) return link;
if (link && typeof link === "object") {
if (link.url) return link.url;
if (link._slug) return `/post/${encodeURIComponent(link._slug)}`;
}
return "";
}
const href = $derived(getHref(linkValue));
const newTab = $derived(
linkValue && typeof linkValue === "object" ? !!linkValue.newTab : false,
);
const variantClasses = $derived(
variant === "urgent"
? "bg-amber-100 text-amber-900 border-amber-300"
: variant === "info"
? "bg-sky-100 text-sky-900 border-sky-300"
: "bg-wald-100 text-wald-900 border-wald-300",
);
const iconName = $derived(
variant === "urgent" ? "mdi:alert-circle" : "mdi:calendar-clock",
);
const hasContent = $derived(!!(text || dateStr || countdownStr));
</script>
{#if hasContent}
<div
class="deadline-banner {layoutClasses}"
data-block-type="deadline_banner"
data-block-slug={block._slug}
>
<div class="border-y {variantClasses}">
<div class="container-custom flex flex-wrap items-center justify-center gap-x-3 gap-y-1 px-4 py-2 text-sm sm:text-base">
<Icon icon={iconName} class="size-5 shrink-0" aria-hidden="true" />
{#if block.label}
<span class="font-semibold">{block.label}:</span>
{/if}
{#if dateStr}
<span class="font-medium">{dateStr}</span>
{/if}
{#if text}
<span>{dateStr ? "·" : ""} {text}</span>
{/if}
{#if countdownStr}
<span class="rounded-full bg-white/60 px-2 py-0.5 text-xs font-semibold uppercase tracking-wide">
{countdownStr}
</span>
{/if}
{#if href}
<a
href={href}
target={newTab ? "_blank" : undefined}
rel={newTab ? "noopener noreferrer" : undefined}
class="ml-auto inline-flex items-center gap-1 underline-offset-2 hover:underline"
>
Mehr
<Icon icon="mdi:arrow-right" class="size-4" aria-hidden="true" />
</a>
{/if}
</div>
</div>
</div>
{/if}
@@ -18,7 +18,7 @@
);
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
const posts = $derived((block.postsResolved ?? []) as (PostEntry & { _resolvedImageUrl?: string })[]);
const design = $derived(block.design ?? "cards");
const design = $derived(block.design || "cards");
const tagBase = "/posts/tag";
</script>