Files
windwiderstand/src/lib/components/PostCard.svelte
T
Peter Meier 0ab2c58eb4
Deploy / verify (push) Successful in 1m26s
Deploy / deploy (push) Successful in 2m26s
feat(header): icon nav redesign + migrate icons mdi→lucide
- Header desktop nav: social icons grouped in a wald-tinted pill with
  labels, active-state (aria-current + bold), opacity hover (no per-item bg)
- Mobile overlay: social links as icon+label rows with active-state
- Search button: lucide:search, icon-only
- Migrate all icons mdi→lucide app-wide (234 refs); brand/language logos
  (google, whatsapp, mastodon, telegram, language-*) stay on mdi
- generate-icons: emit mdi + lucide subsets; add @iconify-json/lucide
- iconify-offline: register lucide collection
- scripts/icon-map|validate|apply: reusable migration tooling

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:44:56 +02:00

134 lines
4.2 KiB
Svelte

<script lang="ts">
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import type { PostEntry } from "$lib/cms";
import PostMeta from "./PostMeta.svelte";
import EventBadges from "./EventBadges.svelte";
import RustyImage from "./RustyImage.svelte";
import Card from "./Card.svelte";
import { getPostEventInfo } from "$lib/blog-utils";
import { useTranslate, T } from "$lib/translations";
import { marked } from "$lib/markdown-safe";
const t = useTranslate();
type PostWithImage = PostEntry & {
_resolvedImageUrl?: string;
_rawImageUrl?: string;
_imageFocal?: { x: number; y: number } | null;
};
let {
post,
href,
tagBase = "/posts/tag",
commentCount = null,
class: className = "",
}: {
post: PostWithImage;
href: string;
tagBase?: string;
/** Approved comment count. `null` = unknown/loading, hide badge. `0` = also hide. */
commentCount?: number | null;
class?: string;
} = $props();
const rawImg = $derived(post._rawImageUrl);
const focal = $derived(post._imageFocal ?? null);
const fallbackImg = $derived(post._resolvedImageUrl);
const eventInfo = $derived(getPostEventInfo(post));
const eventDate = $derived(eventInfo.eventDate);
const locationText = $derived(eventInfo.eventLocation?.text ?? null);
const tagSlugs = $derived(
(post.postTag ?? []).map((t) =>
typeof t === "string" ? t : ((t as { _slug?: string })?._slug ?? ""),
),
);
const hasTerminTag = $derived(tagSlugs.includes("tag-termin"));
const isPastEvent = $derived(
hasTerminTag && !!eventDate && new Date(eventDate).getTime() < Date.now(),
);
</script>
<Card
{href}
layout="post"
class="{isPastEvent ? 'opacity-70 grayscale' : ''} {className}"
>
<div class="relative aspect-3/2 w-full overflow-hidden bg-stein-100">
{#if rawImg}
<RustyImage
src={rawImg}
focal={focal}
width={400}
aspect="3/2"
alt={post.headline ?? ""}
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
class="w-full h-full object-cover"
loading="lazy"
/>
{:else if fallbackImg}
<img
src={fallbackImg}
alt={post.headline ?? ""}
class="w-full h-full object-cover"
loading="lazy"
/>
{:else}
<div class="w-full h-full flex items-center justify-center text-stein-300">
<svg xmlns="http://www.w3.org/2000/svg" class="size-12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
</svg>
</div>
{/if}
{#if eventDate}
<div class="absolute bottom-2 left-2 right-2 min-w-0">
<EventBadges {eventDate} {locationText} />
</div>
{/if}
{#if isPastEvent}
<div class="absolute top-2 right-2">
<span class="inline-flex items-center gap-1 rounded-full bg-stein-800/90 px-2.5 py-1 text-[.65rem] font-semibold uppercase tracking-wide text-white shadow-sm">
Vergangen
</span>
</div>
{/if}
{#if commentCount != null}
<div class="absolute top-2 left-2">
<span
class="inline-flex items-center gap-1 rounded-full bg-white/90 px-2 py-1 text-[.65rem] font-semibold tabular-nums text-stein-700 shadow-sm"
aria-label={t(T.comments_count_aria, { count: commentCount })}
title={t(T.comments_count_aria, { count: commentCount })}
>
<Icon icon="lucide:message-circle" class="size-3" />
{commentCount}
</span>
</div>
{/if}
</div>
<div class="flex-1 flex flex-col justify-start p-4">
<PostMeta
date={post.created ?? null}
tags={post.postTag ?? []}
{tagBase}
/>
<div>
<h5 class="truncate text-sm font-medium leading-tight mb-1">
{post.headline ?? post.linkName ?? post.slug ?? post._slug}
</h5>
{#if post.subheadline}
<div class="text-xs line-clamp-2 mb-1">
{@html marked.parseInline(post.subheadline)}
</div>
{/if}
{#if post.excerpt}
<p class="text-xs font-light line-clamp-3">
{post.excerpt}
</p>
{/if}
</div>
</div>
</Card>