diff --git a/package-lock.json b/package-lock.json
index 689e120..b4fcacc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "lageplan",
- "version": "1.3.1",
+ "version": "1.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lageplan",
- "version": "1.3.1",
+ "version": "1.3.2",
"hasInstallScript": true,
"dependencies": {
"@dnd-kit/core": "^6.1.0",
diff --git a/package.json b/package.json
index 429053b..48252b2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "lageplan",
- "version": "1.3.1",
+ "version": "1.3.2",
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
"private": true,
"scripts": {
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 634210c..cc6286f 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,30 +1,16 @@
-'use client'
-
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Logo } from '@/components/ui/logo'
-import { useAuth } from '@/components/providers/auth-provider'
-import { useRouter } from 'next/navigation'
-import { useEffect, useState } from 'react'
+import { NavAuthButtons } from '@/components/landing/nav-auth-buttons'
+import { ContactForm } from '@/components/landing/contact-form'
import {
- Flame, Map, Shield, Users, Smartphone, FileText, Ruler, Clock,
- Check, ArrowRight, Lock, ChevronRight, MessageSquare, Loader2, Send,
+ Map, Shield, Users, Smartphone, FileText, Ruler, Clock,
+ Check, ArrowRight, Lock, ChevronRight, MessageSquare,
Heart, Coffee, Rocket, Sparkles, Lightbulb, HelpCircle,
MousePointer2, Minus, Pentagon, Square, Circle, Pencil, MoveRight, Type, Eraser,
} from 'lucide-react'
export default function LandingPage() {
- const { user, loading } = useAuth()
- const router = useRouter()
-
- if (loading) {
- return (
-
- )
- }
-
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
@@ -40,11 +26,6 @@ export default function LandingPage() {
priceCurrency: 'CHF',
description: 'Kostenlos für Schweizer Feuerwehren',
},
- aggregateRating: {
- '@type': 'AggregateRating',
- ratingValue: '4.8',
- ratingCount: '12',
- },
author: {
'@type': 'Organization',
name: 'Lageplan.ch',
@@ -62,12 +43,79 @@ export default function LandingPage() {
],
}
+ const faqJsonLd = {
+ '@context': 'https://schema.org',
+ '@type': 'FAQPage',
+ mainEntity: [
+ {
+ '@type': 'Question',
+ name: 'Was kostet Lageplan?',
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: 'Nichts. Lageplan ist kostenlos für alle Feuerwehren in der Schweiz. Die Entwicklung wird durch freiwillige Spenden finanziert.',
+ },
+ },
+ {
+ '@type': 'Question',
+ name: 'Brauche ich eine Installation?',
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: 'Nein. Lageplan läuft komplett im Browser — auf Desktop, Tablet und Smartphone. Einfach registrieren und loslegen.',
+ },
+ },
+ {
+ '@type': 'Question',
+ name: 'Funktioniert es auf dem Tablet im Einsatz?',
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: 'Ja. Die App ist für Touch-Bedienung optimiert und funktioniert auf allen modernen Tablets und Smartphones.',
+ },
+ },
+ {
+ '@type': 'Question',
+ name: 'Können mehrere Personen gleichzeitig arbeiten?',
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: 'Ja. Über Echtzeit-Synchronisation (WebSocket) können mehrere Benutzer gleichzeitig am selben Lageplan zeichnen und das Journal führen.',
+ },
+ },
+ {
+ '@type': 'Question',
+ name: 'Wo werden meine Daten gespeichert?',
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: 'Alle Daten werden auf Servern in der Schweiz gespeichert. Die Applikation ist DSG- und DSGVO-konform.',
+ },
+ },
+ {
+ '@type': 'Question',
+ name: 'Welche Symbole sind verfügbar?',
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: 'Alle 117 offiziellen FKS/BABS-Signaturen sind integriert. Zusätzlich können eigene Symbole hochgeladen werden.',
+ },
+ },
+ {
+ '@type': 'Question',
+ name: 'Kann ich Lagepläne exportieren?',
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: 'Ja. Lagepläne können als PNG oder PDF exportiert werden — inklusive Metadaten, Datum und Einsatzinformationen.',
+ },
+ },
+ ],
+ }
+
return (
+
{/* Navigation */}
- {user ? (
-
-
-
- ) : (
- <>
-
-
-
-
-
-
- >
- )}
+
@@ -564,39 +595,6 @@ function SupportSection() {
}
function ContactSection() {
- const [name, setName] = useState('')
- const [email, setEmail] = useState('')
- const [message, setMessage] = useState('')
- const [sending, setSending] = useState(false)
- const [sent, setSent] = useState(false)
- const [error, setError] = useState('')
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- setSending(true)
- setError('')
- try {
- const res = await fetch('/api/contact', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name, email, message }),
- })
- if (res.ok) {
- setSent(true)
- setName('')
- setEmail('')
- setMessage('')
- } else {
- const data = await res.json()
- setError(data.error || 'Senden fehlgeschlagen')
- }
- } catch {
- setError('Verbindung fehlgeschlagen')
- } finally {
- setSending(false)
- }
- }
-
return (
)
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
index 942320a..46058df 100644
--- a/src/app/sitemap.ts
+++ b/src/app/sitemap.ts
@@ -23,10 +23,10 @@ export default function sitemap(): MetadataRoute.Sitemap {
priority: 0.8,
},
{
- url: `${baseUrl}/impressum`,
+ url: `${baseUrl}/demo`,
lastModified: new Date(),
- changeFrequency: 'yearly',
- priority: 0.3,
+ changeFrequency: 'monthly',
+ priority: 0.7,
},
{
url: `${baseUrl}/spenden`,
@@ -34,5 +34,17 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: 'monthly',
priority: 0.5,
},
+ {
+ url: `${baseUrl}/datenschutz`,
+ lastModified: new Date(),
+ changeFrequency: 'yearly',
+ priority: 0.3,
+ },
+ {
+ url: `${baseUrl}/impressum`,
+ lastModified: new Date(),
+ changeFrequency: 'yearly',
+ priority: 0.3,
+ },
]
}
diff --git a/src/components/landing/contact-form.tsx b/src/components/landing/contact-form.tsx
new file mode 100644
index 0000000..171e5cd
--- /dev/null
+++ b/src/components/landing/contact-form.tsx
@@ -0,0 +1,105 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from '@/components/ui/button'
+import { Loader2, Send, Check } from 'lucide-react'
+
+export function ContactForm() {
+ const [name, setName] = useState('')
+ const [email, setEmail] = useState('')
+ const [message, setMessage] = useState('')
+ const [sending, setSending] = useState(false)
+ const [sent, setSent] = useState(false)
+ const [error, setError] = useState('')
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setSending(true)
+ setError('')
+ try {
+ const res = await fetch('/api/contact', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, email, message }),
+ })
+ if (res.ok) {
+ setSent(true)
+ setName('')
+ setEmail('')
+ setMessage('')
+ } else {
+ const data = await res.json()
+ setError(data.error || 'Senden fehlgeschlagen')
+ }
+ } catch {
+ setError('Verbindung fehlgeschlagen')
+ } finally {
+ setSending(false)
+ }
+ }
+
+ if (sent) {
+ return (
+
+
+
Nachricht gesendet!
+
Vielen Dank! Ich melde mich so schnell wie möglich.
+
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/landing/nav-auth-buttons.tsx b/src/components/landing/nav-auth-buttons.tsx
new file mode 100644
index 0000000..ead7193
--- /dev/null
+++ b/src/components/landing/nav-auth-buttons.tsx
@@ -0,0 +1,36 @@
+'use client'
+
+import Link from 'next/link'
+import { Button } from '@/components/ui/button'
+import { useAuth } from '@/components/providers/auth-provider'
+
+export function NavAuthButtons() {
+ const { user, loading } = useAuth()
+
+ if (loading) {
+ return // placeholder to avoid layout shift
+ }
+
+ if (user) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/components/map/map-view.tsx b/src/components/map/map-view.tsx
index 1099bc5..5f394be 100644
--- a/src/components/map/map-view.tsx
+++ b/src/components/map/map-view.tsx
@@ -728,6 +728,46 @@ export function MapView({
// Expose map instance to parent for export
if (externalMapRef) externalMapRef.current = map.current
+ // --- WebGL context loss recovery ---
+ // When the browser reclaims GPU memory (background tab, memory pressure),
+ // the WebGL context is lost and tiles go black. This recovers automatically.
+ const canvas = map.current.getCanvas()
+ canvas.addEventListener('webglcontextlost', (e) => {
+ console.warn('[Map] WebGL context lost — will restore when possible')
+ e.preventDefault() // allows context to be restored
+ })
+ canvas.addEventListener('webglcontextrestored', () => {
+ console.info('[Map] WebGL context restored — reloading map style')
+ const m = map.current
+ if (m) {
+ // Force full tile reload by re-setting the style
+ const style = m.getStyle()
+ if (style) {
+ m.setStyle(style)
+ }
+ }
+ })
+
+ // --- Page visibility recovery ---
+ // When user switches back to this tab after a while, tiles may be stale/black.
+ // Force a resize + tile re-request on visibility change.
+ const handleVisibilityChange = () => {
+ if (document.visibilityState === 'visible' && map.current) {
+ // Small delay to let browser finish tab switch
+ setTimeout(() => {
+ if (!map.current) return
+ map.current.resize()
+ // Nudge the map to force tile re-requests
+ const center = map.current.getCenter()
+ map.current.setCenter(center)
+ }, 100)
+ }
+ }
+ document.addEventListener('visibilitychange', handleVisibilityChange)
+
+ // Store cleanup reference
+ const cleanupVisibility = () => document.removeEventListener('visibilitychange', handleVisibilityChange)
+
map.current.addControl(new maplibregl.NavigationControl(), 'bottom-right')
map.current.addControl(new maplibregl.ScaleControl(), 'bottom-left')
@@ -1286,6 +1326,7 @@ export function MapView({
})
return () => {
+ cleanupVisibility()
map.current?.remove()
map.current = null
}
@@ -1421,26 +1462,27 @@ export function MapView({
const lineCoords = f.geometry.coordinates as number[][]
if (lineCoords.length < 2) return
- // Get last two points to calculate arrow direction using screen-projected coords
+ // Geographic bearing from p1 to p2 (works for short distances)
const p1 = lineCoords[lineCoords.length - 2]
const p2 = lineCoords[lineCoords.length - 1]
- const px1 = map.current.project(p1 as [number, number])
- const px2 = map.current.project(p2 as [number, number])
- const screenAngle = Math.atan2(px2.y - px1.y, px2.x - px1.x) * (180 / Math.PI) + 90
+ const dLng = p2[0] - p1[0]
+ const dLat = p2[1] - p1[1]
+ // atan2(dLng, dLat) gives angle from north (up), clockwise — matches CSS triangle ▲ default
+ const geoBearing = Math.atan2(dLng, dLat) * (180 / Math.PI)
const color = (f.properties.color as string) || '#000000'
const arrowEl = document.createElement('div')
arrowEl.style.cssText = `
width: 0; height: 0;
- border-left: 10px solid transparent;
- border-right: 10px solid transparent;
- border-bottom: 20px solid ${color};
- transform: rotate(${screenAngle}deg);
+ border-left: 12px solid transparent;
+ border-right: 12px solid transparent;
+ border-bottom: 24px solid ${color};
+ transform: rotate(${geoBearing}deg);
transform-origin: center center;
pointer-events: none;
`
- const marker = new maplibregl.Marker({ element: arrowEl, anchor: 'center', rotationAlignment: 'viewport' })
+ const marker = new maplibregl.Marker({ element: arrowEl, anchor: 'center', rotationAlignment: 'map' })
.setLngLat(p2 as [number, number])
.addTo(map.current)
markersRef.current.push(marker)
@@ -1545,7 +1587,7 @@ export function MapView({
})
}
- const marker = new maplibregl.Marker({ element: el, anchor: 'center', draggable: canEdit, rotationAlignment: 'viewport' })
+ const marker = new maplibregl.Marker({ element: el, anchor: 'center', draggable: canEdit, rotationAlignment: 'map' })
.setLngLat(midpoint)
.addTo(map.current)
@@ -1664,7 +1706,7 @@ export function MapView({
}
try {
- const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'viewport' })
+ const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'map' })
.setLngLat(coords)
.addTo(map.current)
@@ -1730,7 +1772,7 @@ export function MapView({
el.textContent = (f.properties.text as string) || ''
wrapper.appendChild(el)
- const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'viewport' })
+ const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'map' })
.setLngLat(coords)
.addTo(map.current)
@@ -1892,18 +1934,27 @@ export function MapView({
}
}, [drawMode, deselectSymbol])
- // ESC to cancel drawing, DEL to delete selected symbol
+ // ESC to cancel drawing, DEL to delete selected symbol/line/polygon
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
- // DEL / Backspace → delete selected symbol
+ // DEL / Backspace → delete selected symbol or line/polygon
if (e.key === 'Delete' || e.key === 'Backspace') {
const tag = (e.target as HTMLElement)?.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (e.target as HTMLElement)?.isContentEditable) return
+ // Delete selected symbol/text
if (selectedSymbolRef.current) {
e.preventDefault()
deleteSelectedSymbol()
return
}
+ // Delete selected line/polygon/arrow (vertex-editing selection)
+ if (selectedLineIdRef.current) {
+ e.preventDefault()
+ const updated = featuresRef.current.filter(f => f.id !== selectedLineIdRef.current)
+ onFeaturesChangeRef.current(updated)
+ showVertexMarkersRef.current(null)
+ return
+ }
}
if (e.key === 'Escape') {
// In measure mode: finalize (keep line + labels), just stop adding
diff --git a/src/stores/tool-store.ts b/src/stores/tool-store.ts
index f826092..8795c65 100644
--- a/src/stores/tool-store.ts
+++ b/src/stores/tool-store.ts
@@ -21,7 +21,7 @@ interface ToolStore {
export const useToolStore = create((set) => ({
activeTool: 'select',
- activeColor: '#ff0000', // Default Rot
+ activeColor: '#000000', // Default Schwarz
lineType: 'solid',
lineWidth: 3,
selectedFeatureId: null,