feat(organisations-block): enhance OrganisationsBlock with carousel presentation option and improved layout
- Added "carousel" option to the presentation property in OrganisationsBlockData for horizontal slider functionality. - Updated OrganisationsBlock.svelte to support carousel layout with smooth scrolling feature. - Refined Badge component styling for better visual consistency. - Improved overall structure and responsiveness of the organisations display.
This commit is contained in:
@@ -269,8 +269,8 @@ export interface OrganisationsBlockData {
|
|||||||
_slug?: string;
|
_slug?: string;
|
||||||
headline?: string;
|
headline?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
/** default = Karten-Grid; compact = engere Karten, kleinere Typo, Logo links */
|
/** default = Karten-Grid; compact = engere Karten, Logo links; carousel = horizontaler Slider (3 Desktop / 2 Mobile) */
|
||||||
presentation?: "default" | "compact";
|
presentation?: "default" | "compact" | "carousel";
|
||||||
addOrganisationTitle?: string;
|
addOrganisationTitle?: string;
|
||||||
addOrganisationMarkdown?: string;
|
addOrganisationMarkdown?: string;
|
||||||
/** Referenz auf link-Collection (aufgelöst: url, linkName, newTab) */
|
/** Referenz auf link-Collection (aufgelöst: url, linkName, newTab) */
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="shrink-0 rounded-full px-2 py-0.5 text-xs font-medium {colorClasses(color)} {className}">
|
<span class="shrink-0 rounded-full px-1.5 py-0 text-[0.65rem] font-medium {colorClasses(color)} {className}">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
|
import Icon from "@iconify/svelte";
|
||||||
|
import "$lib/iconify-offline";
|
||||||
import { getBlockLayoutClasses } from "$lib/block-layout";
|
import { getBlockLayoutClasses } from "$lib/block-layout";
|
||||||
import type { OrganisationsBlockData, OrganisationEntry } from "$lib/block-types";
|
import type { OrganisationsBlockData, OrganisationEntry } from "$lib/block-types";
|
||||||
import { extractCmsImageField } from "$lib/rusty-image";
|
import { extractCmsImageField } from "$lib/rusty-image";
|
||||||
@@ -26,6 +28,7 @@
|
|||||||
block.layout ? getBlockLayoutClasses(block.layout) : "col-span-12 mb-4",
|
block.layout ? getBlockLayoutClasses(block.layout) : "col-span-12 mb-4",
|
||||||
);
|
);
|
||||||
const compact = $derived(block.presentation === "compact");
|
const compact = $derived(block.presentation === "compact");
|
||||||
|
const carousel = $derived(block.presentation === "carousel");
|
||||||
const orgs = $derived(block.organisations ?? []);
|
const orgs = $derived(block.organisations ?? []);
|
||||||
const ctaHtml = $derived(
|
const ctaHtml = $derived(
|
||||||
block.addOrganisationMarkdown
|
block.addOrganisationMarkdown
|
||||||
@@ -54,19 +57,147 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
const addOrganisationCardClass = $derived(
|
const addOrganisationCardClass = $derived(
|
||||||
`${compact ? "border-dashed border-wald-300 bg-wald-50! p-3! gap-1.5!" : "border-dashed border-wald-300 bg-wald-50!"}${addOrganisationCtaLink ? " cursor-pointer" : ""}`,
|
`border-dashed border-wald-300 bg-wald-50! p-3! gap-1.5!${addOrganisationCtaLink ? " cursor-pointer" : ""}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let scroller: HTMLDivElement | undefined = $state();
|
||||||
|
function scrollByPage(dir: 1 | -1) {
|
||||||
|
if (!scroller) return;
|
||||||
|
scroller.scrollBy({ left: scroller.clientWidth * dir * 0.9, behavior: "smooth" });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet defaultCard(o: OrganisationEntry, linkUrl: string | undefined, logoField: ReturnType<typeof organisationLogoField>, orgIndex: number)}
|
||||||
|
<Card href={linkUrl} class="p-3! gap-1.5! h-full">
|
||||||
|
<div class="h-11 w-11 rounded-md border border-stein-100 bg-stein-50 flex items-center justify-center overflow-hidden shrink-0">
|
||||||
|
{#if logoField}
|
||||||
|
<RustyImage
|
||||||
|
src={logoField.url}
|
||||||
|
focal={logoField.focal ?? null}
|
||||||
|
width={88}
|
||||||
|
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 text-sm font-semibold leading-snug text-stein-900 truncate">{o.name ?? ""}</h5>
|
||||||
|
{#if o.description}
|
||||||
|
<p class="text-xs text-stein-600 line-clamp-2">{o.description}</p>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet ctaCard()}
|
||||||
|
<Card
|
||||||
|
href={addOrganisationCtaLink?.url}
|
||||||
|
target={addOrganisationCtaLink?.newTab ? "_blank" : undefined}
|
||||||
|
rel={addOrganisationCtaLink?.newTab ? "noopener noreferrer" : undefined}
|
||||||
|
class="{addOrganisationCardClass} h-full"
|
||||||
|
>
|
||||||
|
{#if block.addOrganisationTitle}
|
||||||
|
<h5 class="text-sm 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}
|
||||||
|
{#if addOrganisationCtaLink}
|
||||||
|
<p class="mb-0 mt-1 text-xs font-medium text-wald-800">{addOrganisationCtaLink.linkName}</p>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<div class={layoutClasses} data-block-type="organisations" data-block-slug={block._slug}>
|
<div class={layoutClasses} data-block-type="organisations" data-block-slug={block._slug}>
|
||||||
{#if block.headline}
|
{#if block.headline}
|
||||||
<h3 class={compact ? "mb-2 text-lg font-semibold text-stein-900" : "mb-4"}>{block.headline}</h3>
|
<h3 class={compact ? "mb-2 text-lg font-semibold text-stein-900" : "mb-4"}>{block.headline}</h3>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if carousel}
|
||||||
|
<div class="relative">
|
||||||
<div
|
<div
|
||||||
class={compact
|
bind:this={scroller}
|
||||||
? "grid gap-2 sm:grid-cols-2 lg:grid-cols-3"
|
class="flex gap-3 overflow-x-auto scroll-smooth snap-x snap-mandatory pb-2 -mx-2 px-2 [scrollbar-width:thin]"
|
||||||
: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3"}
|
|
||||||
>
|
>
|
||||||
|
{#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 basis-1/2 lg:basis-1/3">
|
||||||
|
{@render defaultCard(o, linkUrl, logoField, orgIndex)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if orgs.length > 2}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => scrollByPage(-1)}
|
||||||
|
aria-label="Vorherige Organisationen"
|
||||||
|
class="hidden md:flex absolute -left-3 top-1/2 -translate-y-1/2 -translate-x-1/2 size-7 items-center justify-center rounded-full bg-white border border-stein-200 shadow-md hover:bg-stein-50 transition"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:chevron-left" class="size-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => scrollByPage(1)}
|
||||||
|
aria-label="Weitere Organisationen"
|
||||||
|
class="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 translate-x-1/2 size-7 items-center justify-center rounded-full bg-white border border-stein-200 shadow-md hover:bg-stein-50 transition"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:chevron-right" class="size-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if showAddOrganisationCard}
|
||||||
|
<div class="mt-4 rounded-md border border-dashed border-wald-300 bg-wald-50 p-3 text-sm text-wald-800">
|
||||||
|
{#if block.addOrganisationTitle}
|
||||||
|
<div class="font-semibold">{block.addOrganisationTitle}</div>
|
||||||
|
{/if}
|
||||||
|
{#if ctaHtml}
|
||||||
|
<div class="markdown text-xs text-wald-700 [&>p]:mb-0">{@html ctaHtml}</div>
|
||||||
|
{/if}
|
||||||
|
{#if addOrganisationCtaLink}
|
||||||
|
<a
|
||||||
|
href={addOrganisationCtaLink.url}
|
||||||
|
target={addOrganisationCtaLink.newTab ? "_blank" : undefined}
|
||||||
|
rel={addOrganisationCtaLink.newTab ? "noopener noreferrer" : undefined}
|
||||||
|
class="mt-1 inline-block font-medium underline-offset-2 hover:underline"
|
||||||
|
>
|
||||||
|
{addOrganisationCtaLink.linkName}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class={compact ? "grid gap-2 sm:grid-cols-2 lg:grid-cols-3" : "grid gap-4 sm:grid-cols-2 lg:grid-cols-3"}>
|
||||||
{#each orgs as org, orgIndex}
|
{#each orgs as org, orgIndex}
|
||||||
{#if typeof org === "object" && org !== null}
|
{#if typeof org === "object" && org !== null}
|
||||||
{@const o = org as OrganisationEntry}
|
{@const o = org as OrganisationEntry}
|
||||||
@@ -80,9 +211,7 @@
|
|||||||
{@const logoField = organisationLogoField(o)}
|
{@const logoField = organisationLogoField(o)}
|
||||||
{#if compact}
|
{#if compact}
|
||||||
<Card href={linkUrl} class="flex! flex-row! items-start! gap-3! p-3! gap-y-0!">
|
<Card href={linkUrl} class="flex! flex-row! items-start! gap-3! p-3! gap-y-0!">
|
||||||
<div
|
<div class="h-10 w-10 rounded-md border border-stein-100 bg-stein-50 flex items-center justify-center overflow-hidden shrink-0">
|
||||||
class="h-10 w-10 rounded-md border border-stein-100 bg-stein-50 flex items-center justify-center overflow-hidden shrink-0"
|
|
||||||
>
|
|
||||||
{#if logoField}
|
{#if logoField}
|
||||||
<RustyImage
|
<RustyImage
|
||||||
src={logoField.url}
|
src={logoField.url}
|
||||||
@@ -97,31 +226,12 @@
|
|||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
{@const fishMaskId = `org-logo-fallback-mask-c-${orgIndex}`}
|
{@const fishMaskId = `org-logo-fallback-mask-c-${orgIndex}`}
|
||||||
<svg
|
<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">
|
||||||
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>
|
<defs>
|
||||||
<mask id={fishMaskId}>
|
<mask id={fishMaskId}>
|
||||||
<g fill="none">
|
<g fill="none">
|
||||||
<path
|
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M24 30v14" />
|
||||||
stroke="#fff"
|
<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" />
|
||||||
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" />
|
<circle cx="24" cy="24" r="2" fill="#fff" />
|
||||||
</g>
|
</g>
|
||||||
</mask>
|
</mask>
|
||||||
@@ -138,102 +248,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{:else}
|
{:else}
|
||||||
<Card href={linkUrl}>
|
{@render defaultCard(o, linkUrl, logoField, orgIndex)}
|
||||||
<div
|
|
||||||
class="h-14 w-14 rounded-md border border-stein-100 bg-stein-50 flex items-center justify-center overflow-hidden shrink-0"
|
|
||||||
>
|
|
||||||
{#if logoField}
|
|
||||||
<RustyImage
|
|
||||||
src={logoField.url}
|
|
||||||
focal={logoField.focal ?? null}
|
|
||||||
width={112}
|
|
||||||
aspect="1/1"
|
|
||||||
fit="contain"
|
|
||||||
quality={85}
|
|
||||||
alt={organisationLogoAlt(o)}
|
|
||||||
class="h-full w-full object-contain p-1"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
{@const fishMaskId = `org-logo-fallback-mask-${orgIndex}`}
|
|
||||||
<svg
|
|
||||||
class="h-10 w-10 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-3 right-3">
|
|
||||||
<Badge color={o.badge.color}>{o.badge.label}</Badge>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<h5 class="min-w-0 text-sm font-semibold leading-snug text-stein-900 truncate">{o.name ?? ""}</h5>
|
|
||||||
{#if o.description}
|
|
||||||
<p class="text-sm text-stein-600">{o.description}</p>
|
|
||||||
{/if}
|
|
||||||
</Card>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if showAddOrganisationCard}
|
{#if showAddOrganisationCard}
|
||||||
<Card
|
{@render ctaCard()}
|
||||||
href={addOrganisationCtaLink?.url}
|
|
||||||
target={addOrganisationCtaLink?.newTab ? "_blank" : undefined}
|
|
||||||
rel={addOrganisationCtaLink?.newTab ? "noopener noreferrer" : undefined}
|
|
||||||
class={addOrganisationCardClass}
|
|
||||||
>
|
|
||||||
{#if block.addOrganisationTitle}
|
|
||||||
<h5
|
|
||||||
class={compact
|
|
||||||
? "text-sm font-semibold text-wald-800 leading-snug"
|
|
||||||
: "text-base font-semibold text-wald-800 leading-snug"}
|
|
||||||
>
|
|
||||||
{block.addOrganisationTitle}
|
|
||||||
</h5>
|
|
||||||
{/if}
|
|
||||||
{#if ctaHtml}
|
|
||||||
<div class={compact ? "markdown text-xs text-wald-700" : "markdown text-sm text-wald-700"}>
|
|
||||||
{@html ctaHtml}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if addOrganisationCtaLink}
|
|
||||||
<p
|
|
||||||
class={compact
|
|
||||||
? "mb-0 mt-1 text-xs font-medium text-wald-800"
|
|
||||||
: "mb-0 mt-2 text-sm font-medium text-wald-800"}
|
|
||||||
>
|
|
||||||
{addOrganisationCtaLink.linkName}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</Card>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user