feat(termin): Detailseite-Redesign + gedeckeltes Bild
Deploy / verify (push) Successful in 1m20s
Deploy / deploy (push) Successful in 1m37s

- 2-Spalten-Layout (Inhalt auf Lesebreite, sticky Aktions-Card rechts)
- Bild gedeckelt (max-h-440, contained, Hover-Zoom) statt full-width
- Countdown-Badge (Heute/Morgen/in X Tagen/vergangen)
- Aktionen gruppiert: "Zum Kalender" (ICS/Google) + "Teilen" (Link/QR/Bild)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-06-21 13:17:27 +02:00
parent 905c2e4d36
commit 0b3ddd86e3
2 changed files with 184 additions and 117 deletions
+5 -2
View File
@@ -228,8 +228,11 @@
"home-outline": {
"body": "<path fill=\"currentColor\" d=\"m12 5.69l5 4.5V18h-2v-6H9v6H7v-7.81zM12 3L2 12h3v8h6v-6h2v6h6v-8h3\"/>"
},
"calendar-month": {
"body": "<path fill=\"currentColor\" d=\"M9 10v2H7v-2zm4 0v2h-2v-2zm4 0v2h-2v-2zm2-7a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h1V1h2v2h8V1h2v2zm0 16V8H5v11zM9 14v2H7v-2zm4 0v2h-2v-2zm4 0v2h-2v-2z\"/>"
"calendar-blank": {
"body": "<path fill=\"currentColor\" d=\"M19 19H5V8h14m-3-7v2H8V1H6v2H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-1V1\"/>"
},
"calendar-export": {
"body": "<path fill=\"currentColor\" d=\"m12 22l4-4h-3v-6h-2v6H8zm7-19h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v14a2 2 0 0 0 2 2h3l-2-2H5V8h14v11h-1l-2 2h3c1.11 0 2-.89 2-2V5a2 2 0 0 0-2-2\"/>"
},
"share-variant-outline": {
"body": "<path fill=\"currentColor\" d=\"M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81c1.66 0 3-1.34 3-3s-1.34-3-3-3s-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66c0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91s-1.31-2.92-2.92-2.92M18 4c.55 0 1 .45 1 1s-.45 1-1 1s-1-.45-1-1s.45-1 1-1M6 13c-.55 0-1-.45-1-1s.45-1 1-1s1 .45 1 1s-.45 1-1 1m12 7c-.55 0-1-.45-1-1s.45-1 1-1s1 .45 1 1s-.45 1-1 1\"/>"
+179 -115
View File
@@ -18,10 +18,29 @@
const t = useTranslate();
const item = $derived(data.item);
// Kanonische URL dieser Seite (für ICS-Link, Teilen, QR).
// Kanonische URL dieser Seite (für ICS-Link, Teilen, QR) + relativer
// Countdown. Client-only (kein SSR), sonst Hydration-Mismatch (Server-Zeit).
let pageUrl = $state("");
let relLabel = $state("");
let isPast = $state(false);
$effect(() => {
pageUrl = typeof window !== "undefined" ? window.location.href : "";
pageUrl = window.location.href;
if (item.terminZeit) {
const startDay = new Date(item.terminZeit).setHours(0, 0, 0, 0);
const today = new Date().setHours(0, 0, 0, 0);
const days = Math.round((startDay - today) / 86_400_000);
isPast = days < 0;
relLabel =
days === 0
? "Heute"
: days === 1
? "Morgen"
: days > 1
? `in ${days} Tagen`
: days === -1
? "Gestern"
: `vor ${-days} Tagen`;
}
});
function toICS(): CalendarItemICS {
@@ -98,126 +117,171 @@
<!-- Breite kommt vom Layout-Wrapper (.container-custom in +layout.svelte) —
hier nur vertikaler Abstand, KEIN eigenes max-w/mx-auto/px (sonst schmaler
+ doppeltes Padding ggü. den anderen Seiten). -->
<article class="pt-10 pb-4">
<p class="mb-2 text-xs font-semibold uppercase tracking-wider text-wald-700">Termin</p>
<h1 class="mb-4 text-3xl font-bold leading-tight text-stein-900">{item.title}</h1>
<article class="pt-8 pb-6">
<a
href={data.calendarUrl}
class="mb-6 inline-flex items-center gap-1 text-sm text-stein-500 no-underline hover:text-wald-700"
>
<Icon icon="mdi:chevron-left" class="size-4" aria-hidden="true" />
Alle Termine
</a>
<!-- Eckdaten -->
<div class="space-y-1.5 text-stein-700">
{#if item.dateLabel}
<p class="flex items-center gap-2">
<Icon icon="mdi:calendar" class="size-5 shrink-0 text-wald-700" aria-hidden="true" />
<span class="font-medium">
{item.weekdayLabel ? `${item.weekdayLabel}, ` : ""}{item.dateLabel}{item.endDateLabel
? ` ${item.endDateLabel}`
: ""}
</span>
</p>
{/if}
{#if item.timeLabel}
<p class="flex items-center gap-2">
<Icon icon="mdi:clock-outline" class="size-5 shrink-0 text-wald-700" aria-hidden="true" />
<span class="tabular-nums">{item.timeLabel}</span>
</p>
{/if}
{#if item.locationText}
<p class="flex items-center gap-2">
<Icon icon="mdi:map-marker" class="size-5 shrink-0 text-wald-700" aria-hidden="true" />
<a
href={mapsUrl(item.locationText)}
target="_blank"
rel="noopener noreferrer"
class="text-stein-700 underline underline-offset-2 hover:text-wald-700"
<div class="grid gap-8 lg:grid-cols-[minmax(0,1fr)_300px] lg:gap-12">
<!-- Hauptspalte: auf Lesebreite begrenzt -->
<div class="min-w-0 max-w-2xl">
<div class="mb-3 flex flex-wrap items-center gap-2">
<span class="text-xs font-semibold uppercase tracking-wider text-wald-700">Termin</span>
{#if relLabel}
<span
class="rounded-full px-2 py-0.5 text-xs font-semibold {isPast
? 'bg-stein-100 text-stein-500'
: 'bg-wald-100 text-wald-800'}"
>
{relLabel}
</span>
{/if}
</div>
<h1 class="text-3xl font-bold leading-tight text-stein-900 sm:text-4xl">{item.title}</h1>
<div class="mt-5 space-y-2 text-stein-700">
{#if item.dateLabel}
<p class="flex items-center gap-2.5">
<Icon icon="mdi:calendar-blank" class="size-5 shrink-0 text-wald-700" aria-hidden="true" />
<span class="font-medium">
{item.weekdayLabel ? `${item.weekdayLabel}, ` : ""}{item.dateLabel}{item.endDateLabel
? ` ${item.endDateLabel}`
: ""}
</span>
</p>
{/if}
{#if item.timeLabel}
<p class="flex items-center gap-2.5">
<Icon icon="mdi:clock-outline" class="size-5 shrink-0 text-wald-700" aria-hidden="true" />
<span class="tabular-nums">{item.timeLabel}</span>
</p>
{/if}
{#if item.locationText}
<p class="flex items-center gap-2.5">
<Icon icon="mdi:map-marker" class="size-5 shrink-0 text-wald-700" aria-hidden="true" />
<a
href={mapsUrl(item.locationText)}
target="_blank"
rel="noopener noreferrer"
class="text-stein-700 underline underline-offset-2 hover:text-wald-700"
>
{item.locationText}
</a>
</p>
{/if}
</div>
{#if item.tags.length > 0}
<div class="mt-4 flex flex-wrap gap-1.5">
{#each item.tags as tg}
<span class="rounded-full bg-himmel-100 px-2.5 py-0.5 text-xs text-himmel-800">{tg}</span>
{/each}
</div>
{/if}
{#if thumbSrc && fullSrc}
<button
type="button"
onclick={() => (modalImage = { src: fullSrc, alt: imageAlt })}
class="group relative mt-6 block w-full cursor-zoom-in overflow-hidden rounded-xl border border-stein-200 bg-stein-100"
aria-label="Bild vergrößern"
>
{item.locationText}
</a>
</p>
{/if}
</div>
<img
src={thumbSrc}
alt={imageAlt}
class="mx-auto max-h-[440px] w-full object-contain transition-transform duration-300 group-hover:scale-[1.02]"
loading="lazy"
/>
<span
class="pointer-events-none absolute bottom-2 right-2 flex items-center gap-1 rounded-md bg-black/55 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100"
>
<Icon icon="mdi:magnify-plus" class="size-3.5" aria-hidden="true" /> Vergrößern
</span>
</button>
{/if}
{#if item.tags.length > 0}
<div class="mt-4 flex flex-wrap gap-1.5">
{#each item.tags as tg}
<span class="rounded-xs bg-himmel-100 px-2 py-0.5 text-xs text-himmel-800">{tg}</span>
{/each}
{#if descHtml}
<div class="prose prose-stein mt-6 max-w-none font-light leading-relaxed">
{@html descHtml}
</div>
{/if}
<div class="mt-6 flex flex-wrap gap-2">
{#if item.attachment}
<a
href={`/cms-files?src=${encodeURIComponent(item.attachment.src)}&dl=1`}
class="btn-outline btn-sm inline-flex group"
>
<Icon icon="mdi:file-download" class="size-4 shrink-0" aria-hidden="true" />
<span>{item.attachment.label}</span>
</a>
{/if}
{#if item.externalLink}
<a
href={item.externalLink}
target="_blank"
rel="noopener noreferrer"
class="btn-tertiary btn-sm inline-flex"
>
{t(T.calendar_more_info)}
<Icon icon="mdi:open-in-new" class="size-3.5" aria-hidden="true" />
</a>
{/if}
</div>
</div>
{/if}
{#if thumbSrc && fullSrc}
<button
type="button"
onclick={() => (modalImage = { src: fullSrc, alt: imageAlt })}
class="group relative mt-6 block w-full overflow-hidden rounded-md"
aria-label="Bild vergrößern"
>
<img src={thumbSrc} alt={imageAlt} class="w-full object-contain" loading="lazy" />
</button>
{/if}
<!-- Aside: Aktions-Card, sticky auf Desktop -->
<aside class="h-fit lg:sticky lg:top-24">
<div class="rounded-xl border border-stein-200 bg-stein-50 p-4">
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-stein-500">
Zum Kalender hinzufügen
</p>
<div class="grid gap-2">
<button
type="button"
onclick={() => downloadICS(toICS(), fileSlug(item.title))}
class="btn-outline btn-sm w-full justify-start"
>
<Icon icon="mdi:calendar-export" class="size-4 shrink-0" aria-hidden="true" />
.ics herunterladen
</button>
<a
href={googleCalUrl(toICS())}
target="_blank"
rel="noopener noreferrer"
class="btn-outline btn-sm w-full justify-start no-underline"
>
<Icon icon="mdi:google" class="size-4 shrink-0" aria-hidden="true" />
Google Kalender
</a>
</div>
{#if descHtml}
<div class="prose prose-stein mt-6 max-w-none font-light leading-relaxed">
{@html descHtml}
</div>
{/if}
<hr class="my-3 border-stein-200" />
{#if item.attachment}
<a
href={`/cms-files?src=${encodeURIComponent(item.attachment.src)}&dl=1`}
class="btn-outline btn-sm mt-6 inline-flex group"
>
<Icon icon="mdi:file-download" class="size-4 shrink-0" aria-hidden="true" />
<span>{item.attachment.label}</span>
</a>
{/if}
{#if item.externalLink}
<div class="mt-4">
<a
href={item.externalLink}
target="_blank"
rel="noopener noreferrer"
class="btn-tertiary btn-sm inline-flex"
>
{t(T.calendar_more_info)}
<Icon icon="mdi:open-in-new" class="size-3.5" aria-hidden="true" />
</a>
</div>
{/if}
<!-- Aktionen ungekürzt -->
<div class="mt-8 flex flex-wrap items-center gap-2 rounded-md border border-stein-200 bg-stein-50 p-2">
<button type="button" onclick={() => downloadICS(toICS(), fileSlug(item.title))} class="action-btn">
<Icon icon="mdi:download" class="size-4" /> ICS
</button>
<a href={googleCalUrl(toICS())} target="_blank" rel="noopener noreferrer" class="action-btn no-underline">
<Icon icon="mdi:google" class="size-4" /> Google
</a>
<span class="mx-1 h-5 w-px bg-stein-300" aria-hidden="true"></span>
<button type="button" onclick={copyLink} class="action-btn">
<Icon icon={copied ? "mdi:check" : "mdi:link-variant"} class="size-4" />
{copied ? t(T.post_action_copied) : "Link"}
</button>
<button type="button" onclick={share} class="action-btn">
<Icon icon={shared ? "mdi:check" : "mdi:share-variant"} class="size-4" />
{shared ? t(T.calendar_share_copied) : t(T.calendar_share)}
</button>
<button type="button" onclick={() => (qrOpen = true)} class="action-btn">
<Icon icon="mdi:qrcode" class="size-4" /> QR
</button>
<button type="button" onclick={() => (socialOpen = true)} class="action-btn">
<Icon icon="mdi:image-outline" class="size-4" /> Bild
</button>
</div>
<!-- Link zum Kalender (kein Redirect) -->
<div class="mt-10 border-t border-stein-200 pt-6">
<a
href={data.calendarUrl}
class="inline-flex items-center gap-1.5 text-sm font-medium text-wald-700 underline underline-offset-2 hover:text-wald-800"
>
<Icon icon="mdi:calendar-month" class="size-4" aria-hidden="true" />
Alle Termine im Kalender
</a>
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-stein-500">Teilen</p>
<div class="flex flex-wrap gap-1.5">
<button type="button" onclick={copyLink} class="action-btn" title={t(T.post_action_copy)}>
<Icon icon={copied ? "mdi:check" : "mdi:link-variant"} class="size-4" />
{copied ? t(T.post_action_copied) : "Link"}
</button>
<button type="button" onclick={share} class="action-btn" title={t(T.calendar_share_aria)}>
<Icon icon={shared ? "mdi:check" : "mdi:share-variant"} class="size-4" />
{shared ? t(T.calendar_share_copied) : t(T.calendar_share)}
</button>
<button type="button" onclick={() => (qrOpen = true)} class="action-btn" title="QR-Code">
<Icon icon="mdi:qrcode" class="size-4" /> QR
</button>
<button type="button" onclick={() => (socialOpen = true)} class="action-btn" title="Social-Bild">
<Icon icon="mdi:image-outline" class="size-4" /> Bild
</button>
</div>
</div>
</aside>
</div>
</article>