feat: Event-Bild im Social-Bild + UX-Verbesserungen
Social-Bild Calendar: - Termin-Bild als Thumbnail neben QR-Code (Reihenfolge: QR | Bild) - Wrapper transparent, Bild im natürlichen Aspect via object-fit:contain und fit=inside auf transform-API — kein Crop, kein Letterbox - Wachstum auf qrSize+28 begrenzt - Title-Fontsize bei vorhandenem Thumb zusätzlich verkleinert - Eckige Kanten (kein border-radius) für QR-Box und Thumb Calendar-Block: - Vorschaubild im Termin-Item gleich gerendert: object-contain, max-w-24/max-h-24, kein Crop - "Teilen"-Gruppe in Aktions-Toolbar bricht auf Mobile auf eigene Zeile um, Trennstrich nur auf sm+ sichtbar Direkt-Link: - Calendar: /kalender/?social=<slug> öffnet Modal automatisch - Post: /post/<slug>?social=1 öffnet Modal automatisch - Modal-Header: "Direktlink"-Button kopiert URL ins Clipboard Skeleton-Loader: - Animierter grauer Skeleton während Bild-Render via satori - Per Theme-Wechsel resetten, fade-in via opacity-Transition - Story-Container Fix: explizite 280x158 Maße statt aspect-ratio Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,43 @@
|
||||
} = $props();
|
||||
|
||||
let theme = $state<"wald" | "transparent-onlight" | "transparent-ondark">("wald");
|
||||
let linkCopied = $state(false);
|
||||
|
||||
/** Direktlink, der dieses Modal erneut öffnet — abhängig von endpointBase. */
|
||||
const directLink = $derived.by(() => {
|
||||
if (typeof window === "undefined") return "";
|
||||
const origin = window.location.origin;
|
||||
if (endpointBase.includes("/calendar")) {
|
||||
return `${origin}/kalender/?social=${encodeURIComponent(slug)}`;
|
||||
}
|
||||
if (endpointBase.includes("/post")) {
|
||||
return `${origin}/post/${encodeURIComponent(slug)}?social=1`;
|
||||
}
|
||||
return `${origin}/`;
|
||||
});
|
||||
|
||||
async function copyDirectLink() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(directLink);
|
||||
linkCopied = true;
|
||||
setTimeout(() => (linkCopied = false), 2000);
|
||||
} catch {
|
||||
linkCopied = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading-State pro Format. Skeleton wird angezeigt bis das jeweilige
|
||||
// Bild fertig geladen ist. Bei Theme-Wechsel resetten.
|
||||
let loadedOg = $state(false);
|
||||
let loadedSq = $state(false);
|
||||
let loadedStory = $state(false);
|
||||
$effect(() => {
|
||||
// Reset bei Theme-Änderung (neuer Request → neuer Loading-Zustand)
|
||||
void theme;
|
||||
loadedOg = false;
|
||||
loadedSq = false;
|
||||
loadedStory = false;
|
||||
});
|
||||
|
||||
const themeQ = $derived(theme === "wald" ? "" : `&theme=${theme}`);
|
||||
const ogUrl = $derived(`${endpointBase}/${slug}?format=og${themeQ}`);
|
||||
@@ -55,14 +92,26 @@
|
||||
<p class="text-xs uppercase tracking-wider text-stein-500">Social-Bild</p>
|
||||
<p class="truncate text-sm font-semibold text-stein-800">{title}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={onclose}
|
||||
class="shrink-0 text-stein-400 hover:text-stein-700"
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<Icon icon="mdi:close" class="size-5" />
|
||||
</button>
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onclick={copyDirectLink}
|
||||
class="inline-flex items-center gap-1 rounded-md border border-stein-300 bg-white px-2 py-1 text-[11px] font-medium text-stein-700 hover:bg-stein-50"
|
||||
title="Direktlink zu dieser Vorschau kopieren"
|
||||
aria-label="Direktlink kopieren"
|
||||
>
|
||||
<Icon icon={linkCopied ? "mdi:check" : "mdi:link-variant"} class="size-3.5" />
|
||||
{linkCopied ? "Kopiert" : "Direktlink"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={onclose}
|
||||
class="text-stein-400 hover:text-stein-700"
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<Icon icon="mdi:close" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 rounded-lg border border-stein-200 bg-stein-50 p-3">
|
||||
@@ -108,7 +157,18 @@
|
||||
<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 {previewBg}" loading="eager" />
|
||||
<div class="relative w-full aspect-[1200/630] rounded-sm border border-stein-200 overflow-hidden {previewBg}">
|
||||
{#if !loadedOg}
|
||||
<div class="absolute inset-0 animate-pulse bg-gradient-to-br from-stein-200 via-stein-300 to-stein-200"></div>
|
||||
{/if}
|
||||
<img
|
||||
src={ogUrl}
|
||||
alt="Social-Bild Quer"
|
||||
class="absolute inset-0 h-full w-full transition-opacity duration-200 {loadedOg ? 'opacity-100' : 'opacity-0'}"
|
||||
loading="eager"
|
||||
onload={() => (loadedOg = true)}
|
||||
/>
|
||||
</div>
|
||||
<a href={ogUrl} download={`${downloadPrefix}-${slug}-og-${theme}.png`} class="btn-outline btn-sm justify-center no-underline!">
|
||||
<Icon icon="mdi:download" class="size-3.5" />
|
||||
Quer herunterladen
|
||||
@@ -116,7 +176,18 @@
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-xs font-medium text-stein-600">Quadrat · 1080 × 1080</p>
|
||||
<img src={sqUrl} alt="Social-Bild Quadrat" class="mx-auto w-full rounded-sm border border-stein-200 {previewBg}" loading="eager" />
|
||||
<div class="relative mx-auto w-full aspect-square rounded-sm border border-stein-200 overflow-hidden {previewBg}">
|
||||
{#if !loadedSq}
|
||||
<div class="absolute inset-0 animate-pulse bg-gradient-to-br from-stein-200 via-stein-300 to-stein-200"></div>
|
||||
{/if}
|
||||
<img
|
||||
src={sqUrl}
|
||||
alt="Social-Bild Quadrat"
|
||||
class="absolute inset-0 h-full w-full transition-opacity duration-200 {loadedSq ? 'opacity-100' : 'opacity-0'}"
|
||||
loading="eager"
|
||||
onload={() => (loadedSq = true)}
|
||||
/>
|
||||
</div>
|
||||
<a href={sqUrl} download={`${downloadPrefix}-${slug}-square-${theme}.png`} class="btn-outline btn-sm justify-center no-underline!">
|
||||
<Icon icon="mdi:download" class="size-3.5" />
|
||||
Quadrat herunterladen
|
||||
@@ -124,7 +195,18 @@
|
||||
</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" />
|
||||
<div class="relative mx-auto h-[280px] w-[158px] rounded-sm border border-stein-200 overflow-hidden {previewBg}">
|
||||
{#if !loadedStory}
|
||||
<div class="absolute inset-0 animate-pulse bg-gradient-to-br from-stein-200 via-stein-300 to-stein-200"></div>
|
||||
{/if}
|
||||
<img
|
||||
src={storyUrl}
|
||||
alt="Social-Bild Story"
|
||||
class="absolute inset-0 h-full w-full transition-opacity duration-200 {loadedStory ? 'opacity-100' : 'opacity-0'}"
|
||||
loading="eager"
|
||||
onload={() => (loadedStory = true)}
|
||||
/>
|
||||
</div>
|
||||
<a href={storyUrl} download={`${downloadPrefix}-${slug}-story-${theme}.png`} class="btn-outline btn-sm justify-center no-underline!">
|
||||
<Icon icon="mdi:download" class="size-3.5" />
|
||||
Story herunterladen
|
||||
|
||||
Reference in New Issue
Block a user