diff --git a/src/lib/blog-utils.ts b/src/lib/blog-utils.ts index de5142e..7c3c29f 100644 --- a/src/lib/blog-utils.ts +++ b/src/lib/blog-utils.ts @@ -121,6 +121,44 @@ export function filterPostsByTag( }); } +export type PostEventLocation = { + text?: string; + lat?: number; + lng?: number; +}; + +export type PostEventInfo = { + isEvent: boolean; + eventDate: string | null; + eventLocation: PostEventLocation | null; +}; + +/** + * Liest event-Daten aus dem aufgelösten calendarItem-Reference des Posts. + * Voraussetzung: post mit resolve=["all"] oder explizit "calendarItem" geladen, + * sodass `post.calendarItem` als Objekt mit terminZeit/location vorliegt. + */ +export function getPostEventInfo( + post: PostEntry | null | undefined, +): PostEventInfo { + const ci = (post as { calendarItem?: unknown } | null | undefined)?.calendarItem; + if (!ci || typeof ci !== "object") { + return { isEvent: false, eventDate: null, eventLocation: null }; + } + const obj = ci as { + terminZeit?: string; + location?: PostEventLocation; + }; + if (!obj.terminZeit) { + return { isEvent: false, eventDate: null, eventLocation: null }; + } + return { + isEvent: true, + eventDate: obj.terminZeit, + eventLocation: obj.location ?? null, + }; +} + /** URL aus post.postImage (RustyCMS img mit src, oder legacy file.url). */ export function getPostImageUrl( postImage: PostEntry["postImage"], @@ -200,32 +238,33 @@ export type PostSearchIndexEntry = { eventDate: string | null; }; -type PostWithEventIndex = PostEntry & { - isEvent?: boolean; - eventDate?: string; +type PostWithImageIndex = PostEntry & { _resolvedImageUrl?: string; }; export function buildPostSearchIndex(posts: PostEntry[]): PostSearchIndexEntry[] { - return posts.map((p) => ({ - slug: p.slug ?? p._slug ?? "", - headline: p.headline ?? "", - excerpt: p.excerpt ?? "", - subheadline: p.subheadline ?? "", - tags: (p.postTag ?? []) - .map((t) => - typeof t === "string" - ? t - : (t as { name?: string; _slug?: string }).name ?? - (t as { _slug?: string })._slug ?? - "", - ) - .filter(Boolean), - image: (p as PostWithEventIndex)._resolvedImageUrl ?? null, - created: p.created ?? null, - isEvent: Boolean((p as PostWithEventIndex).isEvent), - eventDate: (p as PostWithEventIndex).eventDate ?? null, - })); + return posts.map((p) => { + const ev = getPostEventInfo(p); + return { + slug: p.slug ?? p._slug ?? "", + headline: p.headline ?? "", + excerpt: p.excerpt ?? "", + subheadline: p.subheadline ?? "", + tags: (p.postTag ?? []) + .map((t) => + typeof t === "string" + ? t + : (t as { name?: string; _slug?: string }).name ?? + (t as { _slug?: string })._slug ?? + "", + ) + .filter(Boolean), + image: (p as PostWithImageIndex)._resolvedImageUrl ?? null, + created: p.created ?? null, + isEvent: ev.isEvent, + eventDate: ev.eventDate, + }; + }); } export type LoadPostsResult = { @@ -260,7 +299,7 @@ export async function loadPostsList(opts: { // (post → postImage → img-Sidecar; post → postTag → name/icon/color). [posts, tags] = await Promise.all([ getPosts({ - resolve: ["postImage", "postTag"], + resolve: ["postImage", "postTag", "calendarItem"], depth: 2, fields: [ "_slug", @@ -270,8 +309,7 @@ export async function loadPostsList(opts: { "subheadline", "excerpt", "created", - "isEvent", - "eventDate", + "calendarItem", "hideFromListing", "postImage.*", "postTag.name", @@ -342,16 +380,18 @@ export async function loadPostsList(opts: { export function selectUpcomingEvents(posts: PostEntry[], max = 4): PostEntry[] { const now = Date.now(); - return (posts as PostWithEventIndex[]) - .filter( - (p) => p.isEvent && p.eventDate && new Date(p.eventDate).getTime() >= now, - ) - .sort( - (a, b) => - new Date(a.eventDate ?? 0).getTime() - - new Date(b.eventDate ?? 0).getTime(), - ) - .slice(0, max) as PostEntry[]; + type Scored = { post: PostEntry; ts: number }; + const scored: Scored[] = []; + for (const p of posts) { + const ev = getPostEventInfo(p); + if (!ev.isEvent || !ev.eventDate) continue; + const ts = new Date(ev.eventDate).getTime(); + if (ts >= now) scored.push({ post: p, ts }); + } + return scored + .sort((a, b) => a.ts - b.ts) + .slice(0, max) + .map((s) => s.post); } const POST_BASE = "/post"; diff --git a/src/lib/components/BlogOverview.svelte b/src/lib/components/BlogOverview.svelte index 55fe79a..fdcc8c6 100644 --- a/src/lib/components/BlogOverview.svelte +++ b/src/lib/components/BlogOverview.svelte @@ -6,7 +6,7 @@ import Tag from "./Tag.svelte"; import Pagination from "./Pagination.svelte"; import type { PostEntry } from "$lib/cms"; - import { postHref } from "$lib/blog-utils"; + import { postHref, getPostEventInfo } from "$lib/blog-utils"; import { t as tStatic, T } from "$lib/translations"; import type { Translations } from "$lib/translations"; import { @@ -34,8 +34,6 @@ eventDate: string | null; }; - type EventPost = PostEntry & { eventDate?: string; isEvent?: boolean }; - let { posts = [], tags = [], @@ -56,7 +54,7 @@ totalPosts?: number; basePath?: string; translations?: Translations | null; - upcomingEvents?: EventPost[]; + upcomingEvents?: PostEntry[]; searchIndex?: SearchIndexEntry[] | null; } = $props(); @@ -193,12 +191,13 @@