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:
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user