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:
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
import "$lib/iconify-offline";
|
import "$lib/iconify-offline";
|
||||||
import { env } from "$env/dynamic/public";
|
import { env } from "$env/dynamic/public";
|
||||||
import { useTranslate, T } from "$lib/translations";
|
import { useTranslate, T } from "$lib/translations";
|
||||||
@@ -36,6 +37,15 @@
|
|||||||
|
|
||||||
let socialModalOpen = $state(false);
|
let socialModalOpen = $state(false);
|
||||||
|
|
||||||
|
// Direkt-Link-Support: ?social=1 öffnet das Social-Bild-Modal automatisch
|
||||||
|
onMount(() => {
|
||||||
|
if (!socialSlug) return;
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.get("social") === "1") {
|
||||||
|
socialModalOpen = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
|
|
||||||
// ── Comment count fetch (only when commentPageId is set) ────────────────
|
// ── Comment count fetch (only when commentPageId is set) ────────────────
|
||||||
|
|||||||
@@ -21,6 +21,43 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let theme = $state<"wald" | "transparent-onlight" | "transparent-ondark">("wald");
|
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 themeQ = $derived(theme === "wald" ? "" : `&theme=${theme}`);
|
||||||
const ogUrl = $derived(`${endpointBase}/${slug}?format=og${themeQ}`);
|
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="text-xs uppercase tracking-wider text-stein-500">Social-Bild</p>
|
||||||
<p class="truncate text-sm font-semibold text-stein-800">{title}</p>
|
<p class="truncate text-sm font-semibold text-stein-800">{title}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex shrink-0 items-center gap-1">
|
||||||
type="button"
|
<button
|
||||||
onclick={onclose}
|
type="button"
|
||||||
class="shrink-0 text-stein-400 hover:text-stein-700"
|
onclick={copyDirectLink}
|
||||||
aria-label="Schließen"
|
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"
|
||||||
<Icon icon="mdi:close" class="size-5" />
|
aria-label="Direktlink kopieren"
|
||||||
</button>
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
<div class="mb-4 rounded-lg border border-stein-200 bg-stein-50 p-3">
|
<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="grid gap-4 sm:grid-cols-3">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-xs font-medium text-stein-600">Quer · 1200 × 630</p>
|
<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!">
|
<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" />
|
<Icon icon="mdi:download" class="size-3.5" />
|
||||||
Quer herunterladen
|
Quer herunterladen
|
||||||
@@ -116,7 +176,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-xs font-medium text-stein-600">Quadrat · 1080 × 1080</p>
|
<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!">
|
<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" />
|
<Icon icon="mdi:download" class="size-3.5" />
|
||||||
Quadrat herunterladen
|
Quadrat herunterladen
|
||||||
@@ -124,7 +195,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-xs font-medium text-stein-600">Story · 1080 × 1920</p>
|
<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!">
|
<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" />
|
<Icon icon="mdi:download" class="size-3.5" />
|
||||||
Story herunterladen
|
Story herunterladen
|
||||||
|
|||||||
@@ -540,6 +540,17 @@
|
|||||||
.getElementById(hash)
|
.getElementById(hash)
|
||||||
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Direkt-Link für Social-Bild-Modal: ?social=<slug>
|
||||||
|
// Beispiel: /kalender/?social=calendar-item-060626-...
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const socialSlug = params.get("social");
|
||||||
|
if (socialSlug) {
|
||||||
|
const item = items.find((it) => it._slug === socialSlug);
|
||||||
|
if (item) {
|
||||||
|
openSocial(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (tickTimer) clearInterval(tickTimer);
|
if (tickTimer) clearInterval(tickTimer);
|
||||||
@@ -725,9 +736,9 @@
|
|||||||
{@const imageField = extractCmsImageField(item.image)}
|
{@const imageField = extractCmsImageField(item.image)}
|
||||||
{#if imageField}
|
{#if imageField}
|
||||||
{@const thumbSrc = getCmsImageUrl(imageField.url, {
|
{@const thumbSrc = getCmsImageUrl(imageField.url, {
|
||||||
width: 160,
|
width: 240,
|
||||||
height: 160,
|
height: 240,
|
||||||
fit: "cover",
|
fit: "contain",
|
||||||
})}
|
})}
|
||||||
{@const fullSrc = getCmsImageUrl(imageField.url, {
|
{@const fullSrc = getCmsImageUrl(imageField.url, {
|
||||||
width: 1400,
|
width: 1400,
|
||||||
@@ -746,13 +757,13 @@
|
|||||||
src: fullSrc,
|
src: fullSrc,
|
||||||
alt: imageAlt,
|
alt: imageAlt,
|
||||||
})}
|
})}
|
||||||
class="group relative w-24 h-24 rounded overflow-hidden border border-stein-200 shrink-0 hover:opacity-90 transition-opacity"
|
class="group relative flex max-w-24 max-h-24 shrink-0 items-center justify-center overflow-hidden hover:opacity-90 transition-opacity"
|
||||||
aria-label="Bild vergrößern"
|
aria-label="Bild vergrößern"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={thumbSrc}
|
src={thumbSrc}
|
||||||
alt={imageAlt}
|
alt={imageAlt}
|
||||||
class="w-full h-full object-cover"
|
class="max-w-24 max-h-24 object-contain"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -854,8 +865,10 @@
|
|||||||
{@const shareKey = item._slug ?? String(item.start.getTime())}
|
{@const shareKey = item._slug ?? String(item.start.getTime())}
|
||||||
{@const copied = copyToastKey === shareKey}
|
{@const copied = copyToastKey === shareKey}
|
||||||
{@const shared = shareToastKey === shareKey}
|
{@const shared = shareToastKey === shareKey}
|
||||||
|
<!-- Force-Break vor Teilen-Gruppe auf Mobile -->
|
||||||
|
<div class="w-full sm:hidden" aria-hidden="true"></div>
|
||||||
<!-- Gruppe: Teilen -->
|
<!-- Gruppe: Teilen -->
|
||||||
<div class="inline-flex items-center gap-1 border-l border-stein-300 pl-2">
|
<div class="inline-flex items-center gap-1 sm:border-l sm:border-stein-300 sm:pl-2">
|
||||||
<span class="px-1.5 text-[10px] uppercase tracking-wide text-stein-400">Teilen</span>
|
<span class="px-1.5 text-[10px] uppercase tracking-wide text-stein-400">Teilen</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -111,9 +111,35 @@ function weekday(iso: string | undefined): string {
|
|||||||
return new Date(iso).toLocaleDateString("de-DE", { weekday: "long" });
|
return new Date(iso).toLocaleDateString("de-DE", { weekday: "long" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lädt das Event-Image (falls vorhanden) via CMS-Transform-API zugeschnitten
|
||||||
|
* auf das Ziel-Format. Liefert eine data-URL für inline-Embedding.
|
||||||
|
* satori kann nur JPEG/PNG embedden, daher format=jpeg.
|
||||||
|
*/
|
||||||
|
async function loadEventImage(
|
||||||
|
imageSrc: string | undefined,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!imageSrc) return null;
|
||||||
|
try {
|
||||||
|
const cmsBase = (env.PUBLIC_CMS_URL || "http://localhost:3000").replace(/\/$/, "");
|
||||||
|
// fit=inside: Bild wird so verkleinert dass es in w×h passt, KEIN
|
||||||
|
// Letterbox-Padding. Aspect-Ratio erhalten. Resultat ist die intrinsic
|
||||||
|
// shrink-fit Größe.
|
||||||
|
const transformUrl = `${cmsBase}/api/transform?url=${encodeURIComponent(imageSrc)}&w=${w}&h=${h}&fit=inside&format=jpeg&quality=80`;
|
||||||
|
const res = await fetch(transformUrl);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const buf = Buffer.from(await res.arrayBuffer());
|
||||||
|
return `data:image/jpeg;base64,${buf.toString("base64")}`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cache = new Map<string, { png: Buffer; ts: number }>();
|
const cache = new Map<string, { png: Buffer; ts: number }>();
|
||||||
const CACHE_TTL = 10 * 60_000; // 10min
|
const CACHE_TTL = 10 * 60_000; // 10min
|
||||||
const CACHE_VERSION = "v18";
|
const CACHE_VERSION = "v26";
|
||||||
|
|
||||||
function parseFormat(v: string | null): Format {
|
function parseFormat(v: string | null): Format {
|
||||||
if (v === "square") return "square";
|
if (v === "square") return "square";
|
||||||
@@ -148,12 +174,28 @@ export const GET: RequestHandler = async ({ params, url, setHeaders }) => {
|
|||||||
location?: unknown;
|
location?: unknown;
|
||||||
tags?: unknown;
|
tags?: unknown;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
image?: { src?: string } | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { w, h } = SIZES[formatParam];
|
const { w, h } = SIZES[formatParam];
|
||||||
const fonts = await loadFonts();
|
const fonts = await loadFonts();
|
||||||
const logoSvg = await loadLogo();
|
const logoSvg = await loadLogo();
|
||||||
|
|
||||||
|
// Event-Image (falls vorhanden) als Hintergrund. Nur wald-Theme, da
|
||||||
|
// transparent-Varianten für eigene Hintergründe gedacht sind.
|
||||||
|
const imageSrc =
|
||||||
|
typeof item.image === "string"
|
||||||
|
? item.image
|
||||||
|
: item.image && typeof item.image === "object"
|
||||||
|
? (item.image as { src?: string }).src
|
||||||
|
: undefined;
|
||||||
|
// Event-Bild als kleines quadratisches Thumbnail neben dem QR-Code
|
||||||
|
// (alle Themes — auch bei transparent hat das Sinn).
|
||||||
|
const thumbSize = formatParam === "story" ? 280 : 180;
|
||||||
|
const eventThumb = imageSrc
|
||||||
|
? await loadEventImage(imageSrc, thumbSize * 2, thumbSize * 2)
|
||||||
|
: null;
|
||||||
|
|
||||||
const title = (item.title ?? "").trim();
|
const title = (item.title ?? "").trim();
|
||||||
const { date, time } = formatDate(item.terminZeit);
|
const { date, time } = formatDate(item.terminZeit);
|
||||||
const day = weekday(item.terminZeit);
|
const day = weekday(item.terminZeit);
|
||||||
@@ -247,12 +289,17 @@ export const GET: RequestHandler = async ({ params, url, setHeaders }) => {
|
|||||||
|
|
||||||
// Format-spezifische Größen — Title-Fontsize dynamisch, damit lange Titel
|
// Format-spezifische Größen — Title-Fontsize dynamisch, damit lange Titel
|
||||||
// (≥30 Zeichen → 2+ Zeilen) keinen Overlap mit Location verursachen.
|
// (≥30 Zeichen → 2+ Zeilen) keinen Overlap mit Location verursachen.
|
||||||
|
// Bei vorhandenem Event-Thumb nimmt die rechte Spalte mehr Platz weg →
|
||||||
|
// Titel-Schrift zusätzlich verkleinern.
|
||||||
const titleLen = title.length;
|
const titleLen = title.length;
|
||||||
|
const hasThumb = !!eventThumb;
|
||||||
const titleFs = isStory
|
const titleFs = isStory
|
||||||
? titleLen > 70 ? 60 : 76
|
? titleLen > 70 ? 60 : 76
|
||||||
: isSquare
|
: isSquare
|
||||||
? titleLen > 50 ? 44 : 56
|
? titleLen > 50 ? 44 : 56
|
||||||
: titleLen > 35 ? 40 : 52;
|
: hasThumb
|
||||||
|
? titleLen > 35 ? 32 : titleLen > 20 ? 38 : 46
|
||||||
|
: titleLen > 35 ? 40 : 52;
|
||||||
const dateFs = isStory ? 130 : isSquare ? 84 : 76;
|
const dateFs = isStory ? 130 : isSquare ? 84 : 76;
|
||||||
const dayFs = isStory ? 38 : isSquare ? 26 : 26;
|
const dayFs = isStory ? 38 : isSquare ? 26 : 26;
|
||||||
const timeFs = isStory ? 56 : isSquare ? 40 : 40;
|
const timeFs = isStory ? 56 : isSquare ? 40 : 40;
|
||||||
@@ -344,12 +391,25 @@ export const GET: RequestHandler = async ({ params, url, setHeaders }) => {
|
|||||||
|
|
||||||
// OG bleibt mit rechter Spalte (align-items:center). Square/Story stack linksbündig.
|
// OG bleibt mit rechter Spalte (align-items:center). Square/Story stack linksbündig.
|
||||||
const qrAlign = verticalStack ? "flex-start" : "center";
|
const qrAlign = verticalStack ? "flex-start" : "center";
|
||||||
|
// Thumbnail komplett transparent: Wrapper hat keine Fläche, das Bild
|
||||||
|
// nimmt nur die natürliche Größe innerhalb der max-Box (qrSize+28 quadr.)
|
||||||
|
// ein. Bei Portrait → schmaler. Bei Landscape → niedriger. Kein Rahmen,
|
||||||
|
// kein Padding, kein Hintergrund.
|
||||||
|
const thumbMax = qrSize + 28;
|
||||||
|
const thumbBlock = eventThumb
|
||||||
|
? `<div style="display:flex;align-items:center;justify-content:center;max-width:${thumbMax}px;max-height:${thumbMax}px;"><img src="${eventThumb}" style="max-width:${thumbMax}px;max-height:${thumbMax}px;object-fit:contain;" /></div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// QR + (optional) Thumbnail nebeneinander (QR zuerst, dann Bild)
|
||||||
const qrBlock = `
|
const qrBlock = `
|
||||||
<div style="display:flex;flex-direction:column;align-items:${qrAlign};gap:8px;">
|
<div style="display:flex;align-items:flex-start;gap:${verticalStack ? 16 : 20}px;">
|
||||||
<div style="display:flex;background:${palette.qrBoxBg};padding:14px;border-radius:12px;">
|
<div style="display:flex;flex-direction:column;align-items:${qrAlign};gap:8px;">
|
||||||
<img src="${qrDataUrlThemed}" style="width:${qrSize}px;height:${qrSize}px;" />
|
<div style="display:flex;background:${palette.qrBoxBg};padding:14px;">
|
||||||
|
<img src="${qrDataUrlThemed}" style="width:${qrSize}px;height:${qrSize}px;" />
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;font-size:${isStory ? 22 : 16}px;color:${palette.textDim};font-weight:500;letter-spacing:0.05em;">Mehr Infos</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;font-size:${isStory ? 22 : 16}px;color:${palette.textDim};font-weight:500;letter-spacing:0.05em;">Mehr Infos</div>
|
${thumbBlock}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user