0ab2c58eb4
- 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>
134 lines
4.2 KiB
Svelte
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>
|