From 57f4b226f9469df8c0be8551fa51ae4a04a2d26f Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Sun, 12 Jul 2026 11:59:36 +0200 Subject: [PATCH] feat(footer,org): Footer-Redesign + OrganisationCard auf InfoCard-Basis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/lib/components/Footer.svelte | 61 ++++++- src/lib/components/InfoCard.svelte | 71 ++++++-- .../components/OrganisationCard.stories.ts | 54 ++++++ src/lib/components/OrganisationCard.svelte | 123 +++++++++++++ .../blocks/OrganisationsBlock.svelte | 163 +----------------- .../NewsletterInlineFormComponent.svelte | 15 +- src/lib/constants.ts | 2 +- src/lib/iconify-lucide-subset.generated.json | 8 +- src/lib/iconify-mdi-subset.generated.json | 6 +- src/lib/translations.ts | 1 + src/routes/+layout.svelte | 8 +- 11 files changed, 328 insertions(+), 184 deletions(-) create mode 100644 src/lib/components/OrganisationCard.stories.ts create mode 100644 src/lib/components/OrganisationCard.svelte diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte index 774a892..ca3b350 100644 --- a/src/lib/components/Footer.svelte +++ b/src/lib/components/Footer.svelte @@ -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 @@ - {:else} -

+ {/if} + + +

+
+ {#if logoSvgHtml} + + {:else if logoUrl && logoUrl.trim() !== ""} + + {/if} + {siteName} +
+

{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`}

- {/if} +
{/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); diff --git a/src/lib/components/InfoCard.svelte b/src/lib/components/InfoCard.svelte index e26cd74..cddbba2 100644 --- a/src/lib/components/InfoCard.svelte +++ b/src/lib/components/InfoCard.svelte @@ -1,13 +1,21 @@ {#snippet iconBadge(sizeClass: string, iconSize: string)} @@ -55,16 +84,18 @@ {/snippet} -
+{#snippet content()} + {#if badge} +
{@render badge()}
+ {/if} {#if compact}
- {#if icon}
{@render iconBadge("size-5", "size-3")}
{/if} + {#if media} +
{@render media()}
+ {:else if icon} +
{@render iconBadge("size-5", "size-3")}
+ {/if}

{title}

{#if description} @@ -75,10 +106,17 @@
{/if} + {#if footer} +
{@render footer()}
+ {/if}
{:else} - {#if icon}{@render iconBadge("size-11", "size-[23px]")}{/if} + {#if media} + {@render media()} + {:else if icon} + {@render iconBadge("size-11", "size-[23px]")} + {/if}

{title}

{#if description} @@ -90,5 +128,18 @@
{/if} + {#if footer} +
{@render footer()}
+ {/if} {/if} - +{/snippet} + +{#if href} + + {@render content()} + +{:else} +
+ {@render content()} +
+{/if} diff --git a/src/lib/components/OrganisationCard.stories.ts b/src/lib/components/OrganisationCard.stories.ts new file mode 100644 index 0000000..e8aa097 --- /dev/null +++ b/src/lib/components/OrganisationCard.stories.ts @@ -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; + +export default meta; +type Story = StoryObj; + +// 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 }, +}; diff --git a/src/lib/components/OrganisationCard.svelte b/src/lib/components/OrganisationCard.svelte new file mode 100644 index 0000000..533bac4 --- /dev/null +++ b/src/lib/components/OrganisationCard.svelte @@ -0,0 +1,123 @@ + + +{#snippet media()} +
+ {#if logoField} + + {:else} + + {/if} +
+{/snippet} + +{#snippet badgeSlot()} + {badge?.label} +{/snippet} + +{#snippet footer()} + +{/snippet} + + diff --git a/src/lib/components/blocks/OrganisationsBlock.svelte b/src/lib/components/blocks/OrganisationsBlock.svelte index f773a95..453bcb3 100644 --- a/src/lib/components/blocks/OrganisationsBlock.svelte +++ b/src/lib/components/blocks/OrganisationsBlock.svelte @@ -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 @@ } -{#snippet defaultCard(o: OrganisationEntry, linkUrl: string | undefined, logoField: ReturnType, orgIndex: number)} - -
- {#if logoField} - - {:else} - {@const fishMaskId = `org-logo-fallback-mask-${orgIndex}`} - - {/if} -
- {#if o.badge && typeof o.badge === "object" && o.badge.label} -
- {o.badge.label} -
- {/if} -
{o.name ?? ""}
- {#if o.description} -

{o.description}

- {/if} - {#if o.whatsapp || o.youtube} - - {/if} -
-{/snippet} {#snippet ctaCard()} {#if addOrganisationCtaLink}
-
@@ -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)} - + {/if} {/each} @@ -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)}
- {@render defaultCard(o, linkUrl, logoField, orgIndex)} +
{/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} - -
- {#if logoField} - - {:else} - {@const fishMaskId = `org-logo-fallback-mask-c-${orgIndex}`} - - {/if} -
-
-
{o.name ?? ""}
- {#if o.description} -

{o.description}

- {/if} -
-
- {:else} - {@render defaultCard(o, linkUrl, logoField, orgIndex)} - {/if} + {/if} {/each} diff --git a/src/lib/components/internal/NewsletterInlineFormComponent.svelte b/src/lib/components/internal/NewsletterInlineFormComponent.svelte index 5b6052d..11141df 100644 --- a/src/lib/components/internal/NewsletterInlineFormComponent.svelte +++ b/src/lib/components/internal/NewsletterInlineFormComponent.svelte @@ -124,13 +124,19 @@ } -
-

{t(T.newsletter_inline_heading)}

+
+
+

{t(T.newsletter_inline_heading)}

+ {#if t(T.newsletter_inline_desc) !== T.newsletter_inline_desc} +

{t(T.newsletter_inline_desc)}

+ {/if} +
+
{#if formState === "success"} -

{t(T.newsletter_success_body)}

+

{t(T.newsletter_success_body)}

{:else} -
+
diff --git a/src/lib/constants.ts b/src/lib/constants.ts index c386c29..2b07558 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -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"; diff --git a/src/lib/iconify-lucide-subset.generated.json b/src/lib/iconify-lucide-subset.generated.json index 0c59b31..449a026 100644 --- a/src/lib/iconify-lucide-subset.generated.json +++ b/src/lib/iconify-lucide-subset.generated.json @@ -84,6 +84,10 @@ "calendar-plus": { "body": "" }, + "youtube": { + "body": "", + "hidden": true + }, "message-circle": { "body": "" }, @@ -150,10 +154,6 @@ "plus": { "body": "" }, - "youtube": { - "body": "", - "hidden": true - }, "alarm-clock": { "body": "" }, diff --git a/src/lib/iconify-mdi-subset.generated.json b/src/lib/iconify-mdi-subset.generated.json index 839dcc4..31ab19b 100644 --- a/src/lib/iconify-mdi-subset.generated.json +++ b/src/lib/iconify-mdi-subset.generated.json @@ -3,12 +3,12 @@ "width": 24, "height": 24, "icons": { - "email-outline": { - "body": "" - }, "whatsapp": { "body": "" }, + "email-outline": { + "body": "" + }, "youtube": { "body": "" }, diff --git a/src/lib/translations.ts b/src/lib/translations.ts index d033e33..1091788 100644 --- a/src/lib/translations.ts +++ b/src/lib/translations.ts @@ -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; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2b7888b..c380e5f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -408,7 +408,13 @@
-