7a490b1ae1
- Look wie AnnouncementBanner: kompakte zentrierte Leiste, weiße Schrift, 0.7rem, variant-Farbe (urgent=rot), flex-wrap → fließt/stackt sauber mobil - Countdown als dunkles Badge (bg-black/20) - Layout: aktive Frist verdrängt NEU-Banner → nie 2 Balken über der Navi Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
150 lines
5.1 KiB
Svelte
150 lines
5.1 KiB
Svelte
<script lang="ts">
|
|
import { getBlockLayoutClasses } from "$lib/block-layout";
|
|
import type { DeadlineBannerBlockData } from "$lib/block-types";
|
|
|
|
let { block }: { block: DeadlineBannerBlockData } = $props();
|
|
|
|
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
|
|
const showCountdown = $derived(block.showCountdown !== false);
|
|
|
|
// Look wie AnnouncementBanner (kompakte, zentrierte Leiste) — nur farbig.
|
|
// "urgent" = rot, damit die Frist wirkt.
|
|
const barClass = $derived.by(() => {
|
|
switch (block.variant) {
|
|
case "accent":
|
|
return "bg-himmel-600";
|
|
case "info":
|
|
return "bg-wald-600";
|
|
case "urgent":
|
|
return "bg-fire-600";
|
|
default:
|
|
return "bg-stein-700";
|
|
}
|
|
});
|
|
|
|
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: ResolvedItem; ts: number } => ({
|
|
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",
|
|
});
|
|
});
|
|
|
|
/** Kalendertage zwischen heute und Ziel — über Mitternacht-zu-
|
|
* Mitternacht-Diff, nicht roh per ms. Sonst zählt z. B. 30 h vor
|
|
* Termin als „2 Tage" obwohl der Termin morgen ist. */
|
|
const daysLeft = $derived.by(() => {
|
|
if (!dateObj) return null;
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const target = new Date(dateObj);
|
|
target.setHours(0, 0, 0, 0);
|
|
const diffDays = Math.round(
|
|
(target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24),
|
|
);
|
|
if (diffDays < 0) return null;
|
|
return diffDays;
|
|
});
|
|
|
|
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" && link.url) return link.url;
|
|
return "";
|
|
}
|
|
const href = $derived(getHref(linkValue));
|
|
const newTab = $derived(
|
|
linkValue && typeof linkValue === "object" ? !!linkValue.newTab : false,
|
|
);
|
|
const ctaLabel = $derived(
|
|
linkValue && typeof linkValue === "object" && linkValue.linkName
|
|
? linkValue.linkName
|
|
: "Mehr erfahren",
|
|
);
|
|
|
|
const sentence = $derived.by(() => {
|
|
const parts: string[] = [];
|
|
if (text) parts.push(text);
|
|
// Datum nur im Auto-Modus anhängen — im Manual-Text steht es meist schon.
|
|
if (dateStr && block.mode === "auto") parts.push(`am ${dateStr}`);
|
|
return parts.join(" ");
|
|
});
|
|
|
|
const hasContent = $derived(!!(sentence || countdownStr));
|
|
</script>
|
|
|
|
{#if hasContent}
|
|
<div
|
|
class="deadline-banner {layoutClasses} w-full {barClass} text-white print:hidden"
|
|
data-block="DeadlineBanner" data-block-type="deadline_banner"
|
|
data-block-slug={block._slug}
|
|
role="note"
|
|
style="font-size: 0.7rem;"
|
|
>
|
|
<div class="container-custom flex flex-wrap items-center justify-center gap-x-3 gap-y-1.5 py-2 text-center">
|
|
{#if sentence}
|
|
<span>{sentence}</span>
|
|
{/if}
|
|
{#if countdownStr}
|
|
<span class="whitespace-nowrap rounded-full bg-black/20 px-2 py-0.5 font-bold">{countdownStr}</span>
|
|
{/if}
|
|
{#if href}
|
|
<a
|
|
{href}
|
|
target={newTab ? "_blank" : undefined}
|
|
rel={newTab ? "noopener noreferrer" : undefined}
|
|
class="whitespace-nowrap font-semibold underline underline-offset-2 hover:no-underline"
|
|
>{ctaLabel} →</a>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|