feat(windkarte): Stellungnahme-Kriterien per Windvorranggebiet
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:
@@ -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" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import type { WindArea } from "$lib/windkarte";
|
||||
import { useTranslate, T } from "$lib/translations";
|
||||
|
||||
let {
|
||||
areas,
|
||||
@@ -19,6 +20,8 @@
|
||||
hiddenRings?: string[];
|
||||
} = $props();
|
||||
|
||||
const t = useTranslate();
|
||||
|
||||
// Mirror prop into plain variable so Leaflet event handlers read current value.
|
||||
let _hidden: string[] = [];
|
||||
$effect(() => {
|
||||
@@ -262,11 +265,11 @@
|
||||
? `${nrLabel} – ${area.bezeichnung}`
|
||||
: nrLabel;
|
||||
const lines: string[] = [];
|
||||
if (area.flaeche_ha != null) lines.push(`Fläche: ${area.flaeche_ha.toFixed(1)} ha`);
|
||||
if (area.anlagen_geplant != null) lines.push(`Geplante Anlagen: ${area.anlagen_geplant}`);
|
||||
if (area.hoehe_max_m != null) lines.push(`Max. Nabenhöhe: ${area.hoehe_max_m} m`);
|
||||
if (area.flaeche_ha != null) lines.push(`${t(T.windkarte_label_flaeche)}: ${area.flaeche_ha.toFixed(1)} ha`);
|
||||
if (area.anlagen_geplant != null) lines.push(`${t(T.windkarte_label_anlagen_geplant)}: ${area.anlagen_geplant}`);
|
||||
if (area.hoehe_max_m != null) lines.push(`${t(T.windkarte_label_hoehe_max)}: ${area.hoehe_max_m} m`);
|
||||
if (area.gemeinden && area.gemeinden.length > 0) {
|
||||
lines.push(`Gemeinden: ${area.gemeinden.slice(0, 3).join(', ')}${area.gemeinden.length > 3 ? '…' : ''}`);
|
||||
lines.push(`${t(T.windkarte_label_gemeinden)}: ${area.gemeinden.slice(0, 3).join(', ')}${area.gemeinden.length > 3 ? '…' : ''}`);
|
||||
}
|
||||
const esc = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const tooltipHtml =
|
||||
|
||||
@@ -168,6 +168,7 @@ let areas = $state<WindArea[]>([]);
|
||||
<WindAreaPanel
|
||||
area={selectedArea}
|
||||
center={selectedCenter}
|
||||
cmsBase={CMS_BASE}
|
||||
onclose={() => handleSelect(null)}
|
||||
/>
|
||||
</div>
|
||||
@@ -184,7 +185,7 @@ let areas = $state<WindArea[]>([]);
|
||||
onclick={() => toggleStatus(item.status)}
|
||||
class="flex items-center gap-1.5 transition-opacity cursor-pointer select-none
|
||||
{hiddenStatuses.includes(item.status) ? 'opacity-35' : 'opacity-100'}"
|
||||
title={hiddenStatuses.includes(item.status) ? 'Einblenden' : 'Ausblenden'}
|
||||
title={hiddenStatuses.includes(item.status) ? t(T.windkarte_action_show) : t(T.windkarte_action_hide)}
|
||||
>
|
||||
<span class="inline-block size-2.5 rounded-sm shrink-0" style="background:{item.color}; opacity:.7"></span>
|
||||
<span class="{hiddenStatuses.includes(item.status) ? 'line-through' : ''}">{item.label}</span>
|
||||
@@ -204,7 +205,7 @@ let areas = $state<WindArea[]>([]);
|
||||
onclick={() => toggleRing(ring.key)}
|
||||
class="flex items-center gap-1.5 cursor-pointer select-none transition-opacity
|
||||
{hiddenRings.includes(ring.key) ? 'opacity-35' : 'opacity-100'}"
|
||||
title={hiddenRings.includes(ring.key) ? 'Einblenden (alle Gebiete)' : 'Ausblenden'}
|
||||
title={hiddenRings.includes(ring.key) ? t(T.windkarte_action_show) : t(T.windkarte_action_hide)}
|
||||
>
|
||||
<span class="dashed-line {hiddenRings.includes(ring.key) ? 'opacity-40' : ''}" style="border-color:{ring.color}"></span>
|
||||
<span class="{hiddenRings.includes(ring.key) ? 'line-through' : ''}">{ring.label}</span>
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
"printer-outline": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M19 8c1.66 0 3 1.34 3 3v6h-4v4H6v-4H2v-6c0-1.66 1.34-3 3-3h1V3h12v5zM8 5v3h8V5zm8 14v-4H8v4zm2-4h2v-4c0-.55-.45-1-1-1H5c-.55 0-1 .45-1 1v4h2v-2h12zm1-3.5c0 .55-.45 1-1 1s-1-.45-1-1s.45-1 1-1s1 .45 1 1\"/>"
|
||||
},
|
||||
"loading": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8\"/>"
|
||||
},
|
||||
"file-document-edit-outline": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M8 12h8v2H8zm2 8H6V4h7v5h5v3.1l2-2V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4zm-2-2h4.1l.9-.9V16H8zm12.2-5c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1l-2.1-2.1l1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z\"/>"
|
||||
},
|
||||
|
||||
@@ -211,6 +211,13 @@ const TRANSLATION_KEYS = [
|
||||
"windkarte_label_investor",
|
||||
"windkarte_label_gemeinden",
|
||||
"windkarte_label_notizen",
|
||||
"windkarte_label_kriterien",
|
||||
"windkarte_kriterien_loading",
|
||||
"windkarte_label_geodaten_export",
|
||||
"windkarte_action_download_geojson",
|
||||
"windkarte_action_download_kml",
|
||||
"windkarte_action_show",
|
||||
"windkarte_action_hide",
|
||||
"windkarte_stellungnahme_link",
|
||||
"windkarte_action_copy_link",
|
||||
"windkarte_action_copied",
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
export type TextFragment = {
|
||||
_slug: string;
|
||||
id?: string;
|
||||
title?: string;
|
||||
text?: string;
|
||||
tags?: Array<{ _slug: string; name?: string } | string>;
|
||||
};
|
||||
|
||||
export type WindArea = {
|
||||
_slug: string;
|
||||
gebiets_nr: string;
|
||||
@@ -9,6 +17,7 @@ export type WindArea = {
|
||||
investor?: string;
|
||||
gemeinden?: string[];
|
||||
stellungnahme?: { _slug: string; slug?: string } | string | null;
|
||||
stellungnahme_kriterien?: Array<{ _slug: string } | string> | null;
|
||||
notizen?: string;
|
||||
geometry?: GeoJSON.Geometry;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user