feat(termin): Detailseite-Redesign + gedeckeltes Bild
- 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:
@@ -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\"/>"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user