Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a490b1ae1 | |||
| f7529d3b47 |
@@ -1,15 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getBlockLayoutClasses } from "$lib/block-layout";
|
import { getBlockLayoutClasses } from "$lib/block-layout";
|
||||||
import type { DeadlineBannerBlockData } from "$lib/block-types";
|
import type { DeadlineBannerBlockData } from "$lib/block-types";
|
||||||
import Icon from "@iconify/svelte";
|
|
||||||
import "$lib/iconify-offline";
|
|
||||||
|
|
||||||
let { block }: { block: DeadlineBannerBlockData } = $props();
|
let { block }: { block: DeadlineBannerBlockData } = $props();
|
||||||
|
|
||||||
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
|
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
|
||||||
const showCountdown = $derived(block.showCountdown !== false);
|
const showCountdown = $derived(block.showCountdown !== false);
|
||||||
|
|
||||||
let dismissed = $state(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 = {
|
type ResolvedItem = {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -102,56 +113,37 @@
|
|||||||
const sentence = $derived.by(() => {
|
const sentence = $derived.by(() => {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (text) parts.push(text);
|
if (text) parts.push(text);
|
||||||
if (dateStr) parts.push(`am ${dateStr}`);
|
// 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(" ");
|
return parts.join(" ");
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasContent = $derived(!!(sentence || countdownStr));
|
const hasContent = $derived(!!(sentence || countdownStr));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if hasContent && !dismissed}
|
{#if hasContent}
|
||||||
<div
|
<div
|
||||||
class="deadline-banner {layoutClasses}"
|
class="deadline-banner {layoutClasses} w-full {barClass} text-white print:hidden"
|
||||||
data-block="DeadlineBanner" data-block-type="deadline_banner"
|
data-block="DeadlineBanner" data-block-type="deadline_banner"
|
||||||
data-block-slug={block._slug}
|
data-block-slug={block._slug}
|
||||||
|
role="note"
|
||||||
|
style="font-size: 0.7rem;"
|
||||||
>
|
>
|
||||||
<div
|
<div class="container-custom flex flex-wrap items-center justify-center gap-x-3 gap-y-1.5 py-2 text-center">
|
||||||
class="gradient-blob-bg bg-gray-50 py-2.5"
|
{#if sentence}
|
||||||
>
|
<span>{sentence}</span>
|
||||||
<div class="container-custom flex items-center gap-x-6">
|
{/if}
|
||||||
<p class="my-0! flex-1 text-sm/6 text-gray-900">
|
{#if countdownStr}
|
||||||
{sentence}{#if countdownStr}{sentence ? " · " : ""}<span
|
<span class="whitespace-nowrap rounded-full bg-black/20 px-2 py-0.5 font-bold">{countdownStr}</span>
|
||||||
class="font-semibold">{countdownStr}</span
|
{/if}
|
||||||
>{/if}{#if href}
|
{#if href}
|
||||||
{" "}<a
|
<a
|
||||||
{href}
|
{href}
|
||||||
target={newTab ? "_blank" : undefined}
|
target={newTab ? "_blank" : undefined}
|
||||||
rel={newTab ? "noopener noreferrer" : undefined}
|
rel={newTab ? "noopener noreferrer" : undefined}
|
||||||
class="font-semibold whitespace-nowrap text-gray-900 hover:text-gray-700"
|
class="whitespace-nowrap font-semibold underline underline-offset-2 hover:no-underline"
|
||||||
>
|
>{ctaLabel} →</a>
|
||||||
{ctaLabel} <Icon
|
|
||||||
icon="lucide:arrow-right"
|
|
||||||
class="inline-block size-4 align-[-0.15em]"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
|
||||||
<div class="shrink-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={() => (dismissed = true)}
|
|
||||||
class="-m-3 p-3 focus-visible:-outline-offset-4"
|
|
||||||
>
|
|
||||||
<span class="sr-only">Schließen</span>
|
|
||||||
<Icon
|
|
||||||
icon="lucide:x"
|
|
||||||
class="size-5 text-gray-900"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
PageConfigEntry,
|
PageConfigEntry,
|
||||||
TopBannerEntry,
|
TopBannerEntry,
|
||||||
} from '$lib/cms';
|
} from '$lib/cms';
|
||||||
|
import type { DeadlineBannerBlockData } from '$lib/block-types';
|
||||||
import { ensureTransformedImage } from '$lib/rusty-image';
|
import { ensureTransformedImage } from '$lib/rusty-image';
|
||||||
import {
|
import {
|
||||||
DEFAULT_SOCIAL_IMAGE_URL,
|
DEFAULT_SOCIAL_IMAGE_URL,
|
||||||
@@ -89,13 +90,14 @@ export const load: LayoutServerLoad = async ({ locals, route }) => {
|
|||||||
let bootstrapFooter: FooterEntry | null = null;
|
let bootstrapFooter: FooterEntry | null = null;
|
||||||
let bootstrapConfig: PageConfigEntry | null = null;
|
let bootstrapConfig: PageConfigEntry | null = null;
|
||||||
let bootstrapTopBanner: TopBannerEntry | null = null;
|
let bootstrapTopBanner: TopBannerEntry | null = null;
|
||||||
|
let bootstrapDeadlineBanner: DeadlineBannerBlockData | null = null;
|
||||||
try {
|
try {
|
||||||
// TTL-Cache über den ganzen Batch — lief vorher ungecacht bei jedem
|
// TTL-Cache über den ganzen Batch — lief vorher ungecacht bei jedem
|
||||||
// SSR-Request. CSV-Key-Head: Webhook-Purge jeder beteiligten Collection
|
// SSR-Request. CSV-Key-Head: Webhook-Purge jeder beteiligten Collection
|
||||||
// invalidiert den Eintrag (siehe invalidateCollection in cms.ts).
|
// invalidiert den Eintrag (siehe invalidateCollection in cms.ts).
|
||||||
const batch = await cached(
|
const batch = await cached(
|
||||||
'navigation',
|
'navigation',
|
||||||
'navigation,footer,page_config,top_banner:layout-bootstrap:de',
|
'navigation,footer,page_config,top_banner,deadline_banner:layout-bootstrap:de',
|
||||||
() => batchFetch([
|
() => batchFetch([
|
||||||
{
|
{
|
||||||
id: 'navHeader',
|
id: 'navHeader',
|
||||||
@@ -132,12 +134,20 @@ export const load: LayoutServerLoad = async ({ locals, route }) => {
|
|||||||
collection: 'top_banner',
|
collection: 'top_banner',
|
||||||
slug: 'site-announcement',
|
slug: 'site-announcement',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'deadlineBanner',
|
||||||
|
collection: 'deadline_banner',
|
||||||
|
slug: 'deadline-banner-auslegung-2026',
|
||||||
|
locale: 'de',
|
||||||
|
resolve: 'all',
|
||||||
|
},
|
||||||
]));
|
]));
|
||||||
bootstrapNavHeader = batchData<NavigationEntry>(batch.results.navHeader);
|
bootstrapNavHeader = batchData<NavigationEntry>(batch.results.navHeader);
|
||||||
bootstrapNavSocial = batchData<NavigationEntry>(batch.results.navSocial);
|
bootstrapNavSocial = batchData<NavigationEntry>(batch.results.navSocial);
|
||||||
bootstrapFooter = batchData<FooterEntry>(batch.results.footer);
|
bootstrapFooter = batchData<FooterEntry>(batch.results.footer);
|
||||||
bootstrapConfig = batchData<PageConfigEntry>(batch.results.config);
|
bootstrapConfig = batchData<PageConfigEntry>(batch.results.config);
|
||||||
bootstrapTopBanner = batchData<TopBannerEntry>(batch.results.topBanner);
|
bootstrapTopBanner = batchData<TopBannerEntry>(batch.results.topBanner);
|
||||||
|
bootstrapDeadlineBanner = batchData<DeadlineBannerBlockData>(batch.results.deadlineBanner);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
/* CMS nicht erreichbar — alle Felder bleiben null, Page rendert ohne. */
|
/* CMS nicht erreichbar — alle Felder bleiben null, Page rendert ohne. */
|
||||||
logWarn('layout.bootstrap', err);
|
logWarn('layout.bootstrap', err);
|
||||||
@@ -405,5 +415,6 @@ export const load: LayoutServerLoad = async ({ locals, route }) => {
|
|||||||
analyticsPm86SiteId:
|
analyticsPm86SiteId:
|
||||||
(pageConfig as { analyticsPm86SiteId?: string } | null)?.analyticsPm86SiteId ?? null,
|
(pageConfig as { analyticsPm86SiteId?: string } | null)?.analyticsPm86SiteId ?? null,
|
||||||
announcementBanner: bootstrapTopBanner,
|
announcementBanner: bootstrapTopBanner,
|
||||||
|
deadlineBanner: bootstrapDeadlineBanner,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import Footer from '$lib/components/Footer.svelte';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
import TopBanner from '$lib/components/TopBanner.svelte';
|
import TopBanner from '$lib/components/TopBanner.svelte';
|
||||||
import AnnouncementBanner from '$lib/components/AnnouncementBanner.svelte';
|
import AnnouncementBanner from '$lib/components/AnnouncementBanner.svelte';
|
||||||
|
import DeadlineBannerBlock from '$lib/components/blocks/DeadlineBannerBlock.svelte';
|
||||||
import TranslationProvider from '$lib/components/TranslationProvider.svelte';
|
import TranslationProvider from '$lib/components/TranslationProvider.svelte';
|
||||||
import Breadcrumbs from '$lib/ui/Breadcrumbs.svelte';
|
import Breadcrumbs from '$lib/ui/Breadcrumbs.svelte';
|
||||||
import type { LayoutData } from './$types';
|
import type { LayoutData } from './$types';
|
||||||
@@ -25,6 +26,19 @@
|
|||||||
*/
|
*/
|
||||||
const isEmbed = $derived(($page.route.id ?? '').startsWith('/(embed)'));
|
const isEmbed = $derived(($page.route.id ?? '').startsWith('/(embed)'));
|
||||||
|
|
||||||
|
// Nur EIN Balken über der Navi: aktive Frist hat Vorrang, sonst NEU-Banner.
|
||||||
|
const deadlineActive = $derived.by(() => {
|
||||||
|
const b = data.deadlineBanner as { mode?: string; date?: string; items?: unknown[] } | null;
|
||||||
|
if (!b) return false;
|
||||||
|
if (b.mode === 'auto') return (b.items ?? []).length > 0;
|
||||||
|
if (!b.date) return false;
|
||||||
|
const d = new Date(b.date);
|
||||||
|
if (Number.isNaN(d.getTime())) return false;
|
||||||
|
const today = new Date(); today.setHours(0, 0, 0, 0);
|
||||||
|
const t = new Date(d); t.setHours(0, 0, 0, 0);
|
||||||
|
return t.getTime() >= today.getTime();
|
||||||
|
});
|
||||||
|
|
||||||
// Live-preview reload bridge: when the page was loaded with
|
// Live-preview reload bridge: when the page was loaded with
|
||||||
// `?preview_draft=...`, accept `rustycms:reload` postMessage events from the
|
// `?preview_draft=...`, accept `rustycms:reload` postMessage events from the
|
||||||
// admin iframe parent and re-run the SvelteKit `load()` so the iframe
|
// admin iframe parent and re-run the SvelteKit `load()` so the iframe
|
||||||
@@ -328,7 +342,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"
|
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>
|
>Zum Inhalt springen</a>
|
||||||
<div class="flex min-h-screen flex-col text-stein-900">
|
<div class="flex min-h-screen flex-col text-stein-900">
|
||||||
{#if data.announcementBanner}
|
{#if deadlineActive && data.deadlineBanner}
|
||||||
|
<DeadlineBannerBlock block={data.deadlineBanner} />
|
||||||
|
{:else if data.announcementBanner}
|
||||||
<AnnouncementBanner banner={data.announcementBanner} />
|
<AnnouncementBanner banner={data.announcementBanner} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if pageData?.preview || pageData?.previewDraft}
|
{#if pageData?.preview || pageData?.previewDraft}
|
||||||
|
|||||||
Reference in New Issue
Block a user