feat(frist): Deadline-Countdown site-weit + variant-Styling (urgent=rot)
- DeadlineBannerBlock: Farbe nach variant (urgent rot / accent / info / default) - Layout: deadline-banner-auslegung-2026 site-weit im Bootstrap geladen + oben gerendert (vorher nur auf /regionalplan) - page-regionalplan: inline-Banner entfernt (kein Doppel), Content folgt separat Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,20 @@
|
||||
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
|
||||
const showCountdown = $derived(block.showCountdown !== false);
|
||||
|
||||
// Farbgebung nach variant — "urgent" fällt auf (rot), damit die Frist wirkt.
|
||||
const styles = $derived.by(() => {
|
||||
switch (block.variant) {
|
||||
case "urgent":
|
||||
return { bar: "bg-fire-600", text: "text-white", link: "text-white hover:text-white/80" };
|
||||
case "accent":
|
||||
return { bar: "bg-himmel-100", text: "text-himmel-900", link: "text-himmel-900 hover:text-himmel-700" };
|
||||
case "info":
|
||||
return { bar: "bg-wald-50", text: "text-wald-900", link: "text-wald-900 hover:text-wald-700" };
|
||||
default:
|
||||
return { bar: "bg-gray-50", text: "text-gray-900", link: "text-gray-900 hover:text-gray-700" };
|
||||
}
|
||||
});
|
||||
|
||||
let dismissed = $state(false);
|
||||
|
||||
type ResolvedItem = {
|
||||
@@ -116,10 +130,10 @@
|
||||
data-block-slug={block._slug}
|
||||
>
|
||||
<div
|
||||
class="gradient-blob-bg bg-gray-50 py-2.5"
|
||||
class="gradient-blob-bg {styles.bar} py-2.5"
|
||||
>
|
||||
<div class="container-custom flex items-center gap-x-6">
|
||||
<p class="my-0! flex-1 text-sm/6 text-gray-900">
|
||||
<p class="my-0! flex-1 text-sm/6 {styles.text}">
|
||||
{sentence}{#if countdownStr}{sentence ? " · " : ""}<span
|
||||
class="font-semibold">{countdownStr}</span
|
||||
>{/if}{#if href}
|
||||
@@ -127,7 +141,7 @@
|
||||
{href}
|
||||
target={newTab ? "_blank" : undefined}
|
||||
rel={newTab ? "noopener noreferrer" : undefined}
|
||||
class="font-semibold whitespace-nowrap text-gray-900 hover:text-gray-700"
|
||||
class="font-semibold whitespace-nowrap {styles.link}"
|
||||
>
|
||||
{ctaLabel} <Icon
|
||||
icon="lucide:arrow-right"
|
||||
@@ -146,7 +160,7 @@
|
||||
<span class="sr-only">Schließen</span>
|
||||
<Icon
|
||||
icon="lucide:x"
|
||||
class="size-5 text-gray-900"
|
||||
class="size-5 {styles.text}"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
PageConfigEntry,
|
||||
TopBannerEntry,
|
||||
} from '$lib/cms';
|
||||
import type { DeadlineBannerBlockData } from '$lib/block-types';
|
||||
import { ensureTransformedImage } from '$lib/rusty-image';
|
||||
import {
|
||||
DEFAULT_SOCIAL_IMAGE_URL,
|
||||
@@ -89,13 +90,14 @@ export const load: LayoutServerLoad = async ({ locals, route }) => {
|
||||
let bootstrapFooter: FooterEntry | null = null;
|
||||
let bootstrapConfig: PageConfigEntry | null = null;
|
||||
let bootstrapTopBanner: TopBannerEntry | null = null;
|
||||
let bootstrapDeadlineBanner: DeadlineBannerBlockData | null = null;
|
||||
try {
|
||||
// TTL-Cache über den ganzen Batch — lief vorher ungecacht bei jedem
|
||||
// SSR-Request. CSV-Key-Head: Webhook-Purge jeder beteiligten Collection
|
||||
// invalidiert den Eintrag (siehe invalidateCollection in cms.ts).
|
||||
const batch = await cached(
|
||||
'navigation',
|
||||
'navigation,footer,page_config,top_banner:layout-bootstrap:de',
|
||||
'navigation,footer,page_config,top_banner,deadline_banner:layout-bootstrap:de',
|
||||
() => batchFetch([
|
||||
{
|
||||
id: 'navHeader',
|
||||
@@ -132,12 +134,20 @@ export const load: LayoutServerLoad = async ({ locals, route }) => {
|
||||
collection: 'top_banner',
|
||||
slug: 'site-announcement',
|
||||
},
|
||||
{
|
||||
id: 'deadlineBanner',
|
||||
collection: 'deadline_banner',
|
||||
slug: 'deadline-banner-auslegung-2026',
|
||||
locale: 'de',
|
||||
resolve: 'all',
|
||||
},
|
||||
]));
|
||||
bootstrapNavHeader = batchData<NavigationEntry>(batch.results.navHeader);
|
||||
bootstrapNavSocial = batchData<NavigationEntry>(batch.results.navSocial);
|
||||
bootstrapFooter = batchData<FooterEntry>(batch.results.footer);
|
||||
bootstrapConfig = batchData<PageConfigEntry>(batch.results.config);
|
||||
bootstrapTopBanner = batchData<TopBannerEntry>(batch.results.topBanner);
|
||||
bootstrapDeadlineBanner = batchData<DeadlineBannerBlockData>(batch.results.deadlineBanner);
|
||||
} catch (err) {
|
||||
/* CMS nicht erreichbar — alle Felder bleiben null, Page rendert ohne. */
|
||||
logWarn('layout.bootstrap', err);
|
||||
@@ -405,5 +415,6 @@ export const load: LayoutServerLoad = async ({ locals, route }) => {
|
||||
analyticsPm86SiteId:
|
||||
(pageConfig as { analyticsPm86SiteId?: string } | null)?.analyticsPm86SiteId ?? null,
|
||||
announcementBanner: bootstrapTopBanner,
|
||||
deadlineBanner: bootstrapDeadlineBanner,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import TopBanner from '$lib/components/TopBanner.svelte';
|
||||
import AnnouncementBanner from '$lib/components/AnnouncementBanner.svelte';
|
||||
import DeadlineBannerBlock from '$lib/components/blocks/DeadlineBannerBlock.svelte';
|
||||
import TranslationProvider from '$lib/components/TranslationProvider.svelte';
|
||||
import Breadcrumbs from '$lib/ui/Breadcrumbs.svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
@@ -328,6 +329,9 @@
|
||||
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:bg-white focus:px-3 focus:py-2 focus:shadow focus:outline focus:outline-2 focus:outline-wald-600 focus:rounded-xs"
|
||||
>Zum Inhalt springen</a>
|
||||
<div class="flex min-h-screen flex-col text-stein-900">
|
||||
{#if data.deadlineBanner}
|
||||
<DeadlineBannerBlock block={data.deadlineBanner} />
|
||||
{/if}
|
||||
{#if data.announcementBanner}
|
||||
<AnnouncementBanner banner={data.announcementBanner} />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user