feat(footer,org): Footer-Redesign + OrganisationCard auf InfoCard-Basis

Footer:
- Newsletter 2-spaltig (Titel/Desc links, Formular rechts), Desc als
  Translation-Key newsletter_inline_desc
- Link-Spalten-Überschriften als kleine Label-Kappen, Links ohne Unterstrich
- Bottom-Bar: Logo + Site-Name (data.siteName) links, Copyright/Lizenz rechts
- Site-Name-Default "Windwiderstand Thüringen" (constants.ts)

OrganisationCard:
- Neue Komponente auf InfoCard-Basis; InfoCard um href/media/badge/footer-Slots
  erweitert
- OrganisationsBlock: 4 divergierende Kartenpfade → 1 OrganisationCard
- Story + CTA-Button (mitmachen) size md + Icon

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-07-12 11:59:36 +02:00
parent 5a1a49a97b
commit 57f4b226f9
11 changed files with 328 additions and 184 deletions
+56 -5
View File
@@ -8,12 +8,26 @@
id?: string;
}
let { footer = null, translations = {} }: { footer?: FooterEntry | null; translations?: Translations | null } = $props();
let {
footer = null,
translations = {},
logoSvgHtml = null,
logoUrl = null,
siteName = "",
}: {
footer?: FooterEntry | null;
translations?: Translations | null;
logoSvgHtml?: string | null;
logoUrl?: string | null;
siteName?: string;
} = $props();
function t(key: string) {
return tStatic(translations ?? null, key);
}
const year = new Date().getFullYear();
const hasRowContent = $derived(
!!footer &&
((footer.row1Content?.length ?? 0) > 0 ||
@@ -32,13 +46,29 @@
<div class="content-footer text-sm text-stein-200">
<ContentRows layout={footer} translations={translations} />
</div>
{:else}
<p class="text-sm text-stein-0/80 text-center">
{/if}
<!-- Bottom-Bar: Brand links, Copyright/Lizenz rechts (immer, unabhängig vom CMS-Content). -->
<div
class="mt-8 flex flex-col gap-4 border-t border-stein-700 pt-6 text-xs text-stein-400 md:flex-row md:items-center md:justify-between"
>
<div class="flex items-center gap-2.5 text-stein-0">
{#if logoSvgHtml}
<span
class="footer-logo inline-flex h-6 w-auto items-center [&_svg]:block [&_svg]:h-full [&_svg]:w-auto"
aria-hidden="true">{@html logoSvgHtml}</span
>
{:else if logoUrl && logoUrl.trim() !== ""}
<img src={logoUrl} alt="" class="h-6 w-auto object-contain" />
{/if}
<span class="font-bold">{siteName}</span>
</div>
<p class="md:text-right">
{t(T.footer_copyright) !== T.footer_copyright
? t(T.footer_copyright)
: `© ${new Date().getFullYear()} Windwiderstand`}
: `Copyright © ${year} · Inhalte frei nutzbar mit Quellenangabe windwiderstand.de · CC BY 4.0`}
</p>
{/if}
</div>
</div>
</footer>
{/if}
@@ -82,6 +112,27 @@
.content-footer :global(h3) {
color: var(--color-stein-0);
}
/* Link-Listen-Überschriften ("Seiten", "Kontakt & Community") als kleine
Label-Kappen — überschreibt die unlayered h3-Defaults (daher !important). */
.content-footer :global(.link-list h3) {
font-size: 0.75rem !important;
font-weight: 600 !important;
line-height: 1.2 !important;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-stein-400) !important;
margin-bottom: 0.9rem;
}
/* Footer-Navigationslinks ohne Unterstrich (Markdown-Links bleiben unterstrichen). */
.content-footer :global(.link-list a) {
text-decoration: none !important;
}
/* Copyright/Lizenz rendert jetzt die Bottom-Bar → die CMS-Copyright-Markdown-Zeile
(letzte content-row) ausblenden, sonst doppelt. Sauberer wäre, den Block aus
dem footer-Layout (row2Content) zu entfernen — TODO im CMS. */
.content-footer :global(.content-row:last-child) {
display: none;
}
.content-footer :global(.markdown),
.content-footer :global(.prose) {
color: var(--color-stein-200);
+61 -10
View File
@@ -1,13 +1,21 @@
<script lang="ts">
/**
* InfoCard — Icon/Bild + Titel + Beschreibung + optionaler Link.
* Wiederverwendbare Kachel für Sektions-Grids (siehe SectionGrid).
* Wiederverwendbare Kachel für Sektions-Grids (siehe SectionGrid) UND als
* Basis für spezialisierte Karten (OrganisationCard) via Snippet-Slots.
*
* `icon` ist entweder ein Iconify-String ("mdi:email") ODER eine Bild-URL
* (https://… oder /…) — wird automatisch erkannt.
* `tone`: default (weiß) oder green (grün getönt, z.B. Aside-Boxen).
* `compact`: kleinere Maße + Icon inline neben dem Titel (spart Höhe).
*
* Erweiterungspunkte (halten die Basis konsistent, ohne Besonderheiten zu verlieren):
* - `href` → ganze Karte wird Link (Tile-Hover wie Card).
* - `media` → ersetzt das Standard-Icon-Badge (z.B. quadratisches Logo + Fallback).
* - `badge` → absolut oben-rechts overlay.
* - `footer` → am unteren Rand (z.B. Social-Chips).
*/
import type { Snippet } from "svelte";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import RustyImage from "$lib/components/RustyImage.svelte";
@@ -24,6 +32,12 @@
external = false,
tone = "default",
compact = false,
href = undefined,
target = undefined,
rel = undefined,
media = undefined,
badge = undefined,
footer = undefined,
}: {
icon?: string;
title: string;
@@ -33,10 +47,25 @@
external?: boolean;
tone?: Tone;
compact?: boolean;
href?: string;
target?: string;
rel?: string;
media?: Snippet;
badge?: Snippet;
footer?: Snippet;
} = $props();
const isImage = $derived(!!icon && (/^https?:\/\//.test(icon) || icon.startsWith("/")));
const hasLink = $derived(!!linkLabel && !!linkHref);
const shell = $derived(
`relative flex h-full flex-col rounded-lg border ${compact ? "gap-2 p-4" : "gap-[14px] p-[22px]"} ${
tone === "green" ? "border-wald-100 bg-wald-50" : "border-stein-200 bg-white"
}`,
);
/* Klickbare Karte: Tile-Hover identisch zu Card.svelte (variant=tile). */
const linkCls =
"transition-[transform,box-shadow,border-color] duration-200 no-underline text-inherit hover:-translate-y-0.5 hover:shadow-md hover:border-wald-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wald-500 focus-visible:ring-offset-2";
</script>
{#snippet iconBadge(sizeClass: string, iconSize: string)}
@@ -55,16 +84,18 @@
</div>
{/snippet}
<div
class="flex h-full flex-col rounded-lg border {compact ? 'gap-2 p-4' : 'gap-[14px] p-[22px]'} {tone ===
'green'
? 'border-wald-100 bg-wald-50'
: 'border-stein-200 bg-white'}"
>
{#snippet content()}
{#if badge}
<div class="absolute right-2 top-2 z-10">{@render badge()}</div>
{/if}
{#if compact}
<!-- Kompakt: Icon hängt links, Text-Spalte (Titel/Desc/Link) rechts bündig. -->
<div class="flex gap-2.5">
{#if icon}<div class="pt-px">{@render iconBadge("size-5", "size-3")}</div>{/if}
{#if media}
<div class="pt-px">{@render media()}</div>
{:else if icon}
<div class="pt-px">{@render iconBadge("size-5", "size-3")}</div>
{/if}
<div class="flex min-w-0 flex-1 flex-col gap-2">
<h3 class="text-sm! font-bold! leading-[1.3]! tracking-[-0.01em] text-stein-800">{title}</h3>
{#if description}
@@ -75,10 +106,17 @@
<ArrowLink href={linkHref ?? "#"} label={linkLabel ?? ""} {external} size="sm" />
</div>
{/if}
{#if footer}
<div class="pt-0.5">{@render footer()}</div>
{/if}
</div>
</div>
{:else}
{#if icon}{@render iconBadge("size-11", "size-[23px]")}{/if}
{#if media}
{@render media()}
{:else if icon}
{@render iconBadge("size-11", "size-[23px]")}
{/if}
<div class="flex flex-col gap-1">
<h3 class="text-base! font-bold! leading-[1.3]! tracking-[-0.01em] text-stein-800">{title}</h3>
{#if description}
@@ -90,5 +128,18 @@
<ArrowLink href={linkHref ?? "#"} label={linkLabel ?? ""} {external} size="sm" />
</div>
{/if}
{#if footer}
<div class="mt-auto pt-1">{@render footer()}</div>
{/if}
{/if}
</div>
{/snippet}
{#if href}
<a {href} {target} {rel} class="{shell} {linkCls}">
{@render content()}
</a>
{:else}
<div class={shell}>
{@render content()}
</div>
{/if}
@@ -0,0 +1,54 @@
import type { Meta, StoryObj } from '@storybook/sveltekit';
import OrganisationCard from './OrganisationCard.svelte';
import type { OrganisationEntry } from '$lib/block-types';
const LOGO =
'https://cms.pm86.de/api/assets/pages/img_2814-ar4x2.webp?_environment=windwiderstand';
const base: OrganisationEntry = {
name: 'BI Steigerwald',
description:
'Bürgerinitiative gegen überdimensionierte Windkraftplanung im Steigerwald. Aktiv seit 2021.',
link: { url: 'https://example.org' },
};
const meta = {
title: 'Molecules/OrganisationCard',
component: OrganisationCard,
tags: ['autodocs'],
parameters: { layout: 'centered' },
argTypes: {
compact: { control: 'boolean' },
},
} satisfies Meta<typeof OrganisationCard>;
export default meta;
type Story = StoryObj<typeof meta>;
// Logo aus CMS.
export const MitLogo: Story = {
args: { org: { ...base, logo: { src: LOGO } } },
};
// Ohne Logo → Fisch-Fallback.
export const FischFallback: Story = {
args: { org: { ...base, logo: undefined } },
};
// Badge oben rechts + Social-Chips.
export const BadgeUndSocial: Story = {
args: {
org: {
...base,
logo: { src: LOGO },
badge: { label: 'Neu', color: 'green' },
whatsapp: 'https://chat.whatsapp.com/xyz',
youtube: 'https://youtube.com/@example',
},
},
};
// Kompakt: Logo links, kein Social, dichteres Grid.
export const Kompakt: Story = {
args: { org: { ...base, logo: { src: LOGO } }, compact: true },
};
+123
View File
@@ -0,0 +1,123 @@
<script lang="ts">
/**
* OrganisationCard — Organisations-Kachel auf InfoCard-Basis.
* Teilt Shell/Typo/Link-Hover mit InfoCard, behält aber die Besonderheiten:
* - quadratisches Logo + Fisch-Fallback (statt rundem Icon) via media-Snippet
* - Badge oben rechts
* - Social-Chips (WhatsApp/YouTube) im footer
* - ganze Karte klickbar (org.link)
* `compact` = engere Variante (Logo links, keine Social-Chips) für dichte Grids.
*/
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import InfoCard from "$lib/components/InfoCard.svelte";
import Badge from "$lib/components/Badge.svelte";
import RustyImage from "$lib/components/RustyImage.svelte";
import { extractCmsImageField } from "$lib/rusty-image";
import type { OrganisationEntry } from "$lib/block-types";
let {
org,
compact = false,
index = 0,
}: { org: OrganisationEntry; compact?: boolean; index?: number } = $props();
const logoField = $derived(extractCmsImageField(org.logo));
const alt = $derived(
org.logo && typeof org.logo === "object" && org.logo.description ? org.logo.description : (org.name ?? ""),
);
const badge = $derived(org.badge && typeof org.badge === "object" ? org.badge : null);
const href = $derived(
typeof org.link === "object" && org.link !== null && typeof org.link.url === "string" && org.link.url.trim() !== ""
? org.link.url.trim()
: undefined,
);
const hasSocial = $derived(!compact && (!!org.whatsapp || !!org.youtube));
const fishMaskId = $derived(`org-logo-fallback-${index}`);
</script>
{#snippet media()}
<div
class="flex shrink-0 items-center justify-center overflow-hidden rounded-sm border border-stein-100 bg-stein-100 {compact
? 'h-10 w-10'
: 'h-12 w-12'}"
>
{#if logoField}
<RustyImage
src={logoField.url}
focal={logoField.focal ?? null}
width={96}
aspect="1/1"
fit="contain"
quality={85}
{alt}
class="h-full w-full object-contain p-0.5"
loading="lazy"
/>
{:else}
<svg
class="text-stein-300 {compact ? 'h-7 w-7' : 'h-8 w-8'}"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
fill="currentColor"
aria-hidden="true"
>
<defs>
<mask id={fishMaskId}>
<g fill="none">
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M24 30v14" />
<path fill="#555555" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M29 23c11 5 16 14 16 14s-12 0-21-8c-9 8-21 8-21 8s5-10 16-14c0-13 5-19 5-19s5 6 5 19" />
<circle cx="24" cy="24" r="2" fill="#fff" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask={`url(#${fishMaskId})`} />
</svg>
{/if}
</div>
{/snippet}
{#snippet badgeSlot()}
<Badge color={badge?.color as never}>{badge?.label}</Badge>
{/snippet}
{#snippet footer()}
<div class="flex gap-1.5">
{#if org.whatsapp}
<a
href={org.whatsapp}
target="_blank"
rel="noopener noreferrer"
title="WhatsApp"
onclick={(e) => e.stopPropagation()}
class="inline-flex items-center gap-1 rounded-sm border border-stein-200 bg-stein-50 px-2 py-0.5 text-[0.6rem] text-stein-600 no-underline transition-colors hover:border-green-300 hover:bg-green-50 hover:text-green-700"
>
<Icon icon="mdi:whatsapp" class="size-3 shrink-0" />
<span>WhatsApp</span>
</a>
{/if}
{#if org.youtube}
<a
href={org.youtube}
target="_blank"
rel="noopener noreferrer"
title="YouTube"
onclick={(e) => e.stopPropagation()}
class="inline-flex items-center gap-1 rounded-sm border border-stein-200 bg-stein-50 px-2 py-0.5 text-[0.6rem] text-stein-600 no-underline transition-colors hover:border-red-300 hover:bg-red-50 hover:text-red-700"
>
<Icon icon="lucide:youtube" class="size-3 shrink-0" />
<span>YouTube</span>
</a>
{/if}
</div>
{/snippet}
<InfoCard
{href}
title={org.name ?? ""}
description={org.description}
{compact}
{media}
badge={badge?.label ? badgeSlot : undefined}
footer={hasSocial ? footer : undefined}
/>
@@ -4,27 +4,13 @@
import "$lib/iconify-offline";
import { getBlockLayoutClasses } from "$lib/block-layout";
import type { OrganisationsBlockData, OrganisationEntry } from "$lib/block-types";
import { extractCmsImageField } from "$lib/rusty-image";
import Card from "$lib/components/Card.svelte";
import Badge from "$lib/components/Badge.svelte";
import RustyImage from "$lib/components/RustyImage.svelte";
import Button from "$lib/ui/Button.svelte";
import SectionHeader from "$lib/components/SectionHeader.svelte";
import SectionGrid from "$lib/components/SectionGrid.svelte";
import InfoCard from "$lib/components/InfoCard.svelte";
import OrganisationCard from "$lib/components/OrganisationCard.svelte";
import IconButton from "$lib/components/IconButton.svelte";
function organisationLogoField(o: OrganisationEntry) {
return extractCmsImageField(o.logo);
}
function organisationLogoAlt(o: OrganisationEntry): string {
if (o.logo && typeof o.logo === "object" && o.logo.description) {
return o.logo.description;
}
return o.name ?? "";
}
let { block }: { block: OrganisationsBlockData } = $props();
const actionRef = $derived(
block.action && typeof block.action === "object" ? block.action : null,
@@ -71,78 +57,6 @@
}
</script>
{#snippet defaultCard(o: OrganisationEntry, linkUrl: string | undefined, logoField: ReturnType<typeof organisationLogoField>, orgIndex: number)}
<Card href={linkUrl} class="h-full">
<div class="flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-sm border border-stein-100 bg-stein-100">
{#if logoField}
<RustyImage
src={logoField.url}
focal={logoField.focal ?? null}
width={96}
aspect="1/1"
fit="contain"
quality={85}
alt={organisationLogoAlt(o)}
class="h-full w-full object-contain p-0.5"
loading="lazy"
/>
{:else}
{@const fishMaskId = `org-logo-fallback-mask-${orgIndex}`}
<svg class="h-8 w-8 text-stein-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="currentColor" aria-hidden="true">
<defs>
<mask id={fishMaskId}>
<g fill="none">
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M24 30v14" />
<path fill="#555555" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M29 23c11 5 16 14 16 14s-12 0-21-8c-9 8-21 8-21 8s5-10 16-14c0-13 5-19 5-19s5 6 5 19" />
<circle cx="24" cy="24" r="2" fill="#fff" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask={`url(#${fishMaskId})`} />
</svg>
{/if}
</div>
{#if o.badge && typeof o.badge === "object" && o.badge.label}
<div class="absolute top-2 right-2">
<Badge color={o.badge.color}>{o.badge.label}</Badge>
</div>
{/if}
<h5 class="min-w-0 truncate text-sm font-medium leading-tight text-stein-900">{o.name ?? ""}</h5>
{#if o.description}
<p class="line-clamp-2 text-xs text-stein-500">{o.description}</p>
{/if}
{#if o.whatsapp || o.youtube}
<div class="mt-auto flex gap-1.5 pt-1">
{#if o.whatsapp}
<a
href={o.whatsapp}
target="_blank"
rel="noopener noreferrer"
title="WhatsApp"
onclick={(e) => e.stopPropagation()}
class="inline-flex items-center gap-1 rounded-sm border border-stein-200 bg-stein-50 px-2 py-0.5 text-[0.6rem] text-stein-600 no-underline transition-colors hover:border-green-300 hover:bg-green-50 hover:text-green-700"
>
<Icon icon="mdi:whatsapp" class="size-3 shrink-0" />
<span>WhatsApp</span>
</a>
{/if}
{#if o.youtube}
<a
href={o.youtube}
target="_blank"
rel="noopener noreferrer"
title="YouTube"
onclick={(e) => e.stopPropagation()}
class="inline-flex items-center gap-1 rounded-sm border border-stein-200 bg-stein-50 px-2 py-0.5 text-[0.6rem] text-stein-600 no-underline transition-colors hover:border-red-300 hover:bg-red-50 hover:text-red-700"
>
<Icon icon="lucide:youtube" class="size-3 shrink-0" />
<span>YouTube</span>
</a>
{/if}
</div>
{/if}
</Card>
{/snippet}
{#snippet ctaCard()}
<Card
@@ -162,7 +76,8 @@
</div>
{#if addOrganisationCtaLink}
<div class="self-start md:self-center md:shrink-0">
<Button variant="primary" size="sm">
<Button variant="primary" size="md">
<Icon icon="lucide:users" class="size-4 shrink-0" aria-hidden="true" />
{addOrganisationCtaLink.linkName}
</Button>
</div>
@@ -198,15 +113,9 @@
actionHref={actionRef?.url}
columns={3}
>
{#each orgs as org}
{#each orgs as org, orgIndex}
{#if typeof org === "object" && org !== null}
{@const o = org as OrganisationEntry}
{@const logoField = organisationLogoField(o)}
<InfoCard
icon={logoField?.url ?? "mdi:account-group"}
title={o.name ?? ""}
description={o.description}
/>
<OrganisationCard org={org as OrganisationEntry} index={orgIndex} />
{/if}
{/each}
</SectionGrid>
@@ -218,19 +127,10 @@
>
{#each orgs as org, orgIndex}
{#if typeof org === "object" && org !== null}
{@const o = org as OrganisationEntry}
{@const linkUrl =
typeof o.link === "object" &&
o.link !== null &&
typeof o.link.url === "string" &&
o.link.url.trim() !== ""
? o.link.url.trim()
: undefined}
{@const logoField = organisationLogoField(o)}
<div
class="shrink-0 snap-start w-[calc((100%-0.5rem)/1.5)] sm:w-[calc((100%-1rem)/2)] lg:w-[calc((100%-2rem)/3)]"
>
{@render defaultCard(o, linkUrl, logoField, orgIndex)}
<OrganisationCard org={org as OrganisationEntry} index={orgIndex} />
</div>
{/if}
{/each}
@@ -253,56 +153,7 @@
>
{#each orgs as org, orgIndex}
{#if typeof org === "object" && org !== null}
{@const o = org as OrganisationEntry}
{@const linkUrl =
typeof o.link === "object" &&
o.link !== null &&
typeof o.link.url === "string" &&
o.link.url.trim() !== ""
? o.link.url.trim()
: undefined}
{@const logoField = organisationLogoField(o)}
{#if compact}
<Card href={linkUrl} layout="inline" class="h-full">
<div class="flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden bg-stein-100">
{#if logoField}
<RustyImage
src={logoField.url}
focal={logoField.focal ?? null}
width={80}
aspect="1/1"
fit="contain"
quality={85}
alt={organisationLogoAlt(o)}
class="h-full w-full object-contain p-0.5"
loading="lazy"
/>
{:else}
{@const fishMaskId = `org-logo-fallback-mask-c-${orgIndex}`}
<svg class="h-7 w-7 text-stein-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="currentColor" aria-hidden="true">
<defs>
<mask id={fishMaskId}>
<g fill="none">
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M24 30v14" />
<path fill="#555555" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M29 23c11 5 16 14 16 14s-12 0-21-8c-9 8-21 8-21 8s5-10 16-14c0-13 5-19 5-19s5 6 5 19" />
<circle cx="24" cy="24" r="2" fill="#fff" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask={`url(#${fishMaskId})`} />
</svg>
{/if}
</div>
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<h5 class="text-sm font-medium leading-tight text-stein-900">{o.name ?? ""}</h5>
{#if o.description}
<p class="line-clamp-2 text-xs font-light text-stein-600">{o.description}</p>
{/if}
</div>
</Card>
{:else}
{@render defaultCard(o, linkUrl, logoField, orgIndex)}
{/if}
<OrganisationCard org={org as OrganisationEntry} {compact} index={orgIndex} />
{/if}
{/each}
@@ -124,13 +124,19 @@
}
</script>
<div class="max-w-md">
<h3 class="text-stein-0 text-base font-semibold">{t(T.newsletter_inline_heading)}</h3>
<div class="grid gap-6 md:grid-cols-2 md:items-center md:gap-12">
<div>
<h3 class="text-stein-0 text-2xl! font-bold! leading-tight">{t(T.newsletter_inline_heading)}</h3>
{#if t(T.newsletter_inline_desc) !== T.newsletter_inline_desc}
<p class="mt-2 max-w-sm text-sm text-stein-400">{t(T.newsletter_inline_desc)}</p>
{/if}
</div>
<div>
{#if formState === "success"}
<p class="mt-2 text-sm text-wald-200">{t(T.newsletter_success_body)}</p>
<p class="text-sm text-wald-200">{t(T.newsletter_success_body)}</p>
{:else}
<form onsubmit={handleSubmit} novalidate class="mt-3" aria-busy={formState === "pending"}>
<form onsubmit={handleSubmit} novalidate aria-busy={formState === "pending"}>
<!-- Honeypots -->
<div aria-hidden="true" class="pointer-events-none absolute left-[-9999px] h-px w-px overflow-hidden opacity-0">
<input name="_honeypot" type="text" tabindex="-1" autocomplete="off" bind:value={honeypot} />
@@ -178,4 +184,5 @@
{/if}
</form>
{/if}
</div>
</div>
+1 -1
View File
@@ -44,7 +44,7 @@ export const POST_RESOLVE = ["all"];
export const DEFAULT_HOME_PAGE_SLUG = "page-home";
/** Site-Name für og:site_name und JSON-LD (in Layout via PUBLIC_SITE_NAME überschreibbar). */
export const SITE_NAME = "Windwiderstand";
export const SITE_NAME = "Windwiderstand Thüringen";
/** Slug des Translation-Bundles im CMS (content/de/translation_bundle/{slug}.json5). Alle UI-Texte aus diesem Bundle. */
export const TRANSLATION_BUNDLE_SLUG = "app";
+4 -4
View File
@@ -84,6 +84,10 @@
"calendar-plus": {
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M16 19h6M16 2v4m3 10v6m2-9.402V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8.5M3 10h18M8 2v4\"/>"
},
"youtube": {
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M2.5 17a24.1 24.1 0 0 1 0-10a2 2 0 0 1 1.4-1.4a49.6 49.6 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.1 24.1 0 0 1 0 10a2 2 0 0 1-1.4 1.4a49.6 49.6 0 0 1-16.2 0A2 2 0 0 1 2.5 17\"/><path d=\"m10 15l5-3l-5-3z\"/></g>",
"hidden": true
},
"message-circle": {
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092a10 10 0 1 0-4.777-4.719\"/>"
},
@@ -150,10 +154,6 @@
"plus": {
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7v14\"/>"
},
"youtube": {
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"M2.5 17a24.1 24.1 0 0 1 0-10a2 2 0 0 1 1.4-1.4a49.6 49.6 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.1 24.1 0 0 1 0 10a2 2 0 0 1-1.4 1.4a49.6 49.6 0 0 1-16.2 0A2 2 0 0 1 2.5 17\"/><path d=\"m10 15l5-3l-5-3z\"/></g>",
"hidden": true
},
"alarm-clock": {
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><circle cx=\"12\" cy=\"13\" r=\"8\"/><path d=\"M12 9v4l2 2M5 3L2 6m20 0l-3-3M6.38 18.7L4 21m13.64-2.33L20 21\"/></g>"
},
+3 -3
View File
@@ -3,12 +3,12 @@
"width": 24,
"height": 24,
"icons": {
"email-outline": {
"body": "<path fill=\"currentColor\" d=\"M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2zm-2 0l-8 5l-8-5zm0 12H4V8l8 5l8-5z\"/>"
},
"whatsapp": {
"body": "<path fill=\"currentColor\" d=\"M12.04 2c-5.46 0-9.91 4.45-9.91 9.91c0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38c1.45.79 3.08 1.21 4.74 1.21c5.46 0 9.91-4.45 9.91-9.91c0-2.65-1.03-5.14-2.9-7.01A9.82 9.82 0 0 0 12.04 2m.01 1.67c2.2 0 4.26.86 5.82 2.42a8.23 8.23 0 0 1 2.41 5.83c0 4.54-3.7 8.23-8.24 8.23c-1.48 0-2.93-.39-4.19-1.15l-.3-.17l-3.12.82l.83-3.04l-.2-.32a8.2 8.2 0 0 1-1.26-4.38c.01-4.54 3.7-8.24 8.25-8.24M8.53 7.33c-.16 0-.43.06-.66.31c-.22.25-.87.86-.87 2.07c0 1.22.89 2.39 1 2.56c.14.17 1.76 2.67 4.25 3.73c.59.27 1.05.42 1.41.53c.59.19 1.13.16 1.56.1c.48-.07 1.46-.6 1.67-1.18s.21-1.07.15-1.18c-.07-.1-.23-.16-.48-.27c-.25-.14-1.47-.74-1.69-.82c-.23-.08-.37-.12-.56.12c-.16.25-.64.81-.78.97c-.15.17-.29.19-.53.07c-.26-.13-1.06-.39-2-1.23c-.74-.66-1.23-1.47-1.38-1.72c-.12-.24-.01-.39.11-.5c.11-.11.27-.29.37-.44c.13-.14.17-.25.25-.41c.08-.17.04-.31-.02-.43c-.06-.11-.56-1.35-.77-1.84c-.2-.48-.4-.42-.56-.43c-.14 0-.3-.01-.47-.01\"/>"
},
"email-outline": {
"body": "<path fill=\"currentColor\" d=\"M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2zm-2 0l-8 5l-8-5zm0 12H4V8l8 5l8-5z\"/>"
},
"youtube": {
"body": "<path fill=\"currentColor\" d=\"m10 15l5.19-3L10 9zm11.56-7.83c.13.47.22 1.1.28 1.9c.07.8.1 1.49.1 2.09L22 12c0 2.19-.16 3.8-.44 4.83c-.25.9-.83 1.48-1.73 1.73c-.47.13-1.33.22-2.65.28c-1.3.07-2.49.1-3.59.1L12 19c-4.19 0-6.8-.16-7.83-.44c-.9-.25-1.48-.83-1.73-1.73c-.13-.47-.22-1.1-.28-1.9c-.07-.8-.1-1.49-.1-2.09L2 12c0-2.19.16-3.8.44-4.83c.25-.9.83-1.48 1.73-1.73c.47-.13 1.33-.22 2.65-.28c1.3-.07 2.49-.1 3.59-.1L12 5c4.19 0 6.8.16 7.83.44c.9.25 1.48.83 1.73 1.73\"/>"
},
+1
View File
@@ -370,6 +370,7 @@ const TRANSLATION_KEYS = [
"newsletter_error_email_max", // {{n}}
"newsletter_error_consent_required",
"newsletter_inline_heading",
"newsletter_inline_desc",
"newsletter_inline_consent",
] as const;
+7 -1
View File
@@ -408,7 +408,13 @@
</main>
<div class="print:hidden">
<Footer footer={data.footerData} translations={data.translations} />
<Footer
footer={data.footerData}
translations={data.translations}
logoSvgHtml={data.logoSvgHtml}
logoUrl={data.logoUrl}
siteName={data.siteName}
/>
</div>
</div>