feat: event badges, map, accordion, icon components
Deploy to Firebase Hosting / deploy (push) Failing after 1m26s
Deploy to Firebase Hosting / deploy (push) Failing after 1m26s
- EventBadges + EventMap for post event date/location display - Accordion + Ico utility components - PostCard shows event badge overlay on image when post is event - post/[slug] renders event details with map - markdown hr styling (dotted) - disable Astro devToolbar - regenerated cms-api types (includes navgroup, event fields)
This commit is contained in:
@@ -11,6 +11,7 @@ const site = process.env.PUBLIC_SITE_URL || 'https://windwiderstand.de';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site,
|
site,
|
||||||
|
devToolbar: { enabled: false },
|
||||||
integrations: [
|
integrations: [
|
||||||
svelte(),
|
svelte(),
|
||||||
sitemap(),
|
sitemap(),
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import Ico from "./Ico.svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
label,
|
||||||
|
open = false,
|
||||||
|
id = undefined,
|
||||||
|
class: cls = "",
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
open?: boolean;
|
||||||
|
id?: string;
|
||||||
|
class?: string;
|
||||||
|
children: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<details {id} {open} class="pt-4 border-t border-zinc-200 group {cls}">
|
||||||
|
<summary class="flex cursor-pointer list-none items-center gap-2 text-sm font-medium select-none">
|
||||||
|
<Ico icon="mdi:chevron-right" class="size-4 shrink-0 transition-transform group-open:rotate-90" />
|
||||||
|
{label}
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "@iconify/svelte";
|
||||||
|
import "../lib/iconify-offline";
|
||||||
|
|
||||||
|
let {
|
||||||
|
eventDate = null,
|
||||||
|
locationText = null,
|
||||||
|
locationHref = null,
|
||||||
|
}: {
|
||||||
|
eventDate?: string | null;
|
||||||
|
locationText?: string | null;
|
||||||
|
locationHref?: string | null;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
function formatEventDate(dateStr: string): string | null {
|
||||||
|
try {
|
||||||
|
return new Date(dateStr).toLocaleDateString("de-DE", {
|
||||||
|
weekday: "short", day: "2-digit", month: "short",
|
||||||
|
year: "numeric", hour: "2-digit", minute: "2-digit",
|
||||||
|
});
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateLabel = $derived(eventDate ? formatEventDate(eventDate) : null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if dateLabel || locationText}
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#if dateLabel}
|
||||||
|
<span class="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full bg-wald-500 px-3 py-1 text-[.65rem] font-semibold text-white shadow-sm">
|
||||||
|
<Icon icon="mdi:calendar" class="size-3.5 shrink-0 opacity-90" aria-hidden="true" />
|
||||||
|
{dateLabel}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if locationText}
|
||||||
|
{#if locationHref}
|
||||||
|
<a href={locationHref} target={locationHref.startsWith('#') ? undefined : '_blank'} rel={locationHref.startsWith('#') ? undefined : 'noopener noreferrer'} class="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full bg-neutral-800 text-neutral-50! px-3 py-1 text-[.65rem] shadow-sm hover:bg-neutral-700 transition-colors no-underline!">
|
||||||
|
<Icon icon="mdi:map-marker" class="size-3.5 shrink-0 opacity-90" aria-hidden="true" />
|
||||||
|
{locationText}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<span class="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full bg-neutral-800 text-neutral-50 px-3 py-1 text-[.65rem] font-medium shadow-sm">
|
||||||
|
<Icon icon="mdi:map-marker" class="size-3.5 shrink-0 opacity-90" aria-hidden="true" />
|
||||||
|
{locationText}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "@iconify/svelte";
|
||||||
|
import "../lib/iconify-offline";
|
||||||
|
import Accordion from "./Accordion.svelte";
|
||||||
|
import { t, T, type Translations } from "../lib/translations";
|
||||||
|
|
||||||
|
let {
|
||||||
|
embedUrl = null,
|
||||||
|
linkUrl = null,
|
||||||
|
locationText = null,
|
||||||
|
label = "Karte",
|
||||||
|
translations = null,
|
||||||
|
}: {
|
||||||
|
embedUrl?: string | null;
|
||||||
|
linkUrl?: string | null;
|
||||||
|
locationText?: string | null;
|
||||||
|
label?: string;
|
||||||
|
translations?: Translations | null;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let overlayVisible = $state(true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Accordion id="map-section" {label} open class="mt-12">
|
||||||
|
<div class="overflow-hidden rounded-lg border border-wald-200 shadow-sm">
|
||||||
|
{#if embedUrl}
|
||||||
|
<div>
|
||||||
|
<div class="relative">
|
||||||
|
<iframe
|
||||||
|
src={embedUrl}
|
||||||
|
width="600"
|
||||||
|
height="300"
|
||||||
|
class="block w-full border-0"
|
||||||
|
loading="lazy"
|
||||||
|
title={locationText ? `Karte: ${locationText}` : "Veranstaltungsort"}
|
||||||
|
></iframe>
|
||||||
|
{#if overlayVisible}
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
class="absolute inset-0 flex items-center justify-center bg-black/20 backdrop-blur-[1px] cursor-pointer select-none"
|
||||||
|
aria-label="Karte aktivieren"
|
||||||
|
onclick={() => { overlayVisible = false; }}
|
||||||
|
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') overlayVisible = false; }}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 rounded-full bg-white/90 px-4 py-2 text-xs font-medium text-zinc-700 shadow">
|
||||||
|
<Icon icon="mdi:cursor-default-click" class="size-4 shrink-0" aria-hidden="true" />
|
||||||
|
{t(translations, T.post_map_activate)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if linkUrl}
|
||||||
|
<a
|
||||||
|
href={linkUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="flex items-center gap-1.5 px-3 py-1.5 bg-wald-50 text-wald-700 text-[.65rem] font-medium hover:bg-wald-100 transition-colors border-t border-wald-200"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:open-in-new" class="size-3.5 shrink-0" aria-hidden="true" />
|
||||||
|
{t(translations, T.post_map_open_osm)}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if linkUrl}
|
||||||
|
<a
|
||||||
|
href={linkUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="flex items-center gap-2 px-3 py-2 bg-wald-50 text-wald-700 text-xs font-medium hover:bg-wald-100 transition-colors"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:map-marker" class="size-4 shrink-0" aria-hidden="true" />
|
||||||
|
{locationText ?? t(translations, T.post_map_open_osm)}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Accordion>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "@iconify/svelte";
|
||||||
|
import "../lib/iconify-offline";
|
||||||
|
|
||||||
|
let {
|
||||||
|
icon,
|
||||||
|
class: cls = "",
|
||||||
|
}: {
|
||||||
|
icon: string;
|
||||||
|
class?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Icon {icon} class={cls} aria-hidden="true" />
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PostEntry } from "../lib/cms";
|
import type { PostEntry } from "../lib/cms";
|
||||||
import PostMeta from "./PostMeta.svelte";
|
import PostMeta from "./PostMeta.svelte";
|
||||||
|
import EventBadges from "./EventBadges.svelte";
|
||||||
|
|
||||||
type PostWithImage = PostEntry & { _resolvedImageUrl?: string };
|
type PostWithImage = PostEntry & { _resolvedImageUrl?: string };
|
||||||
|
|
||||||
@@ -15,13 +16,23 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const resolvedImg = $derived(post._resolvedImageUrl);
|
const resolvedImg = $derived(post._resolvedImageUrl);
|
||||||
|
|
||||||
|
type PostWithEvent = PostWithImage & {
|
||||||
|
isEvent?: boolean;
|
||||||
|
eventDate?: string;
|
||||||
|
eventLocation?: { text?: string };
|
||||||
|
};
|
||||||
|
const p = post as PostWithEvent;
|
||||||
|
|
||||||
|
const eventDate = $derived(p.isEvent && p.eventDate ? p.eventDate : null);
|
||||||
|
const locationText = $derived(p.eventLocation?.text ?? null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
class="transition-all flex flex-col text-slate-950 no-underline overflow-hidden border border-gray-200 bg-white"
|
class="transition-all flex flex-col text-slate-950 no-underline overflow-hidden border border-gray-200 bg-white"
|
||||||
>
|
>
|
||||||
<div class="aspect-3/2 w-full overflow-hidden bg-gray-100">
|
<div class="relative aspect-3/2 w-full overflow-hidden bg-gray-100">
|
||||||
{#if resolvedImg}
|
{#if resolvedImg}
|
||||||
<img
|
<img
|
||||||
src={resolvedImg}
|
src={resolvedImg}
|
||||||
@@ -36,6 +47,11 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if eventDate}
|
||||||
|
<div class="absolute bottom-2 left-2 right-2">
|
||||||
|
<EventBadges {eventDate} {locationText} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex flex-col justify-start p-4">
|
<div class="flex-1 flex flex-col justify-start p-4">
|
||||||
<PostMeta
|
<PostMeta
|
||||||
|
|||||||
+126
-68
@@ -3100,8 +3100,13 @@ export interface components {
|
|||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
/** @description array – items type string (list of strings) */
|
/** @description array – items type string (list of strings) */
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
/** @description reference – single slug to collection (e.g. img) */
|
/** @description object – image asset with src and alt text */
|
||||||
mainImage?: string;
|
mainImage?: {
|
||||||
|
/** @description Alt text (optional) */
|
||||||
|
description?: string;
|
||||||
|
/** @description Asset URL */
|
||||||
|
src?: string;
|
||||||
|
};
|
||||||
/** @description object – fixed sub-fields (key, value) */
|
/** @description object – fixed sub-fields (key, value) */
|
||||||
meta?: {
|
meta?: {
|
||||||
key?: string;
|
key?: string;
|
||||||
@@ -3219,8 +3224,13 @@ export interface components {
|
|||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
/** @description array – items type string (list of strings) */
|
/** @description array – items type string (list of strings) */
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
/** @description reference – single slug to collection (e.g. img) */
|
/** @description object – image asset with src and alt text */
|
||||||
mainImage?: string;
|
mainImage?: {
|
||||||
|
/** @description Alt text (optional) */
|
||||||
|
description?: string;
|
||||||
|
/** @description Asset URL */
|
||||||
|
src?: string;
|
||||||
|
};
|
||||||
/** @description object – fixed sub-fields (key, value) */
|
/** @description object – fixed sub-fields (key, value) */
|
||||||
meta?: {
|
meta?: {
|
||||||
key?: string;
|
key?: string;
|
||||||
@@ -3417,11 +3427,11 @@ export interface components {
|
|||||||
/** @description Banner headline */
|
/** @description Banner headline */
|
||||||
headline?: string;
|
headline?: string;
|
||||||
/** @description Banner image (optional) */
|
/** @description Banner image (optional) */
|
||||||
image?: string | {
|
image?: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Alt text (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
/** @description Internal name */
|
/** @description Internal name */
|
||||||
name: string;
|
name: string;
|
||||||
@@ -3442,11 +3452,11 @@ export interface components {
|
|||||||
/** @description Banner headline */
|
/** @description Banner headline */
|
||||||
headline?: string;
|
headline?: string;
|
||||||
/** @description Banner image (optional) */
|
/** @description Banner image (optional) */
|
||||||
image?: string | {
|
image?: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Alt text (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
/** @description Internal name */
|
/** @description Internal name */
|
||||||
name: string;
|
name: string;
|
||||||
@@ -3676,8 +3686,13 @@ export interface components {
|
|||||||
};
|
};
|
||||||
/** @description Internal component name */
|
/** @description Internal component name */
|
||||||
name: string;
|
name: string;
|
||||||
/** @description Optional overlay image (reference to img) */
|
/** @description Optional overlay image */
|
||||||
overlayImage?: string;
|
overlayImage?: {
|
||||||
|
/** @description Alt text (optional) */
|
||||||
|
description?: string;
|
||||||
|
/** @description Asset URL */
|
||||||
|
src?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
iframe_input: {
|
iframe_input: {
|
||||||
/** @description URL slug (used as filename) */
|
/** @description URL slug (used as filename) */
|
||||||
@@ -3718,8 +3733,13 @@ export interface components {
|
|||||||
};
|
};
|
||||||
/** @description Internal component name */
|
/** @description Internal component name */
|
||||||
name: string;
|
name: string;
|
||||||
/** @description Optional overlay image (reference to img) */
|
/** @description Optional overlay image */
|
||||||
overlayImage?: string;
|
overlayImage?: {
|
||||||
|
/** @description Alt text (optional) */
|
||||||
|
description?: string;
|
||||||
|
/** @description Asset URL */
|
||||||
|
src?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
image: {
|
image: {
|
||||||
/** @description Entry identifier (filename) */
|
/** @description Entry identifier (filename) */
|
||||||
@@ -3732,12 +3752,12 @@ export interface components {
|
|||||||
aspectRatio: "1:1" | "4:3" | "3:2" | "16:9" | "21:9" | "2:3" | "3:4";
|
aspectRatio: "1:1" | "4:3" | "3:2" | "16:9" | "21:9" | "2:3" | "3:4";
|
||||||
/** @description Image caption */
|
/** @description Image caption */
|
||||||
caption?: string;
|
caption?: string;
|
||||||
/** @description Image: Slug (Referenz) oder Inline-Objekt (gleiche Felder wie img) */
|
/** @description Image asset */
|
||||||
img: string | {
|
img: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Alt text (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
/** @description Column width (grid) */
|
/** @description Column width (grid) */
|
||||||
layout?: {
|
layout?: {
|
||||||
@@ -3779,13 +3799,13 @@ export interface components {
|
|||||||
readonly _slug?: string;
|
readonly _slug?: string;
|
||||||
/** @description Gallery description (optional) */
|
/** @description Gallery description (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Images (references or inline img) */
|
/** @description Bilder (Asset-URL + optionale Bildunterschrift) */
|
||||||
images: (string | {
|
images: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Bildunterschrift / Alt-Text (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src: string;
|
||||||
})[];
|
}[];
|
||||||
/** @description Column width (grid). Optional. */
|
/** @description Column width (grid). Optional. */
|
||||||
layout?: {
|
layout?: {
|
||||||
/**
|
/**
|
||||||
@@ -3824,13 +3844,13 @@ export interface components {
|
|||||||
_slug: string;
|
_slug: string;
|
||||||
/** @description Gallery description (optional) */
|
/** @description Gallery description (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Images (references or inline img) */
|
/** @description Bilder (Asset-URL + optionale Bildunterschrift) */
|
||||||
images: (string | {
|
images: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Bildunterschrift / Alt-Text (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src: string;
|
||||||
})[];
|
}[];
|
||||||
/** @description Column width (grid). Optional. */
|
/** @description Column width (grid). Optional. */
|
||||||
layout?: {
|
layout?: {
|
||||||
/**
|
/**
|
||||||
@@ -3875,12 +3895,12 @@ export interface components {
|
|||||||
aspectRatio: "1:1" | "4:3" | "3:2" | "16:9" | "21:9" | "2:3" | "3:4";
|
aspectRatio: "1:1" | "4:3" | "3:2" | "16:9" | "21:9" | "2:3" | "3:4";
|
||||||
/** @description Image caption */
|
/** @description Image caption */
|
||||||
caption?: string;
|
caption?: string;
|
||||||
/** @description Image: Slug (Referenz) oder Inline-Objekt (gleiche Felder wie img) */
|
/** @description Image asset */
|
||||||
img: string | {
|
img: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Alt text (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
/** @description Column width (grid) */
|
/** @description Column width (grid) */
|
||||||
layout?: {
|
layout?: {
|
||||||
@@ -4269,11 +4289,11 @@ export interface components {
|
|||||||
/** @description Optional link (website, contact, etc.) */
|
/** @description Optional link (website, contact, etc.) */
|
||||||
link?: string;
|
link?: string;
|
||||||
/** @description Optional logo image */
|
/** @description Optional logo image */
|
||||||
logo?: string | {
|
logo?: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Alt text (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
/** @description Organisation name */
|
/** @description Organisation name */
|
||||||
name: string;
|
name: string;
|
||||||
@@ -4288,11 +4308,11 @@ export interface components {
|
|||||||
/** @description Optional link (website, contact, etc.) */
|
/** @description Optional link (website, contact, etc.) */
|
||||||
link?: string;
|
link?: string;
|
||||||
/** @description Optional logo image */
|
/** @description Optional logo image */
|
||||||
logo?: string | {
|
logo?: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Alt text (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
/** @description Organisation name */
|
/** @description Organisation name */
|
||||||
name: string;
|
name: string;
|
||||||
@@ -4424,13 +4444,15 @@ export interface components {
|
|||||||
blogTagPageHeadline?: string;
|
blogTagPageHeadline?: string;
|
||||||
/** @description Footer text (e.g. copyright) */
|
/** @description Footer text (e.g. copyright) */
|
||||||
footerText1: string;
|
footerText1: string;
|
||||||
/** @description Logo image (reference or inline img) */
|
/** @description Logo image */
|
||||||
logo: string | {
|
logo: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Alt text (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
|
/** @description HTML/JS snippet injected below each post. Use {{PAGE_ID}}, {{PAGE_URL}}, {{PAGE_TITLE}} as placeholders. */
|
||||||
|
postCommentSlot?: string;
|
||||||
/** @description Default meta description */
|
/** @description Default meta description */
|
||||||
seoDescription: string;
|
seoDescription: string;
|
||||||
/** @description Default SEO title for website */
|
/** @description Default SEO title for website */
|
||||||
@@ -4451,13 +4473,15 @@ export interface components {
|
|||||||
blogTagPageHeadline?: string;
|
blogTagPageHeadline?: string;
|
||||||
/** @description Footer text (e.g. copyright) */
|
/** @description Footer text (e.g. copyright) */
|
||||||
footerText1: string;
|
footerText1: string;
|
||||||
/** @description Logo image (reference or inline img) */
|
/** @description Logo image */
|
||||||
logo: string | {
|
logo: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Alt text (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
|
/** @description HTML/JS snippet injected below each post. Use {{PAGE_ID}}, {{PAGE_URL}}, {{PAGE_TITLE}} as placeholders. */
|
||||||
|
postCommentSlot?: string;
|
||||||
/** @description Default meta description */
|
/** @description Default meta description */
|
||||||
seoDescription: string;
|
seoDescription: string;
|
||||||
/** @description Default SEO title for website */
|
/** @description Default SEO title for website */
|
||||||
@@ -4544,9 +4568,18 @@ export interface components {
|
|||||||
readonly created?: string;
|
readonly created?: string;
|
||||||
/**
|
/**
|
||||||
* Format: date-time
|
* Format: date-time
|
||||||
* @description Optional display date
|
* @description Event date and time
|
||||||
*/
|
*/
|
||||||
date?: string;
|
eventDate?: string;
|
||||||
|
/** @description Event location */
|
||||||
|
eventLocation?: {
|
||||||
|
/** @description Latitude (optional, for map) */
|
||||||
|
lat?: number;
|
||||||
|
/** @description Longitude (optional, for map) */
|
||||||
|
lng?: number;
|
||||||
|
/** @description Address / description (e.g. Town hall Schleusingen) */
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
/** @description Short summary for previews */
|
/** @description Short summary for previews */
|
||||||
excerpt?: string;
|
excerpt?: string;
|
||||||
/** @description Post headline */
|
/** @description Post headline */
|
||||||
@@ -4563,14 +4596,19 @@ export interface components {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
important: boolean;
|
important: boolean;
|
||||||
|
/**
|
||||||
|
* @description This post is an event
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
isEvent: boolean;
|
||||||
/** @description Display name (e.g. in navigation or link lists) */
|
/** @description Display name (e.g. in navigation or link lists) */
|
||||||
linkName: string;
|
linkName: string;
|
||||||
/** @description Featured image */
|
/** @description Featured image */
|
||||||
postImage?: string | {
|
postImage?: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Alt text / caption (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
/** @description Associated tags */
|
/** @description Associated tags */
|
||||||
postTag?: string[];
|
postTag?: string[];
|
||||||
@@ -4641,9 +4679,18 @@ export interface components {
|
|||||||
content: string;
|
content: string;
|
||||||
/**
|
/**
|
||||||
* Format: date-time
|
* Format: date-time
|
||||||
* @description Optional display date
|
* @description Event date and time
|
||||||
*/
|
*/
|
||||||
date?: string;
|
eventDate?: string;
|
||||||
|
/** @description Event location */
|
||||||
|
eventLocation?: {
|
||||||
|
/** @description Latitude (optional, for map) */
|
||||||
|
lat?: number;
|
||||||
|
/** @description Longitude (optional, for map) */
|
||||||
|
lng?: number;
|
||||||
|
/** @description Address / description (e.g. Town hall Schleusingen) */
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
/** @description Short summary for previews */
|
/** @description Short summary for previews */
|
||||||
excerpt?: string;
|
excerpt?: string;
|
||||||
/** @description Post headline */
|
/** @description Post headline */
|
||||||
@@ -4660,14 +4707,19 @@ export interface components {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
important: boolean;
|
important: boolean;
|
||||||
|
/**
|
||||||
|
* @description This post is an event
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
isEvent: boolean;
|
||||||
/** @description Display name (e.g. in navigation or link lists) */
|
/** @description Display name (e.g. in navigation or link lists) */
|
||||||
linkName: string;
|
linkName: string;
|
||||||
/** @description Featured image */
|
/** @description Featured image */
|
||||||
postImage?: string | {
|
postImage?: {
|
||||||
/** @description Alt text / description for accessibility */
|
/** @description Alt text / caption (optional) */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
/** @description Asset URL */
|
||||||
src: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
/** @description Associated tags */
|
/** @description Associated tags */
|
||||||
postTag?: string[];
|
postTag?: string[];
|
||||||
@@ -12516,6 +12568,8 @@ export interface operations {
|
|||||||
blogPostsPageSubHeadline?: string;
|
blogPostsPageSubHeadline?: string;
|
||||||
/** @description Filter by blogTagPageHeadline */
|
/** @description Filter by blogTagPageHeadline */
|
||||||
blogTagPageHeadline?: string;
|
blogTagPageHeadline?: string;
|
||||||
|
/** @description Filter by postCommentSlot */
|
||||||
|
postCommentSlot?: string;
|
||||||
};
|
};
|
||||||
header?: never;
|
header?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
@@ -12784,8 +12838,12 @@ export interface operations {
|
|||||||
postTag?: string;
|
postTag?: string;
|
||||||
/** @description Filter by created */
|
/** @description Filter by created */
|
||||||
created?: string;
|
created?: string;
|
||||||
/** @description Filter by date */
|
/** @description Filter by isEvent */
|
||||||
date?: string;
|
isEvent?: boolean;
|
||||||
|
/** @description Filter by eventDate */
|
||||||
|
eventDate?: string;
|
||||||
|
/** @description Filter by eventLocation */
|
||||||
|
eventLocation?: string;
|
||||||
/** @description Filter by icon */
|
/** @description Filter by icon */
|
||||||
icon?: string;
|
icon?: string;
|
||||||
/** @description Filter by hideFromListing */
|
/** @description Filter by hideFromListing */
|
||||||
|
|||||||
+105
-12
@@ -3,7 +3,15 @@ import Layout from "../../layouts/Layout.astro";
|
|||||||
import ContentRows from "../../components/ContentRows.svelte";
|
import ContentRows from "../../components/ContentRows.svelte";
|
||||||
import Tag from "../../components/Tag.svelte";
|
import Tag from "../../components/Tag.svelte";
|
||||||
import Tags from "../../components/Tags.svelte";
|
import Tags from "../../components/Tags.svelte";
|
||||||
import { getPostSlugs, getPostBySlug } from "../../lib/cms";
|
import EventBadges from "../../components/EventBadges.svelte";
|
||||||
|
import EventMap from "../../components/EventMap.svelte";
|
||||||
|
import Accordion from "../../components/Accordion.svelte";
|
||||||
|
import {
|
||||||
|
getPostSlugs,
|
||||||
|
getPostBySlug,
|
||||||
|
getPageConfigBySlug,
|
||||||
|
getPageConfigs,
|
||||||
|
} from "../../lib/cms";
|
||||||
import type { PostEntry } from "../../lib/cms";
|
import type { PostEntry } from "../../lib/cms";
|
||||||
import {
|
import {
|
||||||
resolveContentImages,
|
resolveContentImages,
|
||||||
@@ -18,7 +26,7 @@ import {
|
|||||||
formatPostDate,
|
formatPostDate,
|
||||||
} from "../../lib/blog-utils";
|
} from "../../lib/blog-utils";
|
||||||
import { POST_RESOLVE } from "../../lib/constants";
|
import { POST_RESOLVE } from "../../lib/constants";
|
||||||
import { T } from "../../lib/translations";
|
import { T, t } from "../../lib/translations";
|
||||||
|
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
||||||
@@ -63,12 +71,53 @@ const postImageUrl = rawPostImageUrl
|
|||||||
|
|
||||||
const postDate = formatPostDate(post.created ?? post.date);
|
const postDate = formatPostDate(post.created ?? post.date);
|
||||||
|
|
||||||
|
const isEvent = !!(post as { isEvent?: boolean }).isEvent;
|
||||||
|
const eventDateRaw = (post as { eventDate?: string }).eventDate;
|
||||||
|
const eventDateLabel =
|
||||||
|
isEvent && eventDateRaw
|
||||||
|
? new Date(eventDateRaw).toLocaleDateString("de-DE", {
|
||||||
|
weekday: "long",
|
||||||
|
day: "2-digit",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const eventLocation = isEvent
|
||||||
|
? (post as { eventLocation?: { text?: string; lat?: number; lng?: number } })
|
||||||
|
.eventLocation
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const hasCoords = !!(eventLocation?.lat && eventLocation?.lng);
|
||||||
|
const _lat = eventLocation?.lat ?? 0;
|
||||||
|
const _lng = eventLocation?.lng ?? 0;
|
||||||
|
const mapEmbedUrl = hasCoords
|
||||||
|
? `https://www.openstreetmap.org/export/embed.html?bbox=${_lng - 0.01},${_lat - 0.007},${_lng + 0.01},${_lat + 0.007}&layer=mapnik&marker=${_lat},${_lng}`
|
||||||
|
: null;
|
||||||
|
const mapLinkUrl = hasCoords
|
||||||
|
? `https://www.openstreetmap.org/?mlat=${eventLocation!.lat}&mlon=${eventLocation!.lng}#map=15/${eventLocation!.lat}/${eventLocation!.lng}`
|
||||||
|
: eventLocation?.text
|
||||||
|
? `https://www.openstreetmap.org/search?query=${encodeURIComponent(eventLocation.text)}`
|
||||||
|
: null;
|
||||||
|
|
||||||
const contentHtml = await resolveBodyContent(post as { content?: unknown });
|
const contentHtml = await resolveBodyContent(post as { content?: unknown });
|
||||||
const hasRowContent =
|
const hasRowContent =
|
||||||
((post as { row1Content?: unknown[] }).row1Content?.length ?? 0) > 0 ||
|
((post as { row1Content?: unknown[] }).row1Content?.length ?? 0) > 0 ||
|
||||||
((post as { row2Content?: unknown[] }).row2Content?.length ?? 0) > 0 ||
|
((post as { row2Content?: unknown[] }).row2Content?.length ?? 0) > 0 ||
|
||||||
((post as { row3Content?: unknown[] }).row3Content?.length ?? 0) > 0;
|
((post as { row3Content?: unknown[] }).row3Content?.length ?? 0) > 0;
|
||||||
const translations = Astro.locals.translations ?? {};
|
const translations = Astro.locals.translations ?? {};
|
||||||
|
|
||||||
|
const pageConfig =
|
||||||
|
(await getPageConfigBySlug("page-config-default", { locale: "de" })) ??
|
||||||
|
(await getPageConfigs({ locale: "de", per_page: 1 }))[0] ??
|
||||||
|
null;
|
||||||
|
const commentSlotRaw =
|
||||||
|
(pageConfig as { postCommentSlot?: string } | null)?.postCommentSlot ?? "";
|
||||||
|
const commentSlot = commentSlotRaw
|
||||||
|
.replace(/\{\{PAGE_ID\}\}/g, slug)
|
||||||
|
.replace(/\{\{PAGE_URL\}\}/g, Astro.url.href)
|
||||||
|
.replace(/\{\{PAGE_TITLE\}\}/g, post.headline ?? post.linkName ?? slug);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
@@ -82,9 +131,22 @@ const translations = Astro.locals.translations ?? {};
|
|||||||
breadcrumbItems={[
|
breadcrumbItems={[
|
||||||
{ href: "/", translationKey: T.nav_start },
|
{ href: "/", translationKey: T.nav_start },
|
||||||
{ href: "/posts/", translationKey: T.nav_posts },
|
{ href: "/posts/", translationKey: T.nav_posts },
|
||||||
{ label: post.headline ?? post.linkName ?? post.slug ?? post._slug ?? slug },
|
{
|
||||||
|
label: post.headline ?? post.linkName ?? post.slug ?? post._slug ?? slug,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
isEvent && (eventDateLabel || eventLocation?.text) && (
|
||||||
|
<div class="mb-6">
|
||||||
|
<EventBadges
|
||||||
|
eventDate={eventDateRaw ?? null}
|
||||||
|
locationText={eventLocation?.text ?? null}
|
||||||
|
locationHref={(mapEmbedUrl || mapLinkUrl) ? "#map-section" : null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
<div class="md:flex gap-2 items-end">
|
<div class="md:flex gap-2 items-end">
|
||||||
{
|
{
|
||||||
postImageUrl && (
|
postImageUrl && (
|
||||||
@@ -116,15 +178,46 @@ const translations = Astro.locals.translations ?? {};
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="mt-8">
|
<article class="mt-8">
|
||||||
{contentHtml && (
|
{
|
||||||
<div
|
contentHtml && (
|
||||||
class="content markdown max-w-none prose prose-zinc"
|
<div
|
||||||
set:html={contentHtml}
|
class="content markdown max-w-none prose prose-zinc"
|
||||||
/>
|
set:html={contentHtml}
|
||||||
)}
|
/>
|
||||||
{hasRowContent && (
|
)
|
||||||
<ContentRows client:load layout={post} translations={translations} />
|
}
|
||||||
)}
|
{
|
||||||
|
hasRowContent && (
|
||||||
|
<ContentRows client:load layout={post} translations={translations} />
|
||||||
|
)
|
||||||
|
}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
{
|
||||||
|
isEvent && (mapEmbedUrl || mapLinkUrl) && (
|
||||||
|
<EventMap
|
||||||
|
client:load
|
||||||
|
embedUrl={mapEmbedUrl}
|
||||||
|
linkUrl={mapLinkUrl}
|
||||||
|
locationText={eventLocation?.text ?? null}
|
||||||
|
label={t(translations, T.post_map)}
|
||||||
|
translations={translations}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
(post as { showCommentSection?: boolean }).showCommentSection !== false &&
|
||||||
|
commentSlot && (
|
||||||
|
<Accordion label={t(translations, T.post_comments)} class="mt-6">
|
||||||
|
<div
|
||||||
|
class="p-8 border border-zinc-200 rounded-lg"
|
||||||
|
set:html={commentSlot}
|
||||||
|
/>
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
|||||||
@@ -334,6 +334,10 @@ main ol li {
|
|||||||
@apply mt-4 mb-3;
|
@apply mt-4 mb-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown hr {
|
||||||
|
@apply border-stein-200 border-dotted mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown li,
|
.markdown li,
|
||||||
.markdown li p {
|
.markdown li p {
|
||||||
@apply mt-0 mb-0;
|
@apply mt-0 mb-0;
|
||||||
|
|||||||
Reference in New Issue
Block a user