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",
|
"name": "lageplan",
|
||||||
"version": "1.8.8",
|
"version": "1.8.9",
|
||||||
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1031,6 +1031,8 @@ export default function AppPage() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
windLive={windLive}
|
windLive={windLive}
|
||||||
|
onWindChange={canEditMap ? (d) => handleWindChange(d) : undefined}
|
||||||
|
onFetchLiveWind={canEditMap ? handleFetchLiveWind : undefined}
|
||||||
isEditingByMe={isEditingByMe}
|
isEditingByMe={isEditingByMe}
|
||||||
onStopEditing={handleStopEditing}
|
onStopEditing={handleStopEditing}
|
||||||
editingLoading={editingLoading}
|
editingLoading={editingLoading}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
Loader2,
|
Loader2,
|
||||||
Share2,
|
Share2,
|
||||||
|
Wind,
|
||||||
|
Satellite,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { HoseSettingsDialog } from '@/components/dialogs/hose-settings-dialog'
|
import { HoseSettingsDialog } from '@/components/dialogs/hose-settings-dialog'
|
||||||
import { ShareDialog } from '@/components/dialogs/share-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 { formatDateTime } from '@/lib/utils'
|
||||||
import { Logo } from '@/components/ui/logo'
|
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 {
|
interface TopbarProps {
|
||||||
project: Project | null
|
project: Project | null
|
||||||
onNewProject: (mode?: ProjectMode) => void
|
onNewProject: (mode?: ProjectMode) => void
|
||||||
@@ -82,6 +90,10 @@ interface TopbarProps {
|
|||||||
onTogglePresentationLock?: () => void
|
onTogglePresentationLock?: () => void
|
||||||
/** Windrichtung wird live aktualisiert (für die Live-Anzeige oben) */
|
/** Windrichtung wird live aktualisiert (für die Live-Anzeige oben) */
|
||||||
windLive?: boolean
|
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 */
|
/** true, wenn der aktuelle Nutzer den Bearbeitungs-Lock hält */
|
||||||
isEditingByMe?: boolean
|
isEditingByMe?: boolean
|
||||||
/** Karte speichern & für andere freigeben (Lock lösen) */
|
/** Karte speichern & für andere freigeben (Lock lösen) */
|
||||||
@@ -115,6 +127,8 @@ export function Topbar({
|
|||||||
presentationLocked,
|
presentationLocked,
|
||||||
onTogglePresentationLock,
|
onTogglePresentationLock,
|
||||||
windLive,
|
windLive,
|
||||||
|
onWindChange,
|
||||||
|
onFetchLiveWind,
|
||||||
isEditingByMe,
|
isEditingByMe,
|
||||||
onStopEditing,
|
onStopEditing,
|
||||||
editingLoading,
|
editingLoading,
|
||||||
@@ -138,6 +152,8 @@ export function Topbar({
|
|||||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
|
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
|
||||||
const [isDeleting, setIsDeleting] = useState(false)
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
|
|
||||||
|
const [windLoading, setWindLoading] = useState(false)
|
||||||
|
|
||||||
// Live clock
|
// Live clock
|
||||||
const [now, setNow] = useState(new Date())
|
const [now, setNow] = useState(new Date())
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -214,20 +230,70 @@ export function Topbar({
|
|||||||
|
|
||||||
{/* Zone 3 — Uhr + Aktionen */}
|
{/* Zone 3 — Uhr + Aktionen */}
|
||||||
<div className="flex items-center gap-1 md:gap-1.5 shrink-0">
|
<div className="flex items-center gap-1 md:gap-1.5 shrink-0">
|
||||||
{/* Wind — taktische Live-Anzeige (Pfeil zeigt Ausbreitungsrichtung, N oben) */}
|
{/* Wind — klare, einzige Wind-Anzeige. Klick öffnet den Einsteller (Live vom Standort
|
||||||
{typeof project?.windDirection === 'number' && (
|
oder Richtung wählen). Der Pfeil zeigt die Ausbreitungsrichtung, N oben. */}
|
||||||
<div
|
{project && onWindChange && (
|
||||||
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"
|
<DropdownMenu>
|
||||||
title={`Wind aus ${degToCompass(project.windDirection)} (${project.windDirection}°) — treibt nach ${degToCompass(project.windDirection + 180)}${windLive ? ' · live' : ''}`}
|
<DropdownMenuTrigger asChild>
|
||||||
>
|
{typeof project.windDirection === 'number' ? (
|
||||||
<svg viewBox="0 0 24 24" className="w-4 h-4 shrink-0" style={{ transform: `rotate(${project.windDirection + 180}deg)` }} aria-hidden>
|
<button
|
||||||
<path d="M12 3 L18 16 L12 12.5 L6 16 Z" fill="currentColor" />
|
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"
|
||||||
</svg>
|
title={`Wind aus ${degToCompass(project.windDirection)} (${project.windDirection}°) — treibt nach ${degToCompass(project.windDirection + 180)}. Klicken zum Ändern.`}
|
||||||
<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)}
|
<Wind className="w-3.5 h-3.5 shrink-0 opacity-90" />
|
||||||
</span>
|
<svg viewBox="0 0 24 24" className="w-4 h-4 shrink-0" style={{ transform: `rotate(${project.windDirection + 180}deg)` }} aria-hidden>
|
||||||
{windLive && <span className="w-1.5 h-1.5 rounded-full bg-green-300 animate-pulse shrink-0" title="Live" />}
|
<path d="M12 3 L18 16 L12 12.5 L6 16 Z" fill="currentColor" />
|
||||||
</div>
|
</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 */}
|
{/* Uhr — Ops-Rooms leben nach der Uhr */}
|
||||||
|
|||||||
@@ -1,124 +1,42 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { Wind, X, Loader2, Satellite } from 'lucide-react'
|
|
||||||
|
|
||||||
interface MapCompassProps {
|
interface MapCompassProps {
|
||||||
/** Karten-Bearing in Grad (0 = Norden oben) */
|
/** Karten-Bearing in Grad (0 = Norden oben) */
|
||||||
bearing: number
|
bearing: number
|
||||||
/** Windrichtung meteorologisch „Wind aus" in Grad, oder null */
|
// Die folgenden Props werden nicht mehr verwendet (Wind lebt jetzt oben in der Kopfzeile),
|
||||||
windDirection: number | null | undefined
|
// bleiben aber optional erhalten, damit bestehende Aufrufe nicht brechen.
|
||||||
canEdit: boolean
|
windDirection?: number | null
|
||||||
onWindChange: (dir: number | null) => void
|
canEdit?: boolean
|
||||||
/** Live-Wind (Open-Meteo) für den aktuellen Standort holen */
|
onWindChange?: (dir: number | null) => void
|
||||||
onFetchLiveWind?: () => Promise<void> | void
|
onFetchLiveWind?: () => Promise<void> | void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIRS = ['N', 'NO', 'O', 'SO', 'S', 'SW', 'W', 'NW']
|
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 {
|
export function degToCompass(deg: number): string {
|
||||||
return DIRS[Math.round(((deg % 360) + 360) % 360 / 45) % 8]
|
return DIRS[Math.round(((deg % 360) + 360) % 360 / 45) % 8]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8-Richtungs-Auswahl (Grad, meteorologisch „aus")
|
/**
|
||||||
const WIND_OPTIONS: { label: string; deg: number }[] = [
|
* Reiner Nordpfeil/Kompass auf der Karte (dreht mit dem Karten-Bearing).
|
||||||
{ label: 'N', deg: 0 }, { label: 'NO', deg: 45 }, { label: 'O', deg: 90 }, { label: 'SO', deg: 135 },
|
* Die Windrichtung wird NICHT mehr hier angezeigt — sie lebt eindeutig in der Kopfzeile.
|
||||||
{ label: 'S', deg: 180 }, { label: 'SW', deg: 225 }, { label: 'W', deg: 270 }, { label: 'NW', deg: 315 },
|
*/
|
||||||
]
|
export function MapCompass({ bearing }: MapCompassProps) {
|
||||||
|
|
||||||
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) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-3 left-[60px] md:left-3 z-10 flex flex-col items-center gap-1.5">
|
<div className="absolute top-3 left-[60px] md:left-3 z-10">
|
||||||
{/* Kompass mit Nordpfeil (dreht mit dem Karten-Bearing) */}
|
<div
|
||||||
<button
|
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"
|
||||||
type="button"
|
title="Nordpfeil"
|
||||||
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' }}
|
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 40 40" className="w-10 h-10" style={{ transform: `rotate(${-bearing}deg)` }}>
|
<svg viewBox="0 0 40 40" className="w-10 h-10" style={{ transform: `rotate(${-bearing}deg)` }}>
|
||||||
{/* Nordnadel (rot) */}
|
{/* Nordnadel (rot) */}
|
||||||
<polygon points="20,5 24,21 20,18 16,21" fill="#dc2626" />
|
<polygon points="20,5 24,21 20,18 16,21" fill="#dc2626" />
|
||||||
{/* Südnadel (grau) */}
|
{/* Südnadel (grau) */}
|
||||||
<polygon points="20,35 24,19 20,22 16,19" fill="#9ca3af" />
|
<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>
|
</svg>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
{/* 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