refactor(wind): Wind nur noch in der Kopfzeile (Karte = nur Nordpfeil) (v1.8.9)

- Wind-Anzeige von der Karte entfernt (war doppelt und nicht als "Wind" erkennbar);
  MapCompass zeigt jetzt nur den Nordpfeil
- Kopfzeile: Wind-Badge ist klickbar → Einsteller (Live vom Standort / Richtung wählen /
  entfernen), klar mit "Wind" beschriftet
- Topbar erhält onWindChange/onFetchLiveWind (nur bei Bearbeitungsrecht)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-23 22:27:48 +02:00
parent 2323f1d360
commit 405639db7d
4 changed files with 100 additions and 114 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "lageplan",
"version": "1.8.8",
"version": "1.8.9",
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
"private": true,
"scripts": {

View File

@@ -1031,6 +1031,8 @@ export default function AppPage() {
}
}}
windLive={windLive}
onWindChange={canEditMap ? (d) => handleWindChange(d) : undefined}
onFetchLiveWind={canEditMap ? handleFetchLiveWind : undefined}
isEditingByMe={isEditingByMe}
onStopEditing={handleStopEditing}
editingLoading={editingLoading}

View File

@@ -48,6 +48,8 @@ import {
ChevronDown,
Loader2,
Share2,
Wind,
Satellite,
} from 'lucide-react'
import { HoseSettingsDialog } from '@/components/dialogs/hose-settings-dialog'
import { ShareDialog } from '@/components/dialogs/share-dialog'
@@ -56,6 +58,12 @@ import type { Project, DrawFeature, ProjectMode } from '@/types'
import { formatDateTime } from '@/lib/utils'
import { Logo } from '@/components/ui/logo'
// 8-Richtungs-Auswahl (Grad, meteorologisch „Wind aus")
const WIND_OPTIONS: { label: string; deg: number }[] = [
{ label: 'N', deg: 0 }, { label: 'NO', deg: 45 }, { label: 'O', deg: 90 }, { label: 'SO', deg: 135 },
{ label: 'S', deg: 180 }, { label: 'SW', deg: 225 }, { label: 'W', deg: 270 }, { label: 'NW', deg: 315 },
]
interface TopbarProps {
project: Project | null
onNewProject: (mode?: ProjectMode) => void
@@ -82,6 +90,10 @@ interface TopbarProps {
onTogglePresentationLock?: () => void
/** Windrichtung wird live aktualisiert (für die Live-Anzeige oben) */
windLive?: boolean
/** Windrichtung setzen/entfernen (Grad, meteorologisch „Wind aus"; null = entfernen) */
onWindChange?: (dir: number | null) => void
/** Live-Wind (Open-Meteo) vom Standort holen */
onFetchLiveWind?: () => Promise<void> | void
/** true, wenn der aktuelle Nutzer den Bearbeitungs-Lock hält */
isEditingByMe?: boolean
/** Karte speichern & für andere freigeben (Lock lösen) */
@@ -115,6 +127,8 @@ export function Topbar({
presentationLocked,
onTogglePresentationLock,
windLive,
onWindChange,
onFetchLiveWind,
isEditingByMe,
onStopEditing,
editingLoading,
@@ -138,6 +152,8 @@ export function Topbar({
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
const [isDeleting, setIsDeleting] = useState(false)
const [windLoading, setWindLoading] = useState(false)
// Live clock
const [now, setNow] = useState(new Date())
useEffect(() => {
@@ -214,20 +230,70 @@ export function Topbar({
{/* Zone 3 — Uhr + Aktionen */}
<div className="flex items-center gap-1 md:gap-1.5 shrink-0">
{/* Wind — taktische Live-Anzeige (Pfeil zeigt Ausbreitungsrichtung, N oben) */}
{typeof project?.windDirection === 'number' && (
<div
className="hidden sm:flex items-center gap-1.5 rounded-md bg-blue-600 text-white pl-1.5 pr-2 py-1 shrink-0"
title={`Wind aus ${degToCompass(project.windDirection)} (${project.windDirection}°) — treibt nach ${degToCompass(project.windDirection + 180)}${windLive ? ' · live' : ''}`}
>
<svg viewBox="0 0 24 24" className="w-4 h-4 shrink-0" style={{ transform: `rotate(${project.windDirection + 180}deg)` }} aria-hidden>
<path d="M12 3 L18 16 L12 12.5 L6 16 Z" fill="currentColor" />
</svg>
<span className="text-xs font-bold leading-none whitespace-nowrap">
<span className="hidden lg:inline opacity-80 font-medium">Wind </span>aus {degToCompass(project.windDirection)}
</span>
{windLive && <span className="w-1.5 h-1.5 rounded-full bg-green-300 animate-pulse shrink-0" title="Live" />}
</div>
{/* Wind — klare, einzige Wind-Anzeige. Klick öffnet den Einsteller (Live vom Standort
oder Richtung wählen). Der Pfeil zeigt die Ausbreitungsrichtung, N oben. */}
{project && onWindChange && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{typeof project.windDirection === 'number' ? (
<button
className="hidden sm:flex items-center gap-1.5 rounded-md bg-blue-600 hover:bg-blue-700 text-white pl-1.5 pr-2 py-1 shrink-0"
title={`Wind aus ${degToCompass(project.windDirection)} (${project.windDirection}°) — treibt nach ${degToCompass(project.windDirection + 180)}. Klicken zum Ändern.`}
>
<Wind className="w-3.5 h-3.5 shrink-0 opacity-90" />
<svg viewBox="0 0 24 24" className="w-4 h-4 shrink-0" style={{ transform: `rotate(${project.windDirection + 180}deg)` }} aria-hidden>
<path d="M12 3 L18 16 L12 12.5 L6 16 Z" fill="currentColor" />
</svg>
<span className="text-xs font-bold leading-none whitespace-nowrap">
Wind aus {degToCompass(project.windDirection)}
</span>
{windLive && <span className="w-1.5 h-1.5 rounded-full bg-green-300 animate-pulse shrink-0" title="Live" />}
</button>
) : (
<Button variant="outline" size="sm" className="hidden sm:flex h-9 md:h-10 px-2 shrink-0" title="Windrichtung setzen">
<Wind className="w-4 h-4 lg:mr-1.5" />
<span className="hidden lg:inline">Wind</span>
</Button>
)}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52 p-2">
<div className="flex items-center gap-1.5 px-1 pb-1.5 text-xs font-semibold text-muted-foreground">
<Wind className="w-3.5 h-3.5" /> Windrichtung (Wind aus)
</div>
{onFetchLiveWind && (
<button
onClick={async () => { setWindLoading(true); try { await onFetchLiveWind() } finally { setWindLoading(false) } }}
disabled={windLoading}
className="w-full mb-2 flex items-center justify-center gap-1.5 rounded-md bg-blue-600 hover:bg-blue-700 text-white text-xs font-semibold py-1.5 disabled:opacity-60"
>
{windLoading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Satellite className="w-3.5 h-3.5" />}
Live vom Standort
</button>
)}
<div className="text-[10px] text-muted-foreground mb-1 px-0.5">oder Richtung wählen:</div>
<div className="grid grid-cols-4 gap-1">
{WIND_OPTIONS.map((w) => (
<button
key={w.deg}
onClick={() => onWindChange(w.deg)}
className={`h-8 rounded-md text-xs font-semibold transition-colors ${
project.windDirection === w.deg ? 'bg-blue-600 text-white' : 'bg-muted hover:bg-accent text-foreground'
}`}
>
{w.label}
</button>
))}
</div>
{typeof project.windDirection === 'number' && (
<button
onClick={() => onWindChange(null)}
className="mt-2 w-full text-xs text-muted-foreground hover:text-destructive py-1"
>
Wind entfernen
</button>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
{/* Uhr — Ops-Rooms leben nach der Uhr */}

View File

@@ -1,124 +1,42 @@
'use client'
import { useState } from 'react'
import { Wind, X, Loader2, Satellite } from 'lucide-react'
interface MapCompassProps {
/** Karten-Bearing in Grad (0 = Norden oben) */
bearing: number
/** Windrichtung meteorologisch „Wind aus" in Grad, oder null */
windDirection: number | null | undefined
canEdit: boolean
onWindChange: (dir: number | null) => void
/** Live-Wind (Open-Meteo) für den aktuellen Standort holen */
// Die folgenden Props werden nicht mehr verwendet (Wind lebt jetzt oben in der Kopfzeile),
// bleiben aber optional erhalten, damit bestehende Aufrufe nicht brechen.
windDirection?: number | null
canEdit?: boolean
onWindChange?: (dir: number | null) => void
onFetchLiveWind?: () => Promise<void> | void
}
const DIRS = ['N', 'NO', 'O', 'SO', 'S', 'SW', 'W', 'NW']
/** Grad → Kompass-Kürzel (N, NO, …). Wird auch von der Kopfzeile für die Wind-Anzeige genutzt. */
export function degToCompass(deg: number): string {
return DIRS[Math.round(((deg % 360) + 360) % 360 / 45) % 8]
}
// 8-Richtungs-Auswahl (Grad, meteorologisch „aus")
const WIND_OPTIONS: { label: string; deg: number }[] = [
{ label: 'N', deg: 0 }, { label: 'NO', deg: 45 }, { label: 'O', deg: 90 }, { label: 'SO', deg: 135 },
{ label: 'S', deg: 180 }, { label: 'SW', deg: 225 }, { label: 'W', deg: 270 }, { label: 'NW', deg: 315 },
]
export function MapCompass({ bearing, windDirection, canEdit, onWindChange, onFetchLiveWind }: MapCompassProps) {
const [open, setOpen] = useState(false)
const [loadingLive, setLoadingLive] = useState(false)
const hasWind = typeof windDirection === 'number'
const handleLive = async () => {
if (!onFetchLiveWind) return
setLoadingLive(true)
try { await onFetchLiveWind() } finally { setLoadingLive(false) }
}
/**
* Reiner Nordpfeil/Kompass auf der Karte (dreht mit dem Karten-Bearing).
* Die Windrichtung wird NICHT mehr hier angezeigt — sie lebt eindeutig in der Kopfzeile.
*/
export function MapCompass({ bearing }: MapCompassProps) {
return (
<div className="absolute top-3 left-[60px] md:left-3 z-10 flex flex-col items-center gap-1.5">
{/* Kompass mit Nordpfeil (dreht mit dem Karten-Bearing) */}
<button
type="button"
onClick={() => canEdit && setOpen((o) => !o)}
className="w-12 h-12 rounded-full bg-white/95 dark:bg-gray-900/95 shadow-md border border-black/10 dark:border-white/10 flex items-center justify-center relative"
title={canEdit ? 'Wind einstellen' : 'Nordpfeil'}
style={{ cursor: canEdit ? 'pointer' : 'default' }}
<div className="absolute top-3 left-[60px] md:left-3 z-10">
<div
className="w-12 h-12 rounded-full bg-white/95 dark:bg-gray-900/95 shadow-md border border-black/10 dark:border-white/10 flex items-center justify-center"
title="Nordpfeil"
>
<svg viewBox="0 0 40 40" className="w-10 h-10" style={{ transform: `rotate(${-bearing}deg)` }}>
{/* Nordnadel (rot) */}
<polygon points="20,5 24,21 20,18 16,21" fill="#dc2626" />
{/* Südnadel (grau) */}
<polygon points="20,35 24,19 20,22 16,19" fill="#9ca3af" />
<text x="20" y="12" textAnchor="middle" fontSize="7" fontWeight="700" fill="#dc2626" transform="rotate(0 20 20)">N</text>
<text x="20" y="12" textAnchor="middle" fontSize="7" fontWeight="700" fill="#dc2626">N</text>
</svg>
</button>
{/* Wind-Anzeige */}
{hasWind && (
<div className="flex items-center gap-1 rounded-full bg-blue-600 text-white text-[11px] font-semibold pl-1.5 pr-2 py-0.5 shadow">
<svg viewBox="0 0 24 24" className="w-3.5 h-3.5" style={{ transform: `rotate(${(windDirection as number) + 180 - bearing}deg)` }}>
{/* Pfeil zeigt in die Drift-/Ausbreitungsrichtung (wohin es weht) */}
<path d="M12 3 L18 15 L12 12 L6 15 Z" fill="currentColor" />
</svg>
<span>aus {degToCompass(windDirection as number)}</span>
</div>
)}
{!hasWind && canEdit && (
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="flex items-center gap-1 rounded-full bg-white/95 dark:bg-gray-900/95 text-[11px] font-medium text-muted-foreground pl-1.5 pr-2 py-0.5 shadow border border-black/10 dark:border-white/10"
title="Windrichtung einstellen"
>
<Wind className="w-3.5 h-3.5" /> Wind
</button>
)}
{/* Wind-Auswahl */}
{open && canEdit && (
<div className="absolute top-0 left-14 w-40 rounded-xl bg-white dark:bg-gray-900 shadow-xl border border-border p-2 z-20">
<div className="flex items-center justify-between mb-1.5 px-1">
<span className="text-xs font-semibold flex items-center gap-1"><Wind className="w-3.5 h-3.5" /> Wind aus</span>
<button onClick={() => setOpen(false)} className="text-muted-foreground hover:text-foreground"><X className="w-3.5 h-3.5" /></button>
</div>
{onFetchLiveWind && (
<button
onClick={handleLive}
disabled={loadingLive}
className="w-full mb-1.5 flex items-center justify-center gap-1.5 rounded-md bg-blue-600 hover:bg-blue-700 text-white text-xs font-semibold py-1.5 disabled:opacity-60"
title="Aktuelle Windrichtung vom Standort (Open-Meteo)"
>
{loadingLive ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Satellite className="w-3.5 h-3.5" />}
Live vom Standort
</button>
)}
<div className="text-[10px] text-muted-foreground mb-1 px-0.5">oder Richtung wählen:</div>
<div className="grid grid-cols-4 gap-1">
{WIND_OPTIONS.map((w) => (
<button
key={w.deg}
onClick={() => { onWindChange(w.deg); setOpen(false) }}
className={`h-8 rounded-md text-xs font-semibold transition-colors ${
windDirection === w.deg ? 'bg-blue-600 text-white' : 'bg-muted hover:bg-accent text-foreground'
}`}
>
{w.label}
</button>
))}
</div>
{hasWind && (
<button
onClick={() => { onWindChange(null); setOpen(false) }}
className="mt-1.5 w-full text-xs text-muted-foreground hover:text-destructive py-1"
>
Wind entfernen
</button>
)}
</div>
)}
</div>
</div>
)
}