feat: Social-Bild erweitert (Logo, Status-Pill, Description, Story, Themes) + OG-Route + Calendar-Buttons aufgeräumt
Social-Bild-Endpoint: - Logo aus page_config (gefärbt in Theme-Akzent) statt Wordmark - Status-Pill (ONLINE/Demo/Info/Anhörung/Stammtisch/Sitzung) aus Titel-Keywords und Tags, mit eigener Farbe - Description-Snippet unter Titel (Square + Story) - Story-Format 1080x1920 ergänzt - Theme-Varianten: wald (default), transparent-onlight, transparent-ondark - Uhrzeit unter Datum (2. Zeile) mit Clock-Icon im Akzent-Grün - QR linksbündig bei Square + Story, kleiner (180px) - Location-Pin top-aligned, Title-Fontsize dynamisch bei langen Titeln Neue Route /termin/[slug]: OG-Meta-Tags für Link-Previews (FB/WhatsApp/Twitter), JS-Redirect zu /kalender/#event-<slug>. QR-Code im Social-Bild zeigt jetzt auf /termin/[slug]. Calendar-Block Aktionen reorganisiert: - Primary-Reihe: Mehr Info, Auf Karte, Anhang - Kompakte Toolbar (gerahmt): Kalender (ICS, Google) | Teilen (Link, Share, Bild) | Im Grid - Theme-Selector im Social-Modal mit Vorschau-Hintergrund-Indikator Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -447,6 +447,7 @@
|
||||
let modalImage = $state<{ src: string; alt: string } | null>(null);
|
||||
let linkedSlug = $state<string | null>(null);
|
||||
let socialModal = $state<{ slug: string; title: string } | null>(null);
|
||||
let socialTheme = $state<"wald" | "transparent-onlight" | "transparent-ondark">("wald");
|
||||
|
||||
function openSocial(item: EventCardItem) {
|
||||
if (!item._slug) return;
|
||||
@@ -767,166 +768,136 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap gap-2 pt-1 text-xs">
|
||||
{#if eventHref}
|
||||
<!-- Aktionen: Primary-Reihe (Info/Karte/Anhang) + kompakte Action-Toolbar -->
|
||||
<div class="pt-2 space-y-2">
|
||||
{#if eventHref || locText || item.attachment?.src}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#if eventHref}
|
||||
<a
|
||||
href={eventHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-tertiary btn-sm"
|
||||
>
|
||||
{t(T.calendar_more_info)}
|
||||
<Icon
|
||||
icon="mdi:open-in-new"
|
||||
class="size-3.5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</a>
|
||||
{/if}
|
||||
{#if locText}
|
||||
<a
|
||||
href={mapsUrl(locText)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={t(T.calendar_open_maps)}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:map-marker"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap">{t(T.calendar_open_maps)}</span>
|
||||
</a>
|
||||
{/if}
|
||||
{#if item.attachment?.src}
|
||||
{@const dlSrc = `/cms-files?src=${encodeURIComponent(item.attachment.src)}&dl=1`}
|
||||
<a
|
||||
href={dlSrc}
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={item.attachment.label || "Anhang herunterladen"}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:file-download"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap">{item.attachment.label || "Anhang"}</span>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Kompakte Action-Toolbar: gruppiert (Kalender · Teilen · Anzeigen) mit Separatoren -->
|
||||
<div class="flex flex-wrap items-center gap-1 rounded-md border border-stein-200 bg-stein-50 p-1">
|
||||
<!-- Kalender-Aktionen -->
|
||||
<span class="px-1.5 text-[10px] uppercase tracking-wide text-stein-400">Kalender</span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => downloadICS(toICS(item), fileSlug(title))}
|
||||
class="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[11px] text-stein-700 hover:bg-white"
|
||||
title={t(T.calendar_download_ics)}
|
||||
aria-label={t(T.calendar_download_ics)}
|
||||
>
|
||||
<Icon icon="mdi:download" class="size-3.5" />
|
||||
ICS
|
||||
</button>
|
||||
<a
|
||||
href={eventHref}
|
||||
href={googleCalUrl(toICS(item))}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-tertiary btn-sm"
|
||||
class="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[11px] text-stein-700 no-underline hover:bg-white"
|
||||
title={t(T.calendar_add_to_google)}
|
||||
>
|
||||
{t(T.calendar_more_info)}
|
||||
<Icon
|
||||
icon="mdi:open-in-new"
|
||||
class="size-3.5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Icon icon="mdi:google" class="size-3.5" />
|
||||
Google
|
||||
</a>
|
||||
{/if}
|
||||
{#if locText}
|
||||
<a
|
||||
href={mapsUrl(locText)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={t(T.calendar_open_maps)}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:map-marker"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap"
|
||||
>{t(T.calendar_open_maps)}</span
|
||||
|
||||
{#if feedHost}
|
||||
{@const shareKey = item._slug ?? String(item.start.getTime())}
|
||||
{@const copied = copyToastKey === shareKey}
|
||||
{@const shared = shareToastKey === shareKey}
|
||||
<span class="mx-1 h-4 w-px bg-stein-300" aria-hidden="true"></span>
|
||||
<span class="px-1.5 text-[10px] uppercase tracking-wide text-stein-400">Teilen</span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => copyEventLink(item)}
|
||||
class="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[11px] text-stein-700 hover:bg-white"
|
||||
title={t(T.post_action_copy)}
|
||||
aria-label={t(T.post_action_copy)}
|
||||
>
|
||||
</a>
|
||||
{/if}
|
||||
{#if item.attachment?.src}
|
||||
{@const dlSrc = `/cms-files?src=${encodeURIComponent(item.attachment.src)}&dl=1`}
|
||||
<a
|
||||
href={dlSrc}
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={item.attachment.label ||
|
||||
"Anhang herunterladen"}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:file-download"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap"
|
||||
>{item.attachment.label ||
|
||||
"Anhang herunterladen"}</span
|
||||
<Icon icon={copied ? "mdi:check" : "mdi:link-variant"} class="size-3.5" />
|
||||
{copied ? t(T.post_action_copied) : "Link"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => shareEvent(item, title, locText)}
|
||||
class="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[11px] text-stein-700 hover:bg-white"
|
||||
title={t(T.calendar_share_aria)}
|
||||
aria-label={t(T.calendar_share_aria)}
|
||||
>
|
||||
</a>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() =>
|
||||
downloadICS(toICS(item), fileSlug(title))}
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={t(T.calendar_download_ics)}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:download"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap"
|
||||
>{t(T.calendar_download_ics)}</span
|
||||
>
|
||||
</button>
|
||||
<a
|
||||
href={googleCalUrl(toICS(item))}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={t(T.calendar_add_to_google)}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:google"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap"
|
||||
>{t(T.calendar_add_to_google)}</span
|
||||
>
|
||||
</a>
|
||||
{#if feedHost}
|
||||
{@const shareKey =
|
||||
item._slug ?? String(item.start.getTime())}
|
||||
{@const copied = copyToastKey === shareKey}
|
||||
<Icon icon={shared ? "mdi:check" : "mdi:share-variant"} class="size-3.5" />
|
||||
{shared ? t(T.calendar_share_copied) : t(T.calendar_share)}
|
||||
</button>
|
||||
{#if item._slug}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => openSocial(item)}
|
||||
class="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[11px] text-stein-700 hover:bg-white"
|
||||
title="Social-Bild Vorschau"
|
||||
aria-label="Social-Bild Vorschau"
|
||||
>
|
||||
<Icon icon="mdi:image-outline" class="size-3.5" />
|
||||
Bild
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<span class="ml-auto"></span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => copyEventLink(item)}
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={t(T.post_action_copy)}
|
||||
onclick={() => jumpToEventInGrid(item)}
|
||||
class="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[11px] text-stein-500 hover:bg-white"
|
||||
title={t(T.calendar_jump_to_month)}
|
||||
aria-label={t(T.calendar_jump_to_month)}
|
||||
>
|
||||
<Icon
|
||||
icon={copied
|
||||
? "mdi:check"
|
||||
: "mdi:link-variant"}
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap"
|
||||
>{copied
|
||||
? t(T.post_action_copied)
|
||||
: t(T.post_action_copy)}</span
|
||||
>
|
||||
<Icon icon="mdi:calendar-search" class="size-3.5" />
|
||||
Im Grid
|
||||
</button>
|
||||
{@const shared = shareToastKey === shareKey}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => shareEvent(item, title, locText)}
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={t(T.calendar_share_aria)}
|
||||
>
|
||||
<Icon
|
||||
icon={shared
|
||||
? "mdi:check"
|
||||
: "mdi:share-variant"}
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap"
|
||||
>{shared
|
||||
? t(T.calendar_share_copied)
|
||||
: t(T.calendar_share)}</span
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
{#if item._slug}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => openSocial(item)}
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label="Social-Bild als Vorschau öffnen"
|
||||
title="Social-Bild Vorschau"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:image-outline"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap">Social-Bild</span>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => jumpToEventInGrid(item)}
|
||||
class="btn-outline btn-sm group"
|
||||
aria-label={t(T.calendar_jump_to_month)}
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:calendar-search"
|
||||
class="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="whitespace-nowrap"
|
||||
>{t(T.calendar_jump_to_month)}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
@@ -1323,8 +1294,16 @@
|
||||
{/if}
|
||||
|
||||
{#if socialModal}
|
||||
{@const ogUrl = `/api/social/calendar/${socialModal.slug}?format=og`}
|
||||
{@const sqUrl = `/api/social/calendar/${socialModal.slug}?format=square`}
|
||||
{@const themeQ = socialTheme === "wald" ? "" : `&theme=${socialTheme}`}
|
||||
{@const ogUrl = `/api/social/calendar/${socialModal.slug}?format=og${themeQ}`}
|
||||
{@const sqUrl = `/api/social/calendar/${socialModal.slug}?format=square${themeQ}`}
|
||||
{@const storyUrl = `/api/social/calendar/${socialModal.slug}?format=story${themeQ}`}
|
||||
{@const previewBg =
|
||||
socialTheme === "wald"
|
||||
? "bg-stein-50"
|
||||
: socialTheme === "transparent-onlight"
|
||||
? "bg-[repeating-linear-gradient(45deg,#f5f5f4,#f5f5f4_10px,#e7e5e4_10px,#e7e5e4_20px)]"
|
||||
: "bg-[repeating-linear-gradient(45deg,#292524,#292524_10px,#1c1917_10px,#1c1917_20px)]"}
|
||||
<div
|
||||
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 p-4"
|
||||
role="dialog"
|
||||
@@ -1335,7 +1314,7 @@
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="relative max-h-[92vh] w-full max-w-4xl overflow-y-auto rounded-lg bg-white p-5 shadow-2xl"
|
||||
class="relative max-h-[92vh] w-full max-w-5xl overflow-y-auto rounded-lg bg-white p-5 shadow-2xl"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
@@ -1354,18 +1333,59 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<!-- Theme-Selector -->
|
||||
<div class="mb-4 rounded-lg border border-stein-200 bg-stein-50 p-3">
|
||||
<p class="mb-2 text-[11px] font-medium uppercase tracking-wider text-stein-500">Hintergrund</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (socialTheme = "wald")}
|
||||
class="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition
|
||||
{socialTheme === 'wald' ? 'border-wald-500 bg-wald-100 text-wald-800' : 'border-stein-300 bg-white text-stein-700 hover:bg-stein-50'}"
|
||||
>
|
||||
<span class="size-3 rounded-full bg-gradient-to-br from-wald-900 to-wald-600"></span>
|
||||
Wald (Standard)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (socialTheme = "transparent-onlight")}
|
||||
class="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition
|
||||
{socialTheme === 'transparent-onlight' ? 'border-wald-500 bg-wald-100 text-wald-800' : 'border-stein-300 bg-white text-stein-700 hover:bg-stein-50'}"
|
||||
title="Transparenter Hintergrund mit dunklem Text — für eigene helle Bild-Hintergründe"
|
||||
>
|
||||
<span class="size-3 rounded-full border border-stein-300 bg-[repeating-linear-gradient(45deg,#fff,#fff_2px,#e5e5e5_2px,#e5e5e5_4px)]"></span>
|
||||
Transparent · für helle Hintergründe
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (socialTheme = "transparent-ondark")}
|
||||
class="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition
|
||||
{socialTheme === 'transparent-ondark' ? 'border-wald-500 bg-wald-100 text-wald-800' : 'border-stein-300 bg-white text-stein-700 hover:bg-stein-50'}"
|
||||
title="Transparenter Hintergrund mit hellem Text — für eigene dunkle Bild-Hintergründe"
|
||||
>
|
||||
<span class="size-3 rounded-full border border-stein-300 bg-[repeating-linear-gradient(45deg,#1c1917,#1c1917_2px,#44403c_2px,#44403c_4px)]"></span>
|
||||
Transparent · für dunkle Hintergründe
|
||||
</button>
|
||||
</div>
|
||||
{#if socialTheme !== "wald"}
|
||||
<p class="mt-2 text-[10px] text-stein-500">
|
||||
Vorschau-Hintergrund zeigt nur, wie der Text aussieht — die heruntergeladene PNG hat echten Alpha-Kanal.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-xs font-medium text-stein-600">Quer · 1200 × 630</p>
|
||||
<img
|
||||
src={ogUrl}
|
||||
alt="Social-Bild Quer"
|
||||
class="w-full rounded-sm border border-stein-200 bg-stein-50"
|
||||
class="w-full rounded-sm border border-stein-200 {previewBg}"
|
||||
loading="eager"
|
||||
/>
|
||||
<a
|
||||
href={ogUrl}
|
||||
download={`termin-${socialModal.slug}-og.png`}
|
||||
download={`termin-${socialModal.slug}-og-${socialTheme}.png`}
|
||||
class="btn-outline btn-sm justify-center no-underline!"
|
||||
>
|
||||
<Icon icon="mdi:download" class="size-3.5" />
|
||||
@@ -1377,22 +1397,40 @@
|
||||
<img
|
||||
src={sqUrl}
|
||||
alt="Social-Bild Quadrat"
|
||||
class="mx-auto w-full max-w-[300px] rounded-sm border border-stein-200 bg-stein-50 sm:max-w-none"
|
||||
class="mx-auto w-full rounded-sm border border-stein-200 {previewBg}"
|
||||
loading="eager"
|
||||
/>
|
||||
<a
|
||||
href={sqUrl}
|
||||
download={`termin-${socialModal.slug}-square.png`}
|
||||
download={`termin-${socialModal.slug}-square-${socialTheme}.png`}
|
||||
class="btn-outline btn-sm justify-center no-underline!"
|
||||
>
|
||||
<Icon icon="mdi:download" class="size-3.5" />
|
||||
Quadrat herunterladen
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-xs font-medium text-stein-600">Story · 1080 × 1920</p>
|
||||
<img
|
||||
src={storyUrl}
|
||||
alt="Social-Bild Story"
|
||||
class="mx-auto w-auto max-h-[280px] rounded-sm border border-stein-200 {previewBg}"
|
||||
loading="eager"
|
||||
/>
|
||||
<a
|
||||
href={storyUrl}
|
||||
download={`termin-${socialModal.slug}-story-${socialTheme}.png`}
|
||||
class="btn-outline btn-sm justify-center no-underline!"
|
||||
>
|
||||
<Icon icon="mdi:download" class="size-3.5" />
|
||||
Story herunterladen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-[11px] text-stein-400">
|
||||
Die Bilder werden automatisch generiert. Für Posts auf Instagram, Facebook, WhatsApp oder als Hintergrund-Grafik.
|
||||
Quer: Facebook/Twitter/Link-Preview. Quadrat: Instagram-Feed. Story: Instagram/WhatsApp Story.
|
||||
Transparent-Varianten lassen sich als Overlay auf eigene Hintergründe in Stories einsetzen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user