Files
windwiderstand/src/lib/components/blocks/OrganisationsBlock.svelte
T
Peter Meier 57f4b226f9 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>
2026-07-12 11:59:36 +02:00

166 lines
5.8 KiB
Svelte

<script lang="ts">
import { marked } from "$lib/markdown-safe";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import { getBlockLayoutClasses } from "$lib/block-layout";
import type { OrganisationsBlockData, OrganisationEntry } from "$lib/block-types";
import Card from "$lib/components/Card.svelte";
import Button from "$lib/ui/Button.svelte";
import SectionHeader from "$lib/components/SectionHeader.svelte";
import SectionGrid from "$lib/components/SectionGrid.svelte";
import OrganisationCard from "$lib/components/OrganisationCard.svelte";
import IconButton from "$lib/components/IconButton.svelte";
let { block }: { block: OrganisationsBlockData } = $props();
const actionRef = $derived(
block.action && typeof block.action === "object" ? block.action : null,
);
const layoutClasses = $derived(
block.layout ? getBlockLayoutClasses(block.layout) : "col-span-12 mb-4",
);
const compact = $derived(block.presentation === "compact");
const carousel = $derived(block.presentation === "carousel");
// "grid" = SectionGrid + InfoCard-Tiles (Logo im Kreis, Name, Beschreibung).
const grid = $derived(block.presentation === "grid");
const orgs = $derived(block.organisations ?? []);
const ctaHtml = $derived(
block.addOrganisationMarkdown
? (marked.parse(block.addOrganisationMarkdown) as string)
: ""
);
const addOrganisationCtaLink = $derived.by(() => {
const raw = block.addOrganisationLink;
if (!raw || typeof raw !== "object") return undefined;
const url = typeof raw.url === "string" ? raw.url.trim() : "";
if (!url) return undefined;
const linkName =
typeof raw.linkName === "string" && raw.linkName.trim() !== ""
? raw.linkName.trim()
: url;
return { url, linkName, newTab: Boolean(raw.newTab) };
});
const showAddOrganisationCard = $derived(
Boolean(
block.addOrganisationTitle?.trim() ||
block.addOrganisationMarkdown?.trim() ||
addOrganisationCtaLink
)
);
let scroller: HTMLDivElement | undefined = $state();
function scrollByPage(dir: 1 | -1) {
if (!scroller) return;
scroller.scrollBy({ left: scroller.clientWidth * dir * 0.9, behavior: "smooth" });
}
</script>
{#snippet ctaCard()}
<Card
variant="callout"
href={addOrganisationCtaLink?.url}
target={addOrganisationCtaLink?.newTab ? "_blank" : undefined}
rel={addOrganisationCtaLink?.newTab ? "noopener noreferrer" : undefined}
class="h-full w-full max-w-full md:flex-row md:items-center md:justify-between md:gap-4"
>
<div class="flex flex-col gap-1.5 min-w-0">
{#if block.addOrganisationTitle}
<h5 class="text-xl font-semibold text-wald-800 leading-snug">{block.addOrganisationTitle}</h5>
{/if}
{#if ctaHtml}
<div class="markdown text-xs text-wald-700">{@html ctaHtml}</div>
{/if}
</div>
{#if addOrganisationCtaLink}
<div class="self-start md:self-center md:shrink-0">
<Button variant="primary" size="md">
<Icon icon="lucide:users" class="size-4 shrink-0" aria-hidden="true" />
{addOrganisationCtaLink.linkName}
</Button>
</div>
{/if}
</Card>
{/snippet}
<div class={layoutClasses} data-block="Organisations" data-block-type="organisations" data-block-slug={block._slug}>
{#if block.headline && !grid}
{#if compact}
<h3 class="mb-2 text-lg font-semibold text-stein-900">{block.headline}</h3>
{:else}
<div class="mb-4">
<SectionHeader
number={block.number}
kicker={block.kicker}
title={block.headline}
tag="h3"
actionLabel={actionRef?.linkName}
actionHref={actionRef?.url}
/>
</div>
{/if}
{/if}
{#if grid}
<SectionGrid
number={block.number}
kicker={block.kicker}
title={block.headline}
subtitle={block.description}
actionLabel={actionRef?.linkName}
actionHref={actionRef?.url}
columns={3}
>
{#each orgs as org, orgIndex}
{#if typeof org === "object" && org !== null}
<OrganisationCard org={org as OrganisationEntry} index={orgIndex} />
{/if}
{/each}
</SectionGrid>
{:else if carousel}
<div class="relative">
<div
bind:this={scroller}
class="flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory pt-1 pb-2 [scrollbar-width:thin]"
>
{#each orgs as org, orgIndex}
{#if typeof org === "object" && org !== null}
<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)]"
>
<OrganisationCard org={org as OrganisationEntry} index={orgIndex} />
</div>
{/if}
{/each}
</div>
{#if orgs.length > 2}
<IconButton icon="lucide:chevron-left" label="Vorherige Organisationen" variant="surface" size="sm" onclick={() => scrollByPage(-1)} class="hidden md:flex absolute -left-3 top-1/2 -translate-y-1/2 -translate-x-1/2" />
<IconButton icon="lucide:chevron-right" label="Weitere Organisationen" variant="surface" size="sm" onclick={() => scrollByPage(1)} class="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 translate-x-1/2" />
{/if}
</div>
{#if showAddOrganisationCard}
<div class="mt-2 min-w-0">
{@render ctaCard()}
</div>
{/if}
{:else}
<div
class={compact
? "grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3"
: "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"}
>
{#each orgs as org, orgIndex}
{#if typeof org === "object" && org !== null}
<OrganisationCard org={org as OrganisationEntry} {compact} index={orgIndex} />
{/if}
{/each}
{#if showAddOrganisationCard}
{@render ctaCard()}
{/if}
</div>
{/if}
</div>