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:
@@ -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": {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user