v1.3.2: SEO fixes + map bugfixes
SEO: - Landing page converted to Server Component (SSR) - Extracted NavAuthButtons + ContactForm as client islands - Removed fake aggregateRating from JSON-LD - Added FAQPage JSON-LD schema (7 questions) - Extended sitemap: /datenschutz, /spenden, /demo Map fixes: - WebGL context lost recovery (black tiles after inactivity) - Page visibility handler for tile reload on tab switch - Arrow direction: geographic bearing instead of screen angle - All markers rotationAlignment viewport->map (geographic orientation) - DEL key now deletes selected lines/polygons/arrows (not just symbols) - Default drawing color: black
This commit is contained in:
208
src/app/page.tsx
208
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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="animate-pulse text-muted-foreground">Laden...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="min-h-screen bg-white">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
|
||||
/>
|
||||
<main>
|
||||
{/* Navigation */}
|
||||
<nav className="fixed top-0 w-full z-50 bg-white/80 backdrop-blur-md border-b border-gray-100">
|
||||
@@ -84,24 +132,7 @@ export default function LandingPage() {
|
||||
<a href="#roadmap" className="hover:text-gray-900 transition">Roadmap</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{user ? (
|
||||
<Link href="/app">
|
||||
<Button size="sm" className="bg-red-600 hover:bg-red-700">
|
||||
Zur App
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/login">
|
||||
<Button variant="ghost" size="sm">Anmelden</Button>
|
||||
</Link>
|
||||
<Link href="/register">
|
||||
<Button size="sm" className="bg-red-600 hover:bg-red-700">
|
||||
Kostenlos starten
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<NavAuthButtons />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 (
|
||||
<section id="contact" className="py-20 px-4">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
@@ -607,67 +605,7 @@ function ContactSection() {
|
||||
Fragen, Feature-Wünsche oder Feedback? Schreib mir — ich freue mich über jede Nachricht.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{sent ? (
|
||||
<div className="text-center bg-green-50 border border-green-200 rounded-xl p-8">
|
||||
<Check className="w-10 h-10 text-green-600 mx-auto mb-3" />
|
||||
<h3 className="font-semibold text-green-900 text-lg">Nachricht gesendet!</h3>
|
||||
<p className="text-green-700 mt-2">Vielen Dank! Ich melde mich so schnell wie möglich.</p>
|
||||
<Button variant="outline" className="mt-4" onClick={() => setSent(false)}>
|
||||
Weitere Nachricht senden
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
required
|
||||
placeholder="Dein Name"
|
||||
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">E-Mail</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
required
|
||||
placeholder="name@feuerwehr.ch"
|
||||
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Nachricht</label>
|
||||
<textarea
|
||||
value={message}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
required
|
||||
rows={5}
|
||||
placeholder="Deine Nachricht..."
|
||||
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent resize-none"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={sending || !name || !email || !message}
|
||||
>
|
||||
{sending ? (
|
||||
<><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Wird gesendet...</>
|
||||
) : (
|
||||
<><Send className="w-4 h-4 mr-2" /> Nachricht senden</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
<ContactForm />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user