feat(deadline-banner): auto mode pulls all calendar_items globally
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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user