Files
windwiderstand/src/lib/components/blocks/CalendarCompactBlock.svelte
T
Peter Meier 5a1a49a97b
Deploy / verify (push) Successful in 1m47s
Deploy / deploy (push) Successful in 1m32s
feat(calendar): section_header (kicker/number/action/title) im Schema + wired
calendar-Schema extends section_header + title-Feld. CalendarBlockData +
kicker/number/action. CalendarCompact reicht kicker/number an SectionHeader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:20:33 +02:00

138 lines
5.2 KiB
Svelte

<script lang="ts">
import { getBlockLayoutClasses } from "$lib/block-layout";
import SectionHeader from "$lib/components/SectionHeader.svelte";
import type { CalendarBlockData, CalendarItemData } from "$lib/block-types";
import { t as tStatic, T } from "$lib/translations";
import type { Translations } from "$lib/translations";
import "$lib/iconify-offline";
import Icon from "@iconify/svelte";
let {
block,
translations = {},
}: { block: CalendarBlockData; translations?: Translations | null } = $props();
function t(key: string, replacements?: Record<string, string | number>) {
return tStatic(translations ?? null, key, replacements);
}
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
const limit = $derived(typeof block.limit === "number" && block.limit > 0 ? block.limit : 3);
/** Compact-Variante zeigt nur aufgelöste Items (Strings = unresolved Slugs → skip). */
const allItems = $derived(
(block.items ?? []).filter(
(x): x is CalendarItemData => typeof x === "object" && x !== null && "terminZeit" in x,
),
);
/** Nur zukünftige Termine, aufsteigend sortiert, auf Limit gekürzt. */
const upcoming = $derived(
allItems
.map((it) => ({ it, start: it.terminZeit ? new Date(it.terminZeit) : null }))
.filter((x): x is { it: CalendarItemData; start: Date } => {
if (!x.start || isNaN(x.start.getTime())) return false;
return x.start.getTime() >= Date.now() - 12 * 3600 * 1000; // 12h Toleranz
})
.sort((a, b) => a.start.getTime() - b.start.getTime())
.slice(0, limit),
);
function fmtDate(d: Date): string {
return d.toLocaleDateString("de-DE", {
weekday: "short",
day: "2-digit",
month: "long",
});
}
function fmtTime(d: Date): string {
const h = d.getHours();
const m = d.getMinutes();
if (h === 0 && m === 0) return "";
return d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
}
/** Titel + optional nachgestellte "(…)"-Klammer separat (Klammer gedimmt wie im Design). */
function splitTitle(raw: string): { main: string; paren: string } {
const m = raw.match(/^(.*?)\s*(\([^()]*\))\s*$/);
if (m) return { main: m[1], paren: m[2] };
return { main: raw, paren: "" };
}
function locationText(it: CalendarItemData): string {
if (!it.location) return "";
return typeof it.location === "string" ? it.location : ((it.location as { text?: string })?.text ?? "");
}
const linkHref = $derived(() => {
const lt = block.linkTo;
if (!lt) return null;
const slug = typeof lt === "string" ? lt : (lt._slug ?? "");
if (!slug) return null;
const clean = slug.replace(/^\/+|\/+$/g, "");
return clean ? `/${clean}/` : "/";
});
</script>
<div
class="not-prose {layoutClasses} w-full min-w-0"
data-block="CalendarCompact"
data-block-type="calendar"
data-block-variant="compact"
data-block-slug={block._slug}
>
<div class="mb-4">
<SectionHeader
number={block.number}
kicker={block.kicker}
title={block.title ?? t(T.calendar_next_events)}
tag="h3"
actionLabel={linkHref() ? t(T.calendar_all_upcoming) : undefined}
actionHref={linkHref() ?? undefined}
/>
</div>
{#if upcoming.length === 0}
<p class="rounded-lg border border-stein-200 px-3 py-6 text-center text-sm text-stein-500">
{t(T.calendar_no_future_events)}
</p>
{:else}
<ul class="divide-y divide-stein-100 overflow-hidden rounded-lg border border-stein-200 bg-white">
{#each upcoming as { it, start } (it._slug ?? it.title)}
{@const timeStr = fmtTime(start)}
{@const loc = locationText(it)}
{@const title = splitTitle(it.title ?? t(T.calendar_untitled))}
{@const base = linkHref()}
{@const itemHref = base && it._slug ? `${base}#event-${it._slug}` : null}
<li>
<svelte:element
this={itemHref ? "a" : "div"}
href={itemHref ?? undefined}
class="flex items-start gap-3 px-4 py-3 no-underline {itemHref ? 'transition-colors hover:bg-stein-50' : ''}"
>
<div class="flex w-12 shrink-0 flex-col items-center rounded-sm bg-stein-800 px-1 py-1.5 text-white">
<span class="text-[0.6rem] font-semibold uppercase leading-none tracking-wide">
{start.toLocaleDateString("de-DE", { month: "short" }).replace(".", "")}
</span>
<span class="mt-0.5 text-lg font-bold tabular-nums leading-none">
{start.getDate()}
</span>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold leading-snug text-stein-900">
{title.main}{#if title.paren}{" "}<span class="font-normal text-stein-400">{title.paren}</span>{/if}
</p>
<p class="mt-1 flex items-center gap-1.5 text-xs text-stein-500">
<Icon icon="lucide:clock" class="size-3.5 shrink-0" aria-hidden="true" />
<span class="min-w-0 truncate">
{fmtDate(start)}{timeStr ? ` · ${timeStr}` : ""}{loc ? ` · ${loc}` : ""}
</span>
</p>
</div>
</svelte:element>
</li>
{/each}
</ul>
{/if}
</div>