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({
|
||||
site,
|
||||
devToolbar: { enabled: false },
|
||||
integrations: [
|
||||
svelte(),
|
||||
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">
|
||||
import type { PostEntry } from "../lib/cms";
|
||||
import PostMeta from "./PostMeta.svelte";
|
||||
import EventBadges from "./EventBadges.svelte";
|
||||
|
||||
type PostWithImage = PostEntry & { _resolvedImageUrl?: string };
|
||||
|
||||
@@ -15,13 +16,23 @@
|
||||
} = $props();
|
||||
|
||||
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>
|
||||
|
||||
<a
|
||||
href={href}
|
||||
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}
|
||||
<img
|
||||
src={resolvedImg}
|
||||
@@ -36,6 +47,11 @@
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
{#if eventDate}
|
||||
<div class="absolute bottom-2 left-2 right-2">
|
||||
<EventBadges {eventDate} {locationText} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-start p-4">
|
||||
<PostMeta
|
||||
|
||||
+126
-68
@@ -3100,8 +3100,13 @@ export interface components {
|
||||
isActive?: boolean;
|
||||
/** @description array – items type string (list of strings) */
|
||||
labels?: string[];
|
||||
/** @description reference – single slug to collection (e.g. img) */
|
||||
mainImage?: string;
|
||||
/** @description object – image asset with src and alt text */
|
||||
mainImage?: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
/** @description object – fixed sub-fields (key, value) */
|
||||
meta?: {
|
||||
key?: string;
|
||||
@@ -3219,8 +3224,13 @@ export interface components {
|
||||
isActive?: boolean;
|
||||
/** @description array – items type string (list of strings) */
|
||||
labels?: string[];
|
||||
/** @description reference – single slug to collection (e.g. img) */
|
||||
mainImage?: string;
|
||||
/** @description object – image asset with src and alt text */
|
||||
mainImage?: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
/** @description object – fixed sub-fields (key, value) */
|
||||
meta?: {
|
||||
key?: string;
|
||||
@@ -3417,11 +3427,11 @@ export interface components {
|
||||
/** @description Banner headline */
|
||||
headline?: string;
|
||||
/** @description Banner image (optional) */
|
||||
image?: string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
image?: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
src: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
/** @description Internal name */
|
||||
name: string;
|
||||
@@ -3442,11 +3452,11 @@ export interface components {
|
||||
/** @description Banner headline */
|
||||
headline?: string;
|
||||
/** @description Banner image (optional) */
|
||||
image?: string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
image?: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
src: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
/** @description Internal name */
|
||||
name: string;
|
||||
@@ -3676,8 +3686,13 @@ export interface components {
|
||||
};
|
||||
/** @description Internal component name */
|
||||
name: string;
|
||||
/** @description Optional overlay image (reference to img) */
|
||||
overlayImage?: string;
|
||||
/** @description Optional overlay image */
|
||||
overlayImage?: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
};
|
||||
iframe_input: {
|
||||
/** @description URL slug (used as filename) */
|
||||
@@ -3718,8 +3733,13 @@ export interface components {
|
||||
};
|
||||
/** @description Internal component name */
|
||||
name: string;
|
||||
/** @description Optional overlay image (reference to img) */
|
||||
overlayImage?: string;
|
||||
/** @description Optional overlay image */
|
||||
overlayImage?: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
};
|
||||
image: {
|
||||
/** @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";
|
||||
/** @description Image caption */
|
||||
caption?: string;
|
||||
/** @description Image: Slug (Referenz) oder Inline-Objekt (gleiche Felder wie img) */
|
||||
img: string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
/** @description Image asset */
|
||||
img: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
src: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
/** @description Column width (grid) */
|
||||
layout?: {
|
||||
@@ -3779,13 +3799,13 @@ export interface components {
|
||||
readonly _slug?: string;
|
||||
/** @description Gallery description (optional) */
|
||||
description?: string;
|
||||
/** @description Images (references or inline img) */
|
||||
images: (string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
/** @description Bilder (Asset-URL + optionale Bildunterschrift) */
|
||||
images: {
|
||||
/** @description Bildunterschrift / Alt-Text (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
/** @description Asset URL */
|
||||
src: string;
|
||||
})[];
|
||||
}[];
|
||||
/** @description Column width (grid). Optional. */
|
||||
layout?: {
|
||||
/**
|
||||
@@ -3824,13 +3844,13 @@ export interface components {
|
||||
_slug: string;
|
||||
/** @description Gallery description (optional) */
|
||||
description?: string;
|
||||
/** @description Images (references or inline img) */
|
||||
images: (string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
/** @description Bilder (Asset-URL + optionale Bildunterschrift) */
|
||||
images: {
|
||||
/** @description Bildunterschrift / Alt-Text (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
/** @description Asset URL */
|
||||
src: string;
|
||||
})[];
|
||||
}[];
|
||||
/** @description Column width (grid). Optional. */
|
||||
layout?: {
|
||||
/**
|
||||
@@ -3875,12 +3895,12 @@ export interface components {
|
||||
aspectRatio: "1:1" | "4:3" | "3:2" | "16:9" | "21:9" | "2:3" | "3:4";
|
||||
/** @description Image caption */
|
||||
caption?: string;
|
||||
/** @description Image: Slug (Referenz) oder Inline-Objekt (gleiche Felder wie img) */
|
||||
img: string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
/** @description Image asset */
|
||||
img: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
src: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
/** @description Column width (grid) */
|
||||
layout?: {
|
||||
@@ -4269,11 +4289,11 @@ export interface components {
|
||||
/** @description Optional link (website, contact, etc.) */
|
||||
link?: string;
|
||||
/** @description Optional logo image */
|
||||
logo?: string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
logo?: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
src: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
/** @description Organisation name */
|
||||
name: string;
|
||||
@@ -4288,11 +4308,11 @@ export interface components {
|
||||
/** @description Optional link (website, contact, etc.) */
|
||||
link?: string;
|
||||
/** @description Optional logo image */
|
||||
logo?: string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
logo?: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
src: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
/** @description Organisation name */
|
||||
name: string;
|
||||
@@ -4424,13 +4444,15 @@ export interface components {
|
||||
blogTagPageHeadline?: string;
|
||||
/** @description Footer text (e.g. copyright) */
|
||||
footerText1: string;
|
||||
/** @description Logo image (reference or inline img) */
|
||||
logo: string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
/** @description Logo image */
|
||||
logo: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
src: string;
|
||||
/** @description Asset URL */
|
||||
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 */
|
||||
seoDescription: string;
|
||||
/** @description Default SEO title for website */
|
||||
@@ -4451,13 +4473,15 @@ export interface components {
|
||||
blogTagPageHeadline?: string;
|
||||
/** @description Footer text (e.g. copyright) */
|
||||
footerText1: string;
|
||||
/** @description Logo image (reference or inline img) */
|
||||
logo: string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
/** @description Logo image */
|
||||
logo: {
|
||||
/** @description Alt text (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
src: string;
|
||||
/** @description Asset URL */
|
||||
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 */
|
||||
seoDescription: string;
|
||||
/** @description Default SEO title for website */
|
||||
@@ -4544,9 +4568,18 @@ export interface components {
|
||||
readonly created?: string;
|
||||
/**
|
||||
* 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 */
|
||||
excerpt?: string;
|
||||
/** @description Post headline */
|
||||
@@ -4563,14 +4596,19 @@ export interface components {
|
||||
* @default false
|
||||
*/
|
||||
important: boolean;
|
||||
/**
|
||||
* @description This post is an event
|
||||
* @default false
|
||||
*/
|
||||
isEvent: boolean;
|
||||
/** @description Display name (e.g. in navigation or link lists) */
|
||||
linkName: string;
|
||||
/** @description Featured image */
|
||||
postImage?: string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
postImage?: {
|
||||
/** @description Alt text / caption (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
src: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
/** @description Associated tags */
|
||||
postTag?: string[];
|
||||
@@ -4641,9 +4679,18 @@ export interface components {
|
||||
content: string;
|
||||
/**
|
||||
* 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 */
|
||||
excerpt?: string;
|
||||
/** @description Post headline */
|
||||
@@ -4660,14 +4707,19 @@ export interface components {
|
||||
* @default false
|
||||
*/
|
||||
important: boolean;
|
||||
/**
|
||||
* @description This post is an event
|
||||
* @default false
|
||||
*/
|
||||
isEvent: boolean;
|
||||
/** @description Display name (e.g. in navigation or link lists) */
|
||||
linkName: string;
|
||||
/** @description Featured image */
|
||||
postImage?: string | {
|
||||
/** @description Alt text / description for accessibility */
|
||||
postImage?: {
|
||||
/** @description Alt text / caption (optional) */
|
||||
description?: string;
|
||||
/** @description Image URL (e.g. /api/assets/…). Use widget for picker. */
|
||||
src: string;
|
||||
/** @description Asset URL */
|
||||
src?: string;
|
||||
};
|
||||
/** @description Associated tags */
|
||||
postTag?: string[];
|
||||
@@ -12516,6 +12568,8 @@ export interface operations {
|
||||
blogPostsPageSubHeadline?: string;
|
||||
/** @description Filter by blogTagPageHeadline */
|
||||
blogTagPageHeadline?: string;
|
||||
/** @description Filter by postCommentSlot */
|
||||
postCommentSlot?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
@@ -12784,8 +12838,12 @@ export interface operations {
|
||||
postTag?: string;
|
||||
/** @description Filter by created */
|
||||
created?: string;
|
||||
/** @description Filter by date */
|
||||
date?: string;
|
||||
/** @description Filter by isEvent */
|
||||
isEvent?: boolean;
|
||||
/** @description Filter by eventDate */
|
||||
eventDate?: string;
|
||||
/** @description Filter by eventLocation */
|
||||
eventLocation?: string;
|
||||
/** @description Filter by icon */
|
||||
icon?: string;
|
||||
/** @description Filter by hideFromListing */
|
||||
|
||||
+105
-12
@@ -3,7 +3,15 @@ import Layout from "../../layouts/Layout.astro";
|
||||
import ContentRows from "../../components/ContentRows.svelte";
|
||||
import Tag from "../../components/Tag.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 {
|
||||
resolveContentImages,
|
||||
@@ -18,7 +26,7 @@ import {
|
||||
formatPostDate,
|
||||
} from "../../lib/blog-utils";
|
||||
import { POST_RESOLVE } from "../../lib/constants";
|
||||
import { T } from "../../lib/translations";
|
||||
import { T, t } from "../../lib/translations";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
@@ -63,12 +71,53 @@ const postImageUrl = rawPostImageUrl
|
||||
|
||||
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 hasRowContent =
|
||||
((post as { row1Content?: unknown[] }).row1Content?.length ?? 0) > 0 ||
|
||||
((post as { row2Content?: unknown[] }).row2Content?.length ?? 0) > 0 ||
|
||||
((post as { row3Content?: unknown[] }).row3Content?.length ?? 0) > 0;
|
||||
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
|
||||
@@ -82,9 +131,22 @@ const translations = Astro.locals.translations ?? {};
|
||||
breadcrumbItems={[
|
||||
{ href: "/", translationKey: T.nav_start },
|
||||
{ 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">
|
||||
{
|
||||
postImageUrl && (
|
||||
@@ -116,15 +178,46 @@ const translations = Astro.locals.translations ?? {};
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article class="mt-8">
|
||||
{contentHtml && (
|
||||
<div
|
||||
class="content markdown max-w-none prose prose-zinc"
|
||||
set:html={contentHtml}
|
||||
/>
|
||||
)}
|
||||
{hasRowContent && (
|
||||
<ContentRows client:load layout={post} translations={translations} />
|
||||
)}
|
||||
{
|
||||
contentHtml && (
|
||||
<div
|
||||
class="content markdown max-w-none prose prose-zinc"
|
||||
set:html={contentHtml}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasRowContent && (
|
||||
<ContentRows client:load layout={post} translations={translations} />
|
||||
)
|
||||
}
|
||||
</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>
|
||||
|
||||
|
||||
@@ -334,6 +334,10 @@ main ol li {
|
||||
@apply mt-4 mb-3;
|
||||
}
|
||||
|
||||
.markdown hr {
|
||||
@apply border-stein-200 border-dotted mb-4;
|
||||
}
|
||||
|
||||
.markdown li,
|
||||
.markdown li p {
|
||||
@apply mt-0 mb-0;
|
||||
|
||||
Reference in New Issue
Block a user