feat(windkarte): Stellungnahme-Kriterien per Windvorranggebiet
Deploy / verify (push) Successful in 56s
Deploy / deploy (push) Successful in 1m38s

Jedes Windvorranggebiet kann jetzt 1–n text_fragment-Einträge als
Stellungnahme-Kriterien referenzieren. Das Panel lädt diese lazy beim
Öffnen und zeigt sie als aufklappbare Accordion-Sektion.

Außerdem: alle hardcodierten Strings im Karten-Block (Panel, Map,
Block) durch t()-Aufrufe ersetzt; 7 neue Translation-Keys ergänzt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-05-20 14:22:35 +02:00
parent a3f545d727
commit 8e43169b23
6 changed files with 99 additions and 14 deletions
+70 -8
View File
@@ -1,16 +1,18 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import type { WindArea } from "$lib/windkarte";
import type { WindArea, TextFragment } from "$lib/windkarte";
import { useTranslate, T } from "$lib/translations";
let {
area,
center,
cmsBase,
onclose,
}: {
area: WindArea | null;
center: [number, number] | null;
cmsBase: string;
onclose: () => void;
} = $props();
@@ -34,6 +36,33 @@
return null;
}
let kriterien = $state<TextFragment[]>([]);
let loadingKriterien = $state(false);
let expandedSlug = $state<string | null>(null);
$effect(() => {
const slugs = (area?.stellungnahme_kriterien ?? [])
.map((k) => (typeof k === "string" ? k : k._slug))
.filter(Boolean);
kriterien = [];
expandedSlug = null;
if (slugs.length === 0) return;
loadingKriterien = true;
Promise.all(
slugs.map((slug) =>
fetch(`${cmsBase}/api/content/text_fragment/${slug}`)
.then((r) => (r.ok ? r.json() : null))
.catch(() => null),
),
).then((results) => {
kriterien = results.filter(Boolean) as TextFragment[];
loadingKriterien = false;
});
});
let copied = $state(false);
async function copyLink() {
await navigator.clipboard.writeText(location.href);
@@ -49,10 +78,10 @@
const exportInfo = $derived(
area
? [
`Gebiet ${area.gebiets_nr}${area.bezeichnung ? ` ${area.bezeichnung}` : ''}`,
area.flaeche_ha != null ? `Fläche: ${area.flaeche_ha.toFixed(1)} ha` : null,
area.anlagen_geplant != null ? `Geplante Anlagen: ${area.anlagen_geplant}` : null,
area.hoehe_max_m != null ? `Max. Nabenhöhe: ${area.hoehe_max_m} m` : null,
`${area.gebiets_nr}${area.bezeichnung ? ` ${area.bezeichnung}` : ''}`,
area.flaeche_ha != null ? `${t(T.windkarte_label_flaeche)}: ${area.flaeche_ha.toFixed(1)} ha` : null,
area.anlagen_geplant != null ? `${t(T.windkarte_label_anlagen_geplant)}: ${area.anlagen_geplant}` : null,
area.hoehe_max_m != null ? `${t(T.windkarte_label_hoehe_max)}: ${area.hoehe_max_m} m` : null,
]
.filter(Boolean)
.join('\n')
@@ -136,6 +165,39 @@
</div>
{/if}
<!-- Stellungnahme-Kriterien -->
{#if loadingKriterien}
<div class="flex items-center gap-2 text-xs text-stein-400">
<Icon icon="mdi:loading" class="size-3.5 animate-spin" />
<span>{t(T.windkarte_kriterien_loading)}</span>
</div>
{:else if kriterien.length > 0}
<div>
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-stein-500">{t(T.windkarte_label_kriterien)}</p>
<div class="divide-y divide-stein-100 rounded-lg border border-stein-100 overflow-hidden">
{#each kriterien as k (k._slug)}
<div>
<button
onclick={() => { expandedSlug = expandedSlug === k._slug ? null : k._slug; }}
class="flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm font-medium text-stein-700 hover:bg-stein-50 transition-colors"
>
<span class="min-w-0 truncate">{k.title ?? k._slug}</span>
<Icon
icon={expandedSlug === k._slug ? "mdi:chevron-up" : "mdi:chevron-down"}
class="size-4 shrink-0 text-stein-400"
/>
</button>
{#if expandedSlug === k._slug && k.text}
<div class="px-3 pb-3 pt-1 text-sm text-stein-700 leading-relaxed markdown prose prose-sm prose-zinc max-w-none border-t border-stein-100">
{@html k.text}
</div>
{/if}
</div>
{/each}
</div>
</div>
{/if}
<!-- Stellungnahme -->
{#if stellungnahmeHref(area.stellungnahme)}
<a
@@ -171,12 +233,12 @@
<!-- Export-Downloads -->
<div>
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-stein-500">Geodaten-Export ({area.gebiets_nr})</p>
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-stein-500">{t(T.windkarte_label_geodaten_export, { nr: area.gebiets_nr })}</p>
<div class="flex gap-2">
<a
href="/api/vorranggebiete.geojson?id={encodeURIComponent(area.gebiets_nr)}"
download
title={`GeoJSON herunterladen\n${exportInfo}`}
title={`${t(T.windkarte_action_download_geojson)}\n${exportInfo}`}
class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-stein-200 px-2 py-2 text-xs font-medium text-stein-600 no-underline hover:bg-stein-50 transition-colors whitespace-nowrap"
>
<Icon icon="mdi:download" class="size-3.5 shrink-0" />
@@ -185,7 +247,7 @@
<a
href="/api/vorranggebiete.kml?id={encodeURIComponent(area.gebiets_nr)}"
download
title={`KML herunterladen\n${exportInfo}`}
title={`${t(T.windkarte_action_download_kml)}\n${exportInfo}`}
class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-stein-200 px-2 py-2 text-xs font-medium text-stein-600 no-underline hover:bg-stein-50 transition-colors whitespace-nowrap"
>
<Icon icon="mdi:download" class="size-3.5 shrink-0" />