Compare commits
10 Commits
1307a969d7
...
09e8e9230e
| Author | SHA1 | Date |
|---|---|---|
|
|
09e8e9230e | |
|
|
69f3e1b14f | |
|
|
1be8b2e9bd | |
|
|
36067596e6 | |
|
|
9d78fd16da | |
|
|
d21f1c760b | |
|
|
bdcb9d3b75 | |
|
|
b1a2626c8d | |
|
|
de6a911cbf | |
|
|
d71aaebe2a |
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm run dev:*)",
|
||||
"Bash(npm install:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Answer Engine Optimization (AEO) Configuration
|
||||
|
||||
# IndexNow API key for automatic URL submission to search engines
|
||||
# Generate a UUID and create a matching key file in /public/
|
||||
VITE_INDEXNOW_KEY=your-indexnow-key-here
|
||||
|
||||
# Primary domain for canonical URLs and IndexNow submissions
|
||||
VITE_SITE_HOST=energie-profis.de
|
||||
|
||||
# Supabase Configuration (existing)
|
||||
VITE_SUPABASE_URL=your-supabase-url
|
||||
VITE_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
|
|
@ -22,3 +22,5 @@ dist-ssr
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
- `npm run dev` - Start development server (Vite, runs on port 8080)
|
||||
- `npm run build` - Build for production
|
||||
- `npm run build:dev` - Build in development mode
|
||||
- `npm run lint` - Run ESLint to check code quality
|
||||
- `npm run preview` - Preview production build locally
|
||||
|
||||
## Project Architecture
|
||||
|
||||
This is a React + TypeScript application built with Vite, targeting the German renewable energy market. The app connects energy customers with solar and wind installation professionals.
|
||||
|
||||
### Core Technologies
|
||||
- **Vite** - Build tool and dev server
|
||||
- **React 18** - UI framework
|
||||
- **TypeScript** - Type safety
|
||||
- **React Router** - Client-side routing
|
||||
- **TanStack Query** - Server state management
|
||||
- **Supabase** - Backend database and authentication
|
||||
- **shadcn/ui** - Component library built on Radix UI
|
||||
- **Tailwind CSS** - Styling with custom energy-themed colors
|
||||
|
||||
### Application Structure
|
||||
|
||||
**Pages** (`src/pages/`):
|
||||
- `Index.tsx` - Landing page with hero section and energy type overview
|
||||
- `Solar.tsx` / `Wind.tsx` - Energy-specific information pages
|
||||
- `InstallateurFinden.tsx` - Installer search/listing page
|
||||
- `KostenloseBeratung.tsx` - Free consultation request page
|
||||
- `UnternehmenListen.tsx` - Business listing page
|
||||
|
||||
**Components** (`src/components/`):
|
||||
- Standard layout components: `Header`, `Footer`, `HeroSection`
|
||||
- Feature components: `EnergyTypesSection`, `WhyChooseUsSection`, `EnergyTypeCard`
|
||||
- Complete shadcn/ui component library in `ui/` subdirectory
|
||||
|
||||
**Database Integration** (`src/integrations/supabase/`):
|
||||
- `client.ts` - Supabase client configuration
|
||||
- `types.ts` - Auto-generated TypeScript types from Supabase schema
|
||||
|
||||
### Database Schema (Supabase)
|
||||
|
||||
Key tables:
|
||||
- `installers` - Installation companies with location, certifications, ratings
|
||||
- `quotes` - Customer quote requests with project details
|
||||
- `installer_quotes` - Installer responses to quote requests
|
||||
- `reviews` - Customer reviews for installers
|
||||
- `contact_clicks` - Analytics for installer contact interactions
|
||||
- `analytics_events` - General application usage analytics
|
||||
|
||||
Energy types: `solar` | `wind`
|
||||
Quote status: `pending` | `accepted` | `rejected` | `expired`
|
||||
|
||||
### Styling System
|
||||
|
||||
Custom Tailwind configuration with energy-themed colors:
|
||||
- `solar` colors - Orange/yellow theme for solar energy
|
||||
- `wind` colors - Blue/teal theme for wind energy
|
||||
- CSS custom properties defined in `src/index.css`
|
||||
- Gradient backgrounds and shadows for visual branding
|
||||
|
||||
### Path Aliases
|
||||
- `@/*` maps to `src/*` for clean imports
|
||||
|
||||
### TypeScript Configuration
|
||||
- Relaxed strictness settings for rapid development
|
||||
- Path mapping configured for `@/` alias
|
||||
- Composite project structure with separate app and node configs
|
||||
|
||||
### Routing Architecture
|
||||
All routes are defined in `src/App.tsx`:
|
||||
- `/` - Homepage
|
||||
- `/solar`, `/wind` - Energy type pages
|
||||
- `/installateur-finden` - Installer search
|
||||
- `/kostenlose-beratung` - Consultation requests
|
||||
- `/unternehmen-listen` - Business listings
|
||||
- `*` - 404 catch-all
|
||||
|
||||
### Component Patterns
|
||||
- Functional components with hooks
|
||||
- shadcn/ui components for consistent design
|
||||
- Lucide React icons throughout the application
|
||||
- German language content and URLs
|
||||
84
README.md
84
README.md
|
|
@ -1,8 +1,90 @@
|
|||
# Welcome to your Lovable project
|
||||
# EnergieProfis - Renewable Energy Platform
|
||||
|
||||
A React + TypeScript platform connecting energy customers with qualified solar and wind installation professionals in Germany.
|
||||
|
||||
## Project info
|
||||
|
||||
**URL**: https://lovable.dev/projects/54a0b4f8-e87d-43d2-ac51-69b3e38c23fb
|
||||
**Domain**: energie-profis.de
|
||||
|
||||
## Answer Engine Optimization (AEO) Features
|
||||
|
||||
This project implements comprehensive AEO to be cited by ChatGPT, Perplexity, and other AI answer engines:
|
||||
|
||||
### ✅ Implemented Features
|
||||
|
||||
- **Robots.txt**: Explicitly allows PerplexityBot and GPTBot
|
||||
- **IndexNow Integration**: Automatic URL submission with queue and retry logic
|
||||
- **Sitemaps**: XML sitemaps with accurate lastmod dates
|
||||
- **Canonical URLs**: Single canonical per page for AI indexing
|
||||
- **JSON-LD Schema**: FAQ, HowTo, Article, Organization markup
|
||||
- **Content Metadata**: Visible "Zuletzt aktualisiert" and author information
|
||||
|
||||
### 🔧 AEO Setup
|
||||
|
||||
1. **Environment Variables**
|
||||
```bash
|
||||
VITE_INDEXNOW_KEY=your-indexnow-key-here
|
||||
VITE_SITE_HOST=energie-profis.de
|
||||
```
|
||||
|
||||
2. **Create IndexNow Key File**
|
||||
```bash
|
||||
# Create public key file (replace with your actual key)
|
||||
echo "your-indexnow-key-here" > public/your-indexnow-key-here.txt
|
||||
```
|
||||
|
||||
3. **Usage in Components**
|
||||
```tsx
|
||||
import Canonical from '@/components/seo/Canonical';
|
||||
import JsonLd from '@/components/seo/JsonLd';
|
||||
import ContentMeta from '@/components/seo/ContentMeta';
|
||||
import { buildFAQPageJsonLd, exampleFAQs } from '@/lib/schema';
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<>
|
||||
<Canonical />
|
||||
<JsonLd data={buildFAQPageJsonLd(exampleFAQs)} />
|
||||
<ContentMeta lastUpdated={new Date()} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
4. **Manual IndexNow Ping**
|
||||
```bash
|
||||
npm run ping-indexnow https://energie-profis.de/ https://energie-profis.de/solar
|
||||
```
|
||||
|
||||
### 🧪 Testing AEO Features
|
||||
|
||||
Run E2E tests to validate AEO implementation:
|
||||
|
||||
```bash
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
Tests cover:
|
||||
- Robots.txt allows AI bots
|
||||
- Canonical URLs are present and correct
|
||||
- JSON-LD schema is valid
|
||||
- Sitemap includes all pages with lastmod
|
||||
|
||||
### 📋 Manual Validation
|
||||
|
||||
Quick checks to verify AEO compliance:
|
||||
|
||||
```bash
|
||||
# Check robots.txt allows AI bots
|
||||
curl https://energie-profis.de/robots.txt | grep -E "(PerplexityBot|GPTBot)"
|
||||
|
||||
# Validate sitemap structure
|
||||
curl https://energie-profis.de/sitemap.xml | xmllint --format -
|
||||
|
||||
# Check canonical on homepage
|
||||
curl -s https://energie-profis.de/ | grep -o '<link rel="canonical"[^>]*>'
|
||||
```
|
||||
|
||||
## How can I edit this code?
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@
|
|||
<meta name="description" content="Finden Sie qualifizierte Installateure für Solar, Wind, Geothermie und Batteriespeicher. Kostenlose Beratung und Angebote von geprüften Fachbetrieben." />
|
||||
<meta name="author" content="EnergieProfis" />
|
||||
<meta name="keywords" content="Solar, Photovoltaik, Windkraft, Geothermie, Batteriespeicher, Installateur, Deutschland, erneuerbare Energie" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/jpeg" href="/icon_energie-finder.jpeg" />
|
||||
<link rel="shortcut icon" type="image/jpeg" href="/icon_energie-finder.jpeg" />
|
||||
|
||||
<meta property="og:title" content="EnergieProfis - Erneuerbare Energie Installateure" />
|
||||
<meta property="og:description" content="Finden Sie qualifizierte Installateure für Solar, Wind, Geothermie und Batteriespeicher in Deutschland." />
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
|
@ -8,7 +8,11 @@
|
|||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"ping-indexnow": "tsx scripts/ping-indexnow.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
|
|
@ -45,10 +49,12 @@
|
|||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.462.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"posthog-js": "^1.200.0",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
@ -64,6 +70,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/node": "^22.16.5",
|
||||
"@types/react": "^18.3.23",
|
||||
|
|
@ -77,6 +84,7 @@
|
|||
"lovable-tagger": "^1.1.9",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^5.4.19"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Playwright configuration for AEO (Answer Engine Optimization) testing
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: 'http://localhost:8080',
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:8080',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
|
|
@ -10,5 +10,13 @@ Allow: /
|
|||
User-agent: facebookexternalhit
|
||||
Allow: /
|
||||
|
||||
User-agent: PerplexityBot
|
||||
Allow: /
|
||||
|
||||
User-agent: GPTBot
|
||||
Allow: /
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://energie-profis.de/sitemap.xml
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
|
||||
<url>
|
||||
<loc>https://energie-profis.de/</loc>
|
||||
<lastmod>2025-01-28</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://energie-profis.de/solar</loc>
|
||||
<lastmod>2025-01-28</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://energie-profis.de/wind</loc>
|
||||
<lastmod>2025-01-28</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://energie-profis.de/installateur-finden</loc>
|
||||
<lastmod>2025-01-04</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://energie-profis.de/kostenlose-beratung</loc>
|
||||
<lastmod>2024-12-05</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://energie-profis.de/unternehmen-listen</loc>
|
||||
<lastmod>2025-01-04</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
|
||||
</urlset>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.7 MiB |
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Manual IndexNow ping script for CI/CD and manual testing
|
||||
* Usage: npm run ping-indexnow <url1> <url2> ...
|
||||
*/
|
||||
|
||||
import { pingIndexNowManually } from '../src/lib/indexnow.js';
|
||||
|
||||
async function main() {
|
||||
const urls = process.argv.slice(2);
|
||||
|
||||
if (urls.length === 0) {
|
||||
console.error('Usage: npm run ping-indexnow <url1> <url2> ...');
|
||||
console.error('Example: npm run ping-indexnow https://energie-profis.de/ https://energie-profis.de/solar');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Pinging IndexNow for ${urls.length} URLs...`);
|
||||
console.log('URLs:', urls.join(', '));
|
||||
|
||||
try {
|
||||
const success = await pingIndexNowManually(urls);
|
||||
|
||||
if (success) {
|
||||
console.log('✅ IndexNow ping completed successfully');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error('❌ IndexNow ping failed or queued for retry');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ IndexNow ping error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
14
src/App.tsx
14
src/App.tsx
|
|
@ -6,6 +6,12 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|||
import Index from "./pages/Index";
|
||||
import Solar from "./pages/Solar";
|
||||
import Wind from "./pages/Wind";
|
||||
import InstallateurFinden from "./pages/InstallateurFinden";
|
||||
import KostenloseBeratung from "./pages/KostenloseBeratung";
|
||||
import UnternehmenListen from "./pages/UnternehmenListen";
|
||||
import Impressum from "./pages/Impressum";
|
||||
import Datenschutz from "./pages/Datenschutz";
|
||||
import CookieEinstellungen from "./pages/CookieEinstellungen";
|
||||
import NotFound from "./pages/NotFound";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
|
@ -15,11 +21,17 @@ const App = () => (
|
|||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<BrowserRouter>
|
||||
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/solar" element={<Solar />} />
|
||||
<Route path="/wind" element={<Wind />} />
|
||||
<Route path="/installateur-finden" element={<InstallateurFinden />} />
|
||||
<Route path="/kostenlose-beratung" element={<KostenloseBeratung />} />
|
||||
<Route path="/unternehmen-listen" element={<UnternehmenListen />} />
|
||||
<Route path="/impressum" element={<Impressum />} />
|
||||
<Route path="/datenschutz" element={<Datenschutz />} />
|
||||
<Route path="/cookie-einstellungen" element={<CookieEinstellungen />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
import React from 'react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sun, Wind, Calculator, ArrowRight, Zap } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const CalculatorNavigation = () => {
|
||||
return (
|
||||
<section className="py-20 bg-gradient-to-br from-slate-50 via-blue-50 to-green-50">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-gray-800">
|
||||
<Calculator className="inline-block mr-3 text-blue-600" />
|
||||
Einsparungsrechner
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed">
|
||||
Berechnen Sie Ihre potentiellen Einsparungen mit unseren
|
||||
spezialisierten Rechnern für Solar- und Windenergie
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{/* Solar Calculator Card */}
|
||||
<Card className="group hover:shadow-2xl transition-all duration-500 border-0 bg-gradient-to-br from-orange-50 to-yellow-50 overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-orange-400/5 to-yellow-400/5 group-hover:from-orange-400/10 group-hover:to-yellow-400/10 transition-all duration-500"></div>
|
||||
<CardContent className="p-8 relative z-10">
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-orange-500 to-yellow-500 rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300 shadow-lg">
|
||||
<Sun className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div className="bg-orange-100 text-orange-700 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
Beliebt
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold mb-4 text-gray-800 group-hover:text-orange-600 transition-colors">
|
||||
Solar-Einsparungsrechner
|
||||
</h3>
|
||||
|
||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||
Erfahren Sie, wie viel Sie mit einer Photovoltaik-Anlage sparen können.
|
||||
Berechnung basierend auf Ihrer Dachgröße, Standort und Energieverbrauch.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3 mb-8">
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Zap className="w-4 h-4 mr-2 text-orange-500" />
|
||||
<span>Monatliche & jährliche Einsparungen</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Zap className="w-4 h-4 mr-2 text-orange-500" />
|
||||
<span>25-Jahre Gesamteinsparungen</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Zap className="w-4 h-4 mr-2 text-orange-500" />
|
||||
<span>Amortisationsdauer</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="w-full bg-gradient-to-r from-orange-500 to-yellow-500 hover:from-orange-600 hover:to-yellow-600 text-white font-semibold shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-300"
|
||||
>
|
||||
<Link to="/solar#calculator" className="flex items-center justify-center">
|
||||
Solar-Rechner öffnen
|
||||
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Wind Calculator Card */}
|
||||
<Card className="group hover:shadow-2xl transition-all duration-500 border-0 bg-gradient-to-br from-cyan-50 to-emerald-50 overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-cyan-400/5 to-emerald-400/5 group-hover:from-cyan-400/10 group-hover:to-emerald-400/10 transition-all duration-500"></div>
|
||||
<CardContent className="p-8 relative z-10">
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-cyan-500 to-emerald-500 rounded-2xl flex items-center justify-center group-hover:scale-110 transition-transform duration-300 shadow-lg">
|
||||
<Wind className="w-8 h-8 text-white animate-spin" />
|
||||
</div>
|
||||
<div className="bg-cyan-100 text-cyan-700 px-3 py-1 rounded-full text-sm font-semibold">
|
||||
Neu
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold mb-4 text-gray-800 group-hover:text-cyan-600 transition-colors">
|
||||
Windenergie-Rechner
|
||||
</h3>
|
||||
|
||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||
Berechnen Sie das Potential von Windenergie für Ihr Grundstück.
|
||||
Berücksichtigt Grundstücksgröße, Windgeschwindigkeit und lokale Bestimmungen.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3 mb-8">
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Wind className="w-4 h-4 mr-2 text-cyan-500" />
|
||||
<span>Anlagengröße-Empfehlung</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Wind className="w-4 h-4 mr-2 text-cyan-500" />
|
||||
<span>20-Jahre Einsparpotential</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Wind className="w-4 h-4 mr-2 text-cyan-500" />
|
||||
<span>Förderungen & Zuschüsse</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="w-full bg-gradient-to-r from-cyan-500 to-emerald-500 hover:from-cyan-600 hover:to-emerald-600 text-white font-semibold shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-300"
|
||||
>
|
||||
<Link to="/wind#calculator" className="flex items-center justify-center">
|
||||
Wind-Rechner öffnen
|
||||
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<div className="text-center mt-16">
|
||||
<p className="text-gray-600 mb-6">
|
||||
Unsicher, welche Lösung für Sie geeignet ist?
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="border-2 border-gray-300 text-gray-700 hover:bg-gray-50 font-semibold"
|
||||
>
|
||||
<Link to="/installateur-finden" className="flex items-center">
|
||||
Alle Installateure ansehen
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalculatorNavigation;
|
||||
|
|
@ -8,7 +8,7 @@ interface EnergyTypeCardProps {
|
|||
description: string;
|
||||
image: string;
|
||||
gradient: string;
|
||||
buttonVariant: "solar" | "wind" | "geo" | "battery";
|
||||
buttonVariant: "solar" | "wind";
|
||||
href: string;
|
||||
features: string[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import EnergyTypeCard from "./EnergyTypeCard";
|
||||
import solarImage from "@/assets/solar-installation.jpg";
|
||||
import windImage from "@/assets/wind-turbines.jpg";
|
||||
import geoImage from "@/assets/geothermal-system.jpg";
|
||||
import batteryImage from "@/assets/battery-storage.jpg";
|
||||
|
||||
const EnergyTypesSection = () => {
|
||||
const energyTypes = [
|
||||
|
|
@ -33,34 +31,6 @@ const EnergyTypesSection = () => {
|
|||
"Wartungsarme Technologie",
|
||||
"Ideale Ergänzung zu Solar"
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Geothermie",
|
||||
description: "Heizen und kühlen Sie Ihr Gebäude mit der natürlichen Erdwärme - effizient und umweltschonend.",
|
||||
image: geoImage,
|
||||
gradient: "bg-gradient-geo",
|
||||
buttonVariant: "geo" as const,
|
||||
href: "/geothermie",
|
||||
features: [
|
||||
"Ganzjährig konstante Temperaturen",
|
||||
"Niedrige Betriebskosten",
|
||||
"Heizen und Kühlen in einem System",
|
||||
"Sehr lange Lebensdauer"
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Batteriespeicher",
|
||||
description: "Speichern Sie überschüssige Energie und nutzen Sie sie bei Bedarf - für maximale Unabhängigkeit.",
|
||||
image: batteryImage,
|
||||
gradient: "bg-gradient-battery",
|
||||
buttonVariant: "battery" as const,
|
||||
href: "/batteriespeicher",
|
||||
features: [
|
||||
"24/7 Energieverfügbarkeit",
|
||||
"Notstromfunktion",
|
||||
"Intelligente Steuerung",
|
||||
"Erhöhte Eigenverbrauchsquote"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -77,7 +47,7 @@ const EnergyTypesSection = () => {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{energyTypes.map((type) => (
|
||||
<EnergyTypeCard key={type.title} {...type} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,152 +1,171 @@
|
|||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Mail, Phone, MapPin, Facebook, Twitter, Linkedin, Instagram } from "lucide-react";
|
||||
import {
|
||||
Mail,
|
||||
Phone,
|
||||
MapPin,
|
||||
Facebook,
|
||||
Twitter,
|
||||
Linkedin,
|
||||
Instagram,
|
||||
Calculator,
|
||||
TrendingUp,
|
||||
Shield,
|
||||
Award
|
||||
} from "lucide-react";
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="bg-primary text-primary-foreground">
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
|
||||
<footer className="bg-gray-900 text-white mt-20">
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{/* Company Info */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-gradient-hero rounded-lg flex items-center justify-center">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-gradient-to-r from-orange-500 to-blue-500 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-lg">E</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold">EnergieProfis</span>
|
||||
</div>
|
||||
|
||||
<p className="text-primary-foreground/80 leading-relaxed">
|
||||
Ihr vertrauensvoller Partner für erneuerbare Energielösungen in Deutschland.
|
||||
Wir verbinden Sie mit den besten Installateuren für eine nachhaltige Zukunft.
|
||||
<p className="text-gray-300 text-sm leading-relaxed">
|
||||
Ihr vertrauensvoller Partner für erneuerbare Energien.
|
||||
Finden Sie qualifizierte Solar- und Wind-Installateure in Ihrer Region.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Phone className="w-5 h-5 text-solar" />
|
||||
<span>+49 (0) 800 123 4567</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Mail className="w-5 h-5 text-wind" />
|
||||
<span>info@energieprofis.de</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<MapPin className="w-5 h-5 text-geo" />
|
||||
<span>München, Deutschland</span>
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
<Button variant="ghost" size="sm" className="text-gray-300 hover:text-white p-2">
|
||||
<Facebook className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="text-gray-300 hover:text-white p-2">
|
||||
<Twitter className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="text-gray-300 hover:text-white p-2">
|
||||
<Linkedin className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="text-gray-300 hover:text-white p-2">
|
||||
<Instagram className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Energy Types */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold">Energielösungen</h3>
|
||||
<ul className="space-y-3">
|
||||
{/* Quick Links */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Schnellzugriff</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<Link to="/solar" className="text-primary-foreground/80 hover:text-solar transition-colors">
|
||||
Solar-Installateure
|
||||
<Link
|
||||
to="/solar"
|
||||
className="text-gray-300 hover:text-white transition-colors"
|
||||
onClick={() => window.scrollTo(0, 0)}
|
||||
>
|
||||
Solar-Installation
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/wind" className="text-primary-foreground/80 hover:text-wind transition-colors">
|
||||
Wind-Installateure
|
||||
<Link
|
||||
to="/wind"
|
||||
className="text-gray-300 hover:text-white transition-colors"
|
||||
onClick={() => window.scrollTo(0, 0)}
|
||||
>
|
||||
Windenergie
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/geothermie" className="text-primary-foreground/80 hover:text-geo transition-colors">
|
||||
Geothermie-Installateure
|
||||
<Link to="/installateur-finden" className="text-gray-300 hover:text-white transition-colors">
|
||||
Installateur finden
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/batteriespeicher" className="text-primary-foreground/80 hover:text-battery transition-colors">
|
||||
Batteriespeicher-Installateure
|
||||
<Link
|
||||
to="/solar"
|
||||
className="text-gray-300 hover:text-white transition-colors"
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
const calculatorElement = document.querySelector('[data-calculator]') ||
|
||||
document.querySelector('.calculator') ||
|
||||
document.querySelector('h2');
|
||||
if (calculatorElement) {
|
||||
calculatorElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
Solar-Kostenrechner
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to="/wind"
|
||||
className="text-gray-300 hover:text-white transition-colors"
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
const calculatorElement = document.querySelector('[data-calculator]') ||
|
||||
document.querySelector('.calculator') ||
|
||||
document.querySelector('h2');
|
||||
if (calculatorElement) {
|
||||
calculatorElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
Wind-Kostenrechner
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Services */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold">Services</h3>
|
||||
<ul className="space-y-3">
|
||||
<li>
|
||||
<Link to="/installateur-finden" className="text-primary-foreground/80 hover:text-white transition-colors">
|
||||
Installateur Finden
|
||||
</Link>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Unsere Services</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li className="flex items-center gap-2 text-gray-300">
|
||||
<Calculator className="w-4 h-4" />
|
||||
Solar-Einsparungsrechner
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/kostenlose-beratung" className="text-primary-foreground/80 hover:text-white transition-colors">
|
||||
Kostenlose Beratung
|
||||
</Link>
|
||||
<li className="flex items-center gap-2 text-gray-300">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
Windenergie-Rechner
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/unternehmen-listen" className="text-primary-foreground/80 hover:text-white transition-colors">
|
||||
Unternehmen Listen
|
||||
</Link>
|
||||
<li className="flex items-center gap-2 text-gray-300">
|
||||
<Shield className="w-4 h-4" />
|
||||
Verifizierte Installateure
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/preisvergleich" className="text-primary-foreground/80 hover:text-white transition-colors">
|
||||
Preisvergleich
|
||||
</Link>
|
||||
<li className="flex items-center gap-2 text-gray-300">
|
||||
<Award className="w-4 h-4" />
|
||||
Zertifizierte Fachbetriebe
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Newsletter */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-semibold">Newsletter</h3>
|
||||
<p className="text-primary-foreground/80">
|
||||
Bleiben Sie informiert über die neuesten Entwicklungen
|
||||
in der erneuerbaren Energie.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
placeholder="Ihre E-Mail-Adresse"
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-white/60"
|
||||
/>
|
||||
<Button variant="hero" className="w-full bg-white text-primary hover:bg-white/90">
|
||||
Anmelden
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Social Media */}
|
||||
<div className="flex space-x-4">
|
||||
<Button variant="ghost" size="icon" className="hover:bg-white/10">
|
||||
<Facebook className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="hover:bg-white/10">
|
||||
<Twitter className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="hover:bg-white/10">
|
||||
<Linkedin className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="hover:bg-white/10">
|
||||
<Instagram className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-white/20 mt-12 pt-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
|
||||
<p className="text-primary-foreground/60 text-sm">
|
||||
© 2024 EnergieProfis. Alle Rechte vorbehalten.
|
||||
</p>
|
||||
|
||||
|
||||
{/* Bottom Section */}
|
||||
<div className="border-t border-gray-700 mt-12 pt-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<div className="text-sm text-gray-400">
|
||||
© 2025 EnergieProfis. Alle Rechte vorbehalten.
|
||||
</div>
|
||||
<div className="flex space-x-6 text-sm">
|
||||
<Link to="/datenschutz" className="text-primary-foreground/60 hover:text-white transition-colors">
|
||||
Datenschutz
|
||||
</Link>
|
||||
<Link to="/agb" className="text-primary-foreground/60 hover:text-white transition-colors">
|
||||
AGB
|
||||
</Link>
|
||||
<Link to="/impressum" className="text-primary-foreground/60 hover:text-white transition-colors">
|
||||
<Link
|
||||
to="/impressum"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
onClick={() => window.scrollTo(0, 0)}
|
||||
>
|
||||
Impressum
|
||||
</Link>
|
||||
<Link to="/kontakt" className="text-primary-foreground/60 hover:text-white transition-colors">
|
||||
Kontakt
|
||||
<Link
|
||||
to="/datenschutz"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
onClick={() => window.scrollTo(0, 0)}
|
||||
>
|
||||
Datenschutz
|
||||
</Link>
|
||||
<Link
|
||||
to="/cookie-einstellungen"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
onClick={() => window.scrollTo(0, 0)}
|
||||
>
|
||||
Cookie-Einstellungen
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ const Header = () => {
|
|||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<Link to="/" className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-gradient-hero rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-lg">E</span>
|
||||
</div>
|
||||
<img
|
||||
src="/icon_energie-finder.jpeg"
|
||||
alt="EnergieProfis Logo"
|
||||
className="w-8 h-8 rounded-lg"
|
||||
/>
|
||||
<span className="text-xl font-bold text-primary">EnergieProfis</span>
|
||||
</Link>
|
||||
|
||||
|
|
@ -23,12 +25,6 @@ const Header = () => {
|
|||
<Link to="/wind" className="text-foreground hover:text-wind transition-colors font-medium">
|
||||
Wind
|
||||
</Link>
|
||||
<Link to="/geothermie" className="text-foreground hover:text-geo transition-colors font-medium">
|
||||
Geothermie
|
||||
</Link>
|
||||
<Link to="/batteriespeicher" className="text-foreground hover:text-battery transition-colors font-medium">
|
||||
Batteriespeicher
|
||||
</Link>
|
||||
<Link to="/installateur-finden" className="text-foreground hover:text-primary transition-colors font-medium">
|
||||
Installateur Finden
|
||||
</Link>
|
||||
|
|
@ -39,12 +35,6 @@ const Header = () => {
|
|||
<Button variant="outline" size="sm" asChild>
|
||||
<Link to="/unternehmen-listen">Unternehmen Listen</Link>
|
||||
</Button>
|
||||
<Button variant="hero" size="sm" asChild>
|
||||
<Link to="/kostenlose-beratung">
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
Kostenlose Beratung
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
|
|
|
|||
|
|
@ -1,100 +1,177 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search, MapPin, Zap } from "lucide-react";
|
||||
import { Search, MapPin, Zap, ArrowRight, Calculator, Award, Shield } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import heroImage from "@/assets/hero-renewable-energy.jpg";
|
||||
import { usePostHog } from "@/hooks/usePostHog";
|
||||
|
||||
const HeroSection = () => {
|
||||
const posthog = usePostHog();
|
||||
|
||||
const handleSearchClick = () => {
|
||||
posthog.capture('hero_search_clicked', {
|
||||
location: 'hero_section',
|
||||
action: 'search_installers'
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewAllClick = () => {
|
||||
posthog.capture('hero_view_all_clicked', {
|
||||
location: 'hero_section',
|
||||
action: 'view_all_installers'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative min-h-[600px] flex items-center overflow-hidden">
|
||||
{/* Background Image with Overlay */}
|
||||
{/* Background Image with Minimal Overlay */}
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={heroImage}
|
||||
alt="Renewable Energy Solutions in Germany"
|
||||
src="/sun_flow_banner.png"
|
||||
alt="Sun Flow Banner - Renewable Energy Solutions in Germany - Solar und Wind Installateure finden"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/80 via-primary/60 to-transparent"></div>
|
||||
{/* Subtle overlay for text readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black/30 via-black/20 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<div className="max-w-2xl text-white">
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6 leading-tight">
|
||||
Finden Sie Ihren perfekten{" "}
|
||||
<span className="bg-gradient-to-r from-solar via-wind to-geo bg-clip-text text-transparent">
|
||||
Erneuerbaren Energie
|
||||
</span>{" "}
|
||||
Installateur
|
||||
</h1>
|
||||
<div className="max-w-4xl text-white">
|
||||
{/* Clean headline with subtle color accents */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold leading-tight">
|
||||
Finden Sie Ihren perfekten{" "}
|
||||
<span className="text-yellow-300">
|
||||
Erneuerbaren
|
||||
</span>{" "}
|
||||
<span className="text-cyan-200">
|
||||
Energie
|
||||
</span>{" "}
|
||||
<span className="text-green-200">
|
||||
Installateur
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p className="text-xl md:text-2xl text-white/90 mb-8 leading-relaxed">
|
||||
Vergleichen Sie qualifizierte Fachbetriebe für Solar, Wind, Geothermie
|
||||
und Batteriespeicher in Ihrer Region. Kostenlos und unverbindlich.
|
||||
{/* Clean description with subtle accents */}
|
||||
<p className="text-xl md:text-2xl text-white/95 mb-8 leading-relaxed font-medium">
|
||||
Vergleichen Sie qualifizierte Fachbetriebe für{" "}
|
||||
<span className="text-yellow-200 font-semibold">Solar</span> und{" "}
|
||||
<span className="text-cyan-200 font-semibold">Wind</span>
|
||||
in Ihrer Region.{" "}
|
||||
<span className="text-green-200 font-semibold">Kostenlos</span> und{" "}
|
||||
<span className="text-white font-semibold">unverbindlich</span>.
|
||||
</p>
|
||||
|
||||
{/* Search Box */}
|
||||
<div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-2xl p-6 mb-8">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/60 w-5 h-5" />
|
||||
<Input
|
||||
placeholder="PLZ oder Stadt eingeben..."
|
||||
className="pl-10 bg-white/10 border-white/20 text-white placeholder:text-white/60 h-12"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<Zap className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/60 w-5 h-5" />
|
||||
<select className="w-full h-12 pl-10 pr-4 bg-white/10 border border-white/20 rounded-lg text-white appearance-none">
|
||||
<option value="">Energieart wählen</option>
|
||||
<option value="solar">Solar</option>
|
||||
<option value="wind">Wind</option>
|
||||
<option value="geothermal">Geothermie</option>
|
||||
<option value="battery">Batteriespeicher</option>
|
||||
{/* Clean search box */}
|
||||
<div className="bg-white/15 backdrop-blur-sm border border-white/30 rounded-2xl px-12 py-8 mb-8 max-w-4xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
<div className="flex-1 relative group min-w-0">
|
||||
<MapPin className="absolute left-3 top-1/2 transform -translate-y-1/2 text-cyan-200 w-5 h-5 z-10" />
|
||||
<select className="pl-10 bg-white border-gray-300 text-gray-900 h-12 w-full rounded-md border px-3 py-2 text-sm focus:border-cyan-200 focus:ring-2 focus:ring-cyan-200/20 focus:outline-none min-w-[200px]">
|
||||
<option value="">Bundesland wählen</option>
|
||||
<option value="all">Alle Bundesländer</option>
|
||||
<option value="Baden-Württemberg">Baden-Württemberg</option>
|
||||
<option value="Bayern">Bayern</option>
|
||||
<option value="Berlin">Berlin</option>
|
||||
<option value="Brandenburg">Brandenburg</option>
|
||||
<option value="Bremen">Bremen</option>
|
||||
<option value="Hamburg">Hamburg</option>
|
||||
<option value="Hessen">Hessen</option>
|
||||
<option value="Mecklenburg-Vorpommern">Mecklenburg-Vorpommern</option>
|
||||
<option value="Niedersachsen">Niedersachsen</option>
|
||||
<option value="Nordrhein-Westfalen">Nordrhein-Westfalen</option>
|
||||
<option value="Rheinland-Pfalz">Rheinland-Pfalz</option>
|
||||
<option value="Saarland">Saarland</option>
|
||||
<option value="Sachsen">Sachsen</option>
|
||||
<option value="Sachsen-Anhalt">Sachsen-Anhalt</option>
|
||||
<option value="Schleswig-Holstein">Schleswig-Holstein</option>
|
||||
<option value="Thüringen">Thüringen</option>
|
||||
</select>
|
||||
</div>
|
||||
<Button variant="hero" size="lg" className="bg-white text-primary hover:bg-white/90">
|
||||
<Search className="w-5 h-5 mr-2" />
|
||||
Installateur Finden
|
||||
<div className="flex-1 relative group min-w-0">
|
||||
<Zap className="absolute left-3 top-1/2 transform -translate-y-1/2 text-yellow-200 w-5 h-5 z-10" />
|
||||
<select className="pl-10 bg-white border-gray-300 text-gray-900 h-12 w-full rounded-md border px-3 py-2 text-sm focus:border-yellow-200 focus:ring-2 focus:ring-yellow-200/20 focus:outline-none min-w-[200px]">
|
||||
<option value="">Energieart wählen</option>
|
||||
<option value="all">Alle Energiearten</option>
|
||||
<option value="solar">Solar</option>
|
||||
<option value="wind">Wind</option>
|
||||
</select>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
variant="hero"
|
||||
size="lg"
|
||||
className="bg-cyan-600 hover:bg-cyan-700 text-white border-0 shadow-lg hover:shadow-xl transition-all duration-300"
|
||||
onClick={handleSearchClick}
|
||||
>
|
||||
<Link to="/installateur-finden">
|
||||
<Search className="w-5 h-5 mr-2" />
|
||||
Installateur Finden
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Button variant="hero" size="xl" asChild>
|
||||
<Link to="/kostenlose-beratung">
|
||||
Kostenlose Beratung Anfordern
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="xl" className="border-white/30 text-white hover:bg-white hover:text-primary">
|
||||
<Link to="/installateur-finden">
|
||||
Alle Installateure Ansehen
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Stats */}
|
||||
{/* Clean floating stats */}
|
||||
<div className="absolute bottom-8 right-8 hidden xl:block">
|
||||
<div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-2xl p-6 text-white">
|
||||
<div className="grid grid-cols-3 gap-6 text-center">
|
||||
<div className="bg-white/20 backdrop-blur-sm border border-white/30 rounded-2xl p-6 text-white shadow-lg">
|
||||
<div className="grid grid-cols-2 gap-6 text-center">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">500+</div>
|
||||
<div className="text-sm text-white/80">Installateure</div>
|
||||
<div className="text-3xl font-bold text-yellow-200">60+</div>
|
||||
<div className="text-sm text-white/90 font-medium">Installateure</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">2,500+</div>
|
||||
<div className="text-sm text-white/80">Projekte</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">4.8★</div>
|
||||
<div className="text-sm text-white/80">Bewertung</div>
|
||||
<div className="text-3xl font-bold text-green-200">4.8★</div>
|
||||
<div className="text-sm text-white/90 font-medium">Bewertung</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SEO: Additional content section for better indexing */}
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent py-3 hidden lg:block">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid grid-cols-2 gap-8 text-white/90 max-w-2xl mx-auto">
|
||||
<div className="text-center">
|
||||
<Calculator className="w-8 h-8 mx-auto mb-2 text-yellow-300" />
|
||||
<h3 className="font-semibold mb-1">Kostenlose Rechner</h3>
|
||||
<p className="text-sm">Solar- und Windenergie berechnen</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<Shield className="w-8 h-8 mx-auto mb-2 text-green-300" />
|
||||
<h3 className="font-semibold mb-1">Verifizierte Experten</h3>
|
||||
<p className="text-sm">Zertifizierte Fachbetriebe</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logo in top left corner */}
|
||||
<div className="absolute top-8 left-8 z-20">
|
||||
<img
|
||||
src="/icon_energie-finder.jpeg"
|
||||
alt="EnergieProfis Logo"
|
||||
className="w-12 h-12 rounded-lg shadow-lg border-2 border-white/30 bg-white/10 backdrop-blur-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Moved "Alle Installateure Ansehen" button to upper right corner */}
|
||||
<div className="absolute top-8 right-8 z-20">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="border-2 border-white/40 text-white hover:bg-white hover:text-primary bg-white/10 backdrop-blur-sm hover:shadow-lg transition-all duration-300"
|
||||
onClick={handleViewAllClick}
|
||||
>
|
||||
<Link to="/installateur-finden" className="flex items-center">
|
||||
Alle Installateure Ansehen
|
||||
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,535 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import { analyticsService, quoteService } from '@/lib/database';
|
||||
|
||||
interface CalculatorResults {
|
||||
monthlySavings: number;
|
||||
yearlySavings: number;
|
||||
lifetimeSavings: number;
|
||||
systemSize: number;
|
||||
installCost: number;
|
||||
taxCredit: number;
|
||||
netCost: number;
|
||||
paybackPeriod: number;
|
||||
}
|
||||
|
||||
const SolarCalculator = () => {
|
||||
const [monthlyBill, setMonthlyBill] = useState<string>('150');
|
||||
const [homeSize, setHomeSize] = useState<string>('2000');
|
||||
const [roofType, setRoofType] = useState<string>('asphalt');
|
||||
const [sunlightHours, setSunlightHours] = useState<string>('6');
|
||||
const [electricityRate, setElectricityRate] = useState<string>('0.12');
|
||||
const [zipCode, setZipCode] = useState<string>('');
|
||||
const [results, setResults] = useState<CalculatorResults | null>(null);
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
const [showQuoteForm, setShowQuoteForm] = useState(false);
|
||||
|
||||
// Quote form fields
|
||||
const [customerName, setCustomerName] = useState('');
|
||||
const [customerEmail, setCustomerEmail] = useState('');
|
||||
const [customerPhone, setCustomerPhone] = useState('');
|
||||
|
||||
const calculateSavings = async () => {
|
||||
const monthlyBillNum = parseFloat(monthlyBill) || 0;
|
||||
const homeSizeNum = parseFloat(homeSize) || 0;
|
||||
const sunlightHoursNum = parseFloat(sunlightHours) || 6;
|
||||
const electricityRateNum = parseFloat(electricityRate) || 0.12;
|
||||
|
||||
if (monthlyBillNum === 0) {
|
||||
alert('Bitte geben Sie Ihre monatliche Stromrechnung ein');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate system size based on monthly usage
|
||||
const monthlyUsage = monthlyBillNum / electricityRateNum; // kWh per month
|
||||
const dailyUsage = monthlyUsage / 30; // kWh per day
|
||||
const systemSize = Math.ceil(dailyUsage / sunlightHoursNum); // kW system size
|
||||
|
||||
// Roof type efficiency factors
|
||||
const roofFactors: { [key: string]: number } = {
|
||||
asphalt: 1.0,
|
||||
tile: 0.95,
|
||||
metal: 1.05,
|
||||
flat: 0.9
|
||||
};
|
||||
|
||||
const roofFactor = roofFactors[roofType] || 1.0;
|
||||
const adjustedSystemSize = Math.max(3, Math.min(20, systemSize * roofFactor));
|
||||
|
||||
// Cost calculations (converted to EUR for German market)
|
||||
const costPerWatt = 2.8; // Average cost per watt installed in EUR
|
||||
const installCost = adjustedSystemSize * 1000 * costPerWatt;
|
||||
const federalTaxCredit = installCost * 0.19; // German VAT can be deducted
|
||||
const netCost = installCost - federalTaxCredit;
|
||||
|
||||
// Energy production calculations
|
||||
const annualProduction = adjustedSystemSize * sunlightHoursNum * 365 * 0.85; // 85% efficiency
|
||||
const monthlyProduction = annualProduction / 12;
|
||||
|
||||
// Savings calculations
|
||||
const monthlySavings = Math.min(monthlyProduction * electricityRateNum, monthlyBillNum * 0.9);
|
||||
const yearlySavings = monthlySavings * 12;
|
||||
const lifetimeSavings = yearlySavings * 25; // 25-year system life
|
||||
|
||||
// Payback period
|
||||
const paybackPeriod = Math.round(netCost / yearlySavings * 10) / 10;
|
||||
|
||||
setResults({
|
||||
monthlySavings: Math.round(monthlySavings),
|
||||
yearlySavings: Math.round(yearlySavings),
|
||||
lifetimeSavings: Math.round(lifetimeSavings),
|
||||
systemSize: Math.round(adjustedSystemSize * 10) / 10,
|
||||
installCost: Math.round(installCost),
|
||||
taxCredit: Math.round(federalTaxCredit),
|
||||
netCost: Math.round(netCost),
|
||||
paybackPeriod
|
||||
});
|
||||
|
||||
setShowResults(true);
|
||||
|
||||
// Track calculator usage
|
||||
try {
|
||||
await analyticsService.trackEvent({
|
||||
event_type: 'solar_calculator_used',
|
||||
page_url: window.location.pathname,
|
||||
user_agent: navigator.userAgent,
|
||||
session_id: sessionStorage.getItem('session_id') || 'anonymous',
|
||||
event_data: {
|
||||
monthly_bill: monthlyBillNum,
|
||||
home_size: homeSizeNum,
|
||||
roof_type: roofType,
|
||||
sunlight_hours: sunlightHoursNum,
|
||||
electricity_rate: electricityRateNum,
|
||||
zip_code: zipCode,
|
||||
calculated_savings: {
|
||||
monthly: Math.round(monthlySavings),
|
||||
yearly: Math.round(yearlySavings),
|
||||
lifetime: Math.round(lifetimeSavings),
|
||||
system_size: Math.round(adjustedSystemSize * 10) / 10,
|
||||
payback_period: paybackPeriod
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error tracking calculator usage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-populate electricity rate based on ZIP code (German postal codes)
|
||||
useEffect(() => {
|
||||
if (zipCode.length === 5) {
|
||||
// Simplified German electricity rates by region
|
||||
const rates: { [key: string]: number } = {
|
||||
'0': 0.32, // Eastern Germany
|
||||
'1': 0.34, // Berlin/Brandenburg
|
||||
'2': 0.33, // Northern Germany
|
||||
'3': 0.35, // Central Germany
|
||||
'4': 0.33, // Western Germany
|
||||
'5': 0.34, // NRW
|
||||
'6': 0.35, // Hessen/Baden-Württemberg
|
||||
'7': 0.33, // Baden-Württemberg
|
||||
'8': 0.32, // Bavaria
|
||||
'9': 0.31 // Bavaria
|
||||
};
|
||||
|
||||
const firstDigit = zipCode[0];
|
||||
const rate = rates[firstDigit] || 0.33;
|
||||
setElectricityRate(rate.toFixed(2));
|
||||
}
|
||||
}, [zipCode]);
|
||||
|
||||
// Submit quote request
|
||||
const handleQuoteSubmission = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!customerName || !customerEmail || !results) {
|
||||
alert('Bitte füllen Sie alle Pflichtfelder aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await quoteService.submitQuote({
|
||||
customer_name: customerName,
|
||||
customer_email: customerEmail,
|
||||
customer_phone: customerPhone,
|
||||
energy_type: 'solar',
|
||||
location: zipCode,
|
||||
monthly_bill: parseFloat(monthlyBill),
|
||||
property_size: parseFloat(homeSize),
|
||||
property_type: 'residential',
|
||||
roof_type: roofType,
|
||||
estimated_cost: results.installCost,
|
||||
estimated_savings: results.yearlySavings,
|
||||
additional_requirements: `Berechnet: ${results.systemSize} kW System, ${results.paybackPeriod} Jahre Amortisation`,
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
alert('Ihr Angebot wurde erfolgreich eingereicht! Sie erhalten bald Kontakt von qualifizierten Installateuren.');
|
||||
setShowQuoteForm(false);
|
||||
|
||||
// Reset form
|
||||
setCustomerName('');
|
||||
setCustomerEmail('');
|
||||
setCustomerPhone('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error submitting quote:', error);
|
||||
alert('Fehler beim Senden des Angebots. Bitte versuchen Sie es später erneut.');
|
||||
}
|
||||
};
|
||||
|
||||
const createSavingsChart = () => {
|
||||
if (!results) return null;
|
||||
|
||||
const bars = [
|
||||
{ label: 'Monat 1', value: results.monthlySavings },
|
||||
{ label: 'Jahr 1', value: results.yearlySavings },
|
||||
{ label: '5 Jahre', value: results.yearlySavings * 5 },
|
||||
{ label: '10 Jahre', value: results.yearlySavings * 10 },
|
||||
{ label: '25 Jahre', value: results.lifetimeSavings }
|
||||
];
|
||||
|
||||
const maxValue = results.lifetimeSavings;
|
||||
|
||||
return (
|
||||
<div className="flex items-end justify-center h-32 gap-3 mt-4">
|
||||
{bars.map((bar, index) => (
|
||||
<div key={index} className="flex flex-col items-center">
|
||||
<div
|
||||
className="bg-gradient-to-t from-solar to-solar-light rounded-t-sm w-6 transition-all duration-1000 ease-out"
|
||||
style={{
|
||||
height: `${(bar.value / maxValue) * 120}px`,
|
||||
minHeight: '10px'
|
||||
}}
|
||||
></div>
|
||||
<span className="text-xs text-muted-foreground mt-2 text-center">
|
||||
{bar.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-blue-900 via-blue-700 to-cyan-500 py-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="bg-white rounded-3xl shadow-2xl overflow-hidden backdrop-blur-sm">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-orange-400 via-orange-500 to-red-500 text-white p-12 text-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div className="absolute inset-0" style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Ccircle cx='30' cy='30' r='4'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
|
||||
animation: 'float 20s linear infinite'
|
||||
}}></div>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-white to-yellow-100 bg-clip-text text-transparent">
|
||||
Solar-Einsparungsrechner
|
||||
</h2>
|
||||
<p className="text-xl opacity-95 font-light">
|
||||
Entdecken Sie, wie viel Sie mit Solarenergie sparen können
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0">
|
||||
{/* Calculator Section */}
|
||||
<div className="p-8 lg:p-12 bg-gradient-to-br from-slate-50 to-slate-100 border-r border-gray-200">
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="flex gap-4">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold text-gray-800 mb-8 relative">
|
||||
Berechnen Sie Ihre Solar-Einsparungen
|
||||
<div className="absolute bottom-0 left-0 w-16 h-1 bg-gradient-to-r from-orange-400 to-orange-500 rounded"></div>
|
||||
</h3>
|
||||
|
||||
<Card className="bg-white/70 border-gray-200 backdrop-blur-sm mb-6">
|
||||
<CardContent className="p-6">
|
||||
<div className="grid gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Durchschnittliche monatliche Stromrechnung
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600 font-semibold">€</span>
|
||||
<Input
|
||||
type="number"
|
||||
value={monthlyBill}
|
||||
onChange={(e) => setMonthlyBill(e.target.value)}
|
||||
placeholder="150"
|
||||
className="pl-8 h-12 border-2 border-gray-200 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Hausgröße (m²)
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={homeSize}
|
||||
onChange={(e) => setHomeSize(e.target.value)}
|
||||
placeholder="200"
|
||||
className="h-12 border-2 border-gray-200 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Dachtyp
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<select
|
||||
value={roofType}
|
||||
onChange={(e) => setRoofType(e.target.value)}
|
||||
className="h-12 w-full rounded-md border-2 border-gray-200 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||
>
|
||||
<option value="asphalt">Ziegeldach</option>
|
||||
<option value="tile">Tonziegel</option>
|
||||
<option value="metal">Metalldach</option>
|
||||
<option value="flat">Flachdach</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 border-gray-200 backdrop-blur-sm mb-8">
|
||||
<CardContent className="p-6">
|
||||
<div className="grid gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Tägliche Sonnenstunden
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<select
|
||||
value={sunlightHours}
|
||||
onChange={(e) => setSunlightHours(e.target.value)}
|
||||
className="h-12 w-full rounded-md border-2 border-gray-200 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||
>
|
||||
<option value="4">4 Stunden (Weniger sonnige Gebiete)</option>
|
||||
<option value="5">5 Stunden (Mäßige Sonne)</option>
|
||||
<option value="6">6 Stunden (Gute Sonneneinstrahlung)</option>
|
||||
<option value="7">7 Stunden (Sehr sonnig)</option>
|
||||
<option value="8">8+ Stunden (Ausgezeichnete Sonne)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Strompreis (pro kWh)
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600 font-semibold">€</span>
|
||||
<Input
|
||||
type="number"
|
||||
value={electricityRate}
|
||||
onChange={(e) => setElectricityRate(e.target.value)}
|
||||
step="0.01"
|
||||
placeholder="0.33"
|
||||
className="pl-8 h-12 border-2 border-gray-200 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Postleitzahl
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={zipCode}
|
||||
onChange={(e) => setZipCode(e.target.value)}
|
||||
placeholder="12345"
|
||||
pattern="[0-9]{5}"
|
||||
className="h-12 border-2 border-gray-200 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button
|
||||
onClick={calculateSavings}
|
||||
className="w-full h-14 text-lg font-bold bg-gradient-to-r from-blue-600 to-blue-800 hover:from-blue-700 hover:to-blue-900 transform hover:-translate-y-1 transition-all duration-300 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
Meine Einsparungen berechnen
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Results Section */}
|
||||
<div className="p-8 lg:p-12 bg-gradient-to-br from-white to-gray-50">
|
||||
<h3 className="text-2xl font-bold text-gray-800 mb-8 relative">
|
||||
Ihre Solar-Einsparungen
|
||||
<div className="absolute bottom-0 left-0 w-16 h-1 bg-gradient-to-r from-blue-400 to-cyan-400 rounded"></div>
|
||||
</h3>
|
||||
|
||||
{showResults && results ? (
|
||||
<div className="space-y-6 animate-in fade-in duration-500">
|
||||
<Card className="bg-gradient-to-r from-purple-600 to-purple-800 text-white">
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="text-3xl font-bold mb-1">€{results.monthlySavings}</div>
|
||||
<div className="opacity-90">Monatliche Einsparungen</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-r from-green-600 to-green-800 text-white">
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="text-3xl font-bold mb-1">€{results.yearlySavings.toLocaleString()}</div>
|
||||
<div className="opacity-90">Jährliche Einsparungen</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-r from-orange-600 to-red-600 text-white">
|
||||
<CardContent className="p-6 text-center">
|
||||
<div className="text-3xl font-bold mb-1">€{results.lifetimeSavings.toLocaleString()}</div>
|
||||
<div className="opacity-90">25-Jahre Einsparungen</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-50 border-gray-200">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-gray-800">Kostenaufschlüsselung</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span>Anlagengröße:</span>
|
||||
<span className="font-semibold">{results.systemSize} kW</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span>Installationskosten:</span>
|
||||
<span className="font-semibold">€{results.installCost.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span>Förderung/MwSt. (19%):</span>
|
||||
<span className="font-semibold text-green-600">-€{results.taxCredit.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-gray-200">
|
||||
<span>Nettokosten:</span>
|
||||
<span className="font-semibold">€{results.netCost.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 font-bold text-green-600">
|
||||
<span>Amortisationsdauer:</span>
|
||||
<span>{results.paybackPeriod} Jahre</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-50 border-gray-200">
|
||||
<CardContent className="p-6">
|
||||
<h4 className="font-semibold text-center mb-4 text-gray-800">
|
||||
Einsparungen über die Zeit
|
||||
</h4>
|
||||
{createSavingsChart()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quote Request Button */}
|
||||
<div className="text-center">
|
||||
<Button
|
||||
onClick={() => setShowQuoteForm(true)}
|
||||
className="bg-green-600 hover:bg-green-700 text-white font-semibold shadow-lg"
|
||||
size="lg"
|
||||
>
|
||||
Kostenloses Angebot anfordern
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-gray-500 py-12">
|
||||
<div className="text-lg mb-2">Bereit zur Berechnung</div>
|
||||
<div className="text-sm">Füllen Sie das Formular aus und klicken Sie auf "Berechnen"</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quote Form Modal */}
|
||||
{showQuoteForm && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<Card className="max-w-md w-full mx-4">
|
||||
<CardHeader>
|
||||
<CardTitle>Kostenloses Angebot anfordern</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleQuoteSubmission} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Name *
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={customerName}
|
||||
onChange={(e) => setCustomerName(e.target.value)}
|
||||
required
|
||||
placeholder="Ihr vollständiger Name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
E-Mail *
|
||||
</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={customerEmail}
|
||||
onChange={(e) => setCustomerEmail(e.target.value)}
|
||||
required
|
||||
placeholder="ihre.email@beispiel.de"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Telefon (optional)
|
||||
</label>
|
||||
<Input
|
||||
type="tel"
|
||||
value={customerPhone}
|
||||
onChange={(e) => setCustomerPhone(e.target.value)}
|
||||
placeholder="+49 123 456 7890"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setShowQuoteForm(false)}
|
||||
className="flex-1"
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex-1 bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
Angebot anfordern
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SolarCalculator;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Shield, Users, Award, Clock, HeartHandshake, TrendingUp } from "lucide-react";
|
||||
import { Shield, Award, Clock, TrendingUp } from "lucide-react";
|
||||
|
||||
const WhyChooseUsSection = () => {
|
||||
const features = [
|
||||
|
|
@ -9,29 +9,17 @@ const WhyChooseUsSection = () => {
|
|||
description: "Alle Fachbetriebe werden sorgfältig geprüft und zertifiziert für höchste Qualitätsstandards.",
|
||||
gradient: "bg-gradient-solar"
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Kostenlose Beratung",
|
||||
description: "Erhalten Sie unverbindliche Beratung und bis zu 3 Angebote von qualifizierten Experten.",
|
||||
gradient: "bg-gradient-wind"
|
||||
},
|
||||
{
|
||||
icon: Award,
|
||||
title: "Beste Preise",
|
||||
description: "Durch unseren Vergleich erhalten Sie garantiert die besten Konditionen für Ihr Projekt.",
|
||||
gradient: "bg-gradient-geo"
|
||||
gradient: "bg-gradient-solar"
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
title: "Schnelle Vermittlung",
|
||||
description: "In nur wenigen Minuten erhalten Sie passende Installateur-Vorschläge für Ihre Region.",
|
||||
gradient: "bg-gradient-battery"
|
||||
},
|
||||
{
|
||||
icon: HeartHandshake,
|
||||
title: "Persönlicher Service",
|
||||
description: "Unser Expertenteam steht Ihnen bei allen Fragen rund um erneuerbare Energien zur Seite.",
|
||||
gradient: "bg-gradient-solar"
|
||||
gradient: "bg-gradient-wind"
|
||||
},
|
||||
{
|
||||
icon: TrendingUp,
|
||||
|
|
@ -54,7 +42,7 @@ const WhyChooseUsSection = () => {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{features.map((feature, index) => (
|
||||
<Card key={index} className="group hover:shadow-xl transition-all duration-300 border-0 bg-white/50 backdrop-blur-sm">
|
||||
<CardContent className="p-8 text-center">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,474 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { HelpCircle, Wind, Zap } from 'lucide-react';
|
||||
import { analyticsService } from '@/lib/database';
|
||||
|
||||
interface WindCalculatorResults {
|
||||
monthlySavings: number;
|
||||
yearlySavings: number;
|
||||
lifetimeSavings: number;
|
||||
turbineSize: number;
|
||||
installCost: number;
|
||||
taxCredit: number;
|
||||
netCost: number;
|
||||
paybackPeriod: number;
|
||||
}
|
||||
|
||||
const WindCalculator = () => {
|
||||
const [monthlyBill, setMonthlyBill] = useState<string>('150');
|
||||
const [propertySize, setPropertySize] = useState<string>('2');
|
||||
const [zoning, setZoning] = useState<string>('rural');
|
||||
const [windSpeed, setWindSpeed] = useState<string>('12');
|
||||
const [turbineHeight, setTurbineHeight] = useState<string>('80');
|
||||
const [electricityRate, setElectricityRate] = useState<string>('0.33');
|
||||
const [zipCode, setZipCode] = useState<string>('');
|
||||
const [results, setResults] = useState<WindCalculatorResults | null>(null);
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
|
||||
const calculateWindSavings = async () => {
|
||||
const monthlyBillNum = parseFloat(monthlyBill) || 0;
|
||||
const propertySizeNum = parseFloat(propertySize) || 0;
|
||||
const windSpeedNum = parseFloat(windSpeed) || 12;
|
||||
const turbineHeightNum = parseFloat(turbineHeight) || 80;
|
||||
const electricityRateNum = parseFloat(electricityRate) || 0.33;
|
||||
|
||||
if (monthlyBillNum === 0) {
|
||||
alert('Bitte geben Sie Ihre monatliche Stromrechnung ein');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wind power calculation (simplified for German market)
|
||||
const airDensity = 1.225; // kg/m³ at sea level
|
||||
const powerCoefficient = 0.35; // Typical for small wind turbines
|
||||
const efficiency = 0.85;
|
||||
|
||||
// Determine turbine size based on property and zoning
|
||||
let maxTurbineKW = 0;
|
||||
const zoningSizeFactors: { [key: string]: number } = {
|
||||
rural: 1.5,
|
||||
agricultural: 2.0,
|
||||
suburban: 0.8,
|
||||
urban: 0.5
|
||||
};
|
||||
|
||||
const sizeFactor = zoningSizeFactors[zoning] || 1.0;
|
||||
|
||||
// Base turbine size on property size and height allowance
|
||||
if (propertySizeNum >= 0.8 && turbineHeightNum >= 80) { // 0.8 hectares ≈ 2 acres
|
||||
maxTurbineKW = Math.min(20, propertySizeNum * 3 * sizeFactor);
|
||||
} else if (propertySizeNum >= 0.4 && turbineHeightNum >= 60) {
|
||||
maxTurbineKW = Math.min(10, propertySizeNum * 2 * sizeFactor);
|
||||
} else {
|
||||
maxTurbineKW = Math.min(5, propertySizeNum * 1.5 * sizeFactor);
|
||||
}
|
||||
|
||||
// Wind speed factor affects actual output
|
||||
let windSpeedFactor = 1.0;
|
||||
if (windSpeedNum >= 15) windSpeedFactor = 1.4;
|
||||
else if (windSpeedNum >= 12) windSpeedFactor = 1.0;
|
||||
else if (windSpeedNum >= 10) windSpeedFactor = 0.7;
|
||||
else windSpeedFactor = 0.4;
|
||||
|
||||
const effectiveTurbineKW = maxTurbineKW * windSpeedFactor;
|
||||
|
||||
// Height bonus (higher = better wind)
|
||||
let heightFactor = 1.0;
|
||||
if (turbineHeightNum >= 120) heightFactor = 1.3;
|
||||
else if (turbineHeightNum >= 100) heightFactor = 1.2;
|
||||
else if (turbineHeightNum >= 80) heightFactor = 1.1;
|
||||
else if (turbineHeightNum >= 60) heightFactor = 1.0;
|
||||
else heightFactor = 0.9;
|
||||
|
||||
const finalTurbineKW = Math.max(1, effectiveTurbineKW * heightFactor);
|
||||
|
||||
// Annual energy production (kWh/year)
|
||||
// Small wind turbines typically produce 25-35% capacity factor
|
||||
const capacityFactor = Math.min(0.35, windSpeedNum * 0.025);
|
||||
const annualProduction = finalTurbineKW * 8760 * capacityFactor; // 8760 hours/year
|
||||
const monthlyProduction = annualProduction / 12;
|
||||
|
||||
// Cost calculations (adapted for German market)
|
||||
const costPerKW = 6500; // EUR - Higher than solar due to complexity, adapted for German market
|
||||
const installCost = finalTurbineKW * costPerKW;
|
||||
const germanIncentives = installCost * 0.25; // KfW subsidies and regional incentives
|
||||
const netCost = installCost - germanIncentives;
|
||||
|
||||
// Savings calculations
|
||||
const monthlyUsage = monthlyBillNum / electricityRateNum;
|
||||
const monthlySavings = Math.min(monthlyProduction * electricityRateNum, monthlyBillNum * 0.8);
|
||||
const yearlySavings = monthlySavings * 12;
|
||||
const lifetimeSavings = yearlySavings * 20; // 20-year turbine life
|
||||
|
||||
// Payback period
|
||||
const paybackPeriod = Math.round(netCost / yearlySavings * 10) / 10;
|
||||
|
||||
setResults({
|
||||
monthlySavings: Math.round(monthlySavings),
|
||||
yearlySavings: Math.round(yearlySavings),
|
||||
lifetimeSavings: Math.round(lifetimeSavings),
|
||||
turbineSize: Math.round(finalTurbineKW * 10) / 10,
|
||||
installCost: Math.round(installCost),
|
||||
taxCredit: Math.round(germanIncentives),
|
||||
netCost: Math.round(netCost),
|
||||
paybackPeriod
|
||||
});
|
||||
|
||||
setShowResults(true);
|
||||
|
||||
// Track wind calculator usage
|
||||
try {
|
||||
await analyticsService.trackEvent({
|
||||
event_type: 'wind_calculator_used',
|
||||
page_url: window.location.pathname,
|
||||
user_agent: navigator.userAgent,
|
||||
session_id: sessionStorage.getItem('session_id') || 'anonymous',
|
||||
event_data: {
|
||||
monthly_bill: monthlyBillNum,
|
||||
property_size: propertySizeNum,
|
||||
zoning: zoning,
|
||||
wind_speed: windSpeedNum,
|
||||
turbine_height: turbineHeightNum,
|
||||
electricity_rate: electricityRateNum,
|
||||
zip_code: zipCode,
|
||||
calculated_savings: {
|
||||
monthly: Math.round(monthlySavings),
|
||||
yearly: Math.round(yearlySavings),
|
||||
lifetime: Math.round(lifetimeSavings),
|
||||
turbine_size: Math.round(finalTurbineKW * 10) / 10,
|
||||
payback_period: paybackPeriod
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error tracking wind calculator usage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-populate electricity rate based on German ZIP code
|
||||
useEffect(() => {
|
||||
if (zipCode.length === 5) {
|
||||
// German electricity rates by region (higher than solar calculator due to wind being less common)
|
||||
const rates: { [key: string]: number } = {
|
||||
'0': 0.34, // Eastern Germany
|
||||
'1': 0.36, // Berlin/Brandenburg
|
||||
'2': 0.35, // Northern Germany (better wind resources)
|
||||
'3': 0.37, // Central Germany
|
||||
'4': 0.35, // Western Germany
|
||||
'5': 0.36, // NRW
|
||||
'6': 0.37, // Hessen/Baden-Württemberg
|
||||
'7': 0.35, // Baden-Württemberg
|
||||
'8': 0.34, // Bavaria
|
||||
'9': 0.33 // Bavaria
|
||||
};
|
||||
|
||||
const firstDigit = zipCode[0];
|
||||
const rate = rates[firstDigit] || 0.35;
|
||||
setElectricityRate(rate.toFixed(2));
|
||||
}
|
||||
}, [zipCode]);
|
||||
|
||||
const createSavingsChart = () => {
|
||||
if (!results) return null;
|
||||
|
||||
const bars = [
|
||||
{ label: 'Monat 1', value: results.monthlySavings },
|
||||
{ label: 'Jahr 1', value: results.yearlySavings },
|
||||
{ label: '5 Jahre', value: results.yearlySavings * 5 },
|
||||
{ label: '10 Jahre', value: results.yearlySavings * 10 },
|
||||
{ label: '20 Jahre', value: results.lifetimeSavings }
|
||||
];
|
||||
|
||||
const maxValue = results.lifetimeSavings;
|
||||
|
||||
return (
|
||||
<div className="flex items-end justify-center h-32 gap-3 mt-4">
|
||||
{bars.map((bar, index) => (
|
||||
<div key={index} className="flex flex-col items-center">
|
||||
<div
|
||||
className="bg-gradient-to-t from-wind-dark to-wind-light rounded-t-sm w-6 transition-all duration-1000 ease-out animate-pulse"
|
||||
style={{
|
||||
height: `${(bar.value / maxValue) * 120}px`,
|
||||
minHeight: '10px'
|
||||
}}
|
||||
></div>
|
||||
<span className="text-xs text-muted-foreground mt-2 text-center">
|
||||
{bar.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-teal-800 via-emerald-700 to-green-600 py-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="bg-white rounded-3xl shadow-2xl overflow-hidden backdrop-blur-sm">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-cyan-600 via-teal-600 to-slate-700 text-white p-12 text-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M30 30c0-11.046 8.954-20 20-20s20 8.954 20 20-8.954 20-20 20-20-8.954-20-20zm0 0c0 11.046-8.954 20-20 20s-20-8.954-20-20 8.954-20 20-20 20 8.954 20 20z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
|
||||
animation: 'windFlow 15s linear infinite'
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4 bg-gradient-to-r from-white to-cyan-100 bg-clip-text text-transparent">
|
||||
<Wind className="inline-block mr-3 animate-spin" />
|
||||
Windenergie-Einsparungsrechner
|
||||
</h2>
|
||||
<p className="text-xl opacity-95 font-light">
|
||||
Nutzen Sie die Kraft des Windes, um Ihre Energiekosten zu senken
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0">
|
||||
{/* Calculator Section */}
|
||||
<div className="p-8 lg:p-12 bg-gradient-to-br from-emerald-50 to-green-100 border-r border-green-200">
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="flex gap-4">
|
||||
<div className="w-3 h-3 rounded-full bg-emerald-600"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold text-gray-800 mb-8 relative">
|
||||
<Wind className="inline mr-2 text-emerald-600 animate-spin" />
|
||||
Berechnen Sie Ihre Windenergie-Einsparungen
|
||||
<div className="absolute bottom-0 left-0 w-16 h-1 bg-gradient-to-r from-cyan-500 to-teal-600 rounded"></div>
|
||||
</h3>
|
||||
|
||||
<Card className="bg-white/70 border-green-200 backdrop-blur-sm mb-6">
|
||||
<CardContent className="p-6">
|
||||
<div className="grid gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Durchschnittliche monatliche Stromrechnung
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600 font-semibold">€</span>
|
||||
<Input
|
||||
type="number"
|
||||
value={monthlyBill}
|
||||
onChange={(e) => setMonthlyBill(e.target.value)}
|
||||
placeholder="150"
|
||||
className="pl-8 h-12 border-2 border-green-200 focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Grundstücksgröße (Hektar)
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={propertySize}
|
||||
onChange={(e) => setPropertySize(e.target.value)}
|
||||
placeholder="0.8"
|
||||
step="0.1"
|
||||
min="0"
|
||||
className="h-12 border-2 border-green-200 focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Grundstücksart
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<select
|
||||
value={zoning}
|
||||
onChange={(e) => setZoning(e.target.value)}
|
||||
className="h-12 w-full rounded-md border-2 border-green-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none"
|
||||
>
|
||||
<option value="rural">Ländlich</option>
|
||||
<option value="suburban">Vorstädtisch</option>
|
||||
<option value="urban">Städtisch</option>
|
||||
<option value="agricultural">Landwirtschaftlich</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/70 border-green-200 backdrop-blur-sm mb-8">
|
||||
<CardContent className="p-6">
|
||||
<div className="grid gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Durchschnittliche Windgeschwindigkeit (km/h)
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<select
|
||||
value={windSpeed}
|
||||
onChange={(e) => setWindSpeed(e.target.value)}
|
||||
className="h-12 w-full rounded-md border-2 border-green-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none"
|
||||
>
|
||||
<option value="8">13 km/h (Schwache Windzone)</option>
|
||||
<option value="10">16 km/h (Mäßiger Wind)</option>
|
||||
<option value="12">19 km/h (Gute Windressource)</option>
|
||||
<option value="15">24 km/h (Ausgezeichneter Wind)</option>
|
||||
<option value="18">29+ km/h (Hervorragender Wind)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Max. erlaubte Anlagenhöhe (m)
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<select
|
||||
value={turbineHeight}
|
||||
onChange={(e) => setTurbineHeight(e.target.value)}
|
||||
className="h-12 w-full rounded-md border-2 border-green-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none"
|
||||
>
|
||||
<option value="40">12 m (Eingeschränkte Gebiete)</option>
|
||||
<option value="60">18 m (Vorstädtisches Limit)</option>
|
||||
<option value="80">24 m (Standard Wohngebiet)</option>
|
||||
<option value="100">30 m (Ländliche Gebiete)</option>
|
||||
<option value="120">36+ m (Keine Beschränkungen)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Strompreis (pro kWh)
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600 font-semibold">€</span>
|
||||
<Input
|
||||
type="number"
|
||||
value={electricityRate}
|
||||
onChange={(e) => setElectricityRate(e.target.value)}
|
||||
step="0.01"
|
||||
placeholder="0.35"
|
||||
className="pl-8 h-12 border-2 border-green-200 focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||
Postleitzahl
|
||||
<HelpCircle className="inline w-4 h-4 ml-1 text-gray-400" />
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={zipCode}
|
||||
onChange={(e) => setZipCode(e.target.value)}
|
||||
placeholder="12345"
|
||||
pattern="[0-9]{5}"
|
||||
className="h-12 border-2 border-green-200 focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button
|
||||
onClick={calculateWindSavings}
|
||||
className="w-full h-14 text-lg font-bold bg-gradient-to-r from-emerald-600 to-teal-700 hover:from-emerald-700 hover:to-teal-800 transform hover:-translate-y-1 transition-all duration-300 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<Wind className="w-5 h-5 mr-2" />
|
||||
Meine Windenergie-Einsparungen berechnen
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Results Section */}
|
||||
<div className="p-8 lg:p-12 bg-gradient-to-br from-white to-gray-50">
|
||||
<h3 className="text-2xl font-bold text-gray-800 mb-8 relative">
|
||||
<Zap className="inline mr-2 text-emerald-600" />
|
||||
Ihre Windenergie-Einsparungen
|
||||
<div className="absolute bottom-0 left-0 w-16 h-1 bg-gradient-to-r from-emerald-500 to-green-600 rounded"></div>
|
||||
</h3>
|
||||
|
||||
{showResults && results ? (
|
||||
<div className="space-y-6 animate-in fade-in duration-500">
|
||||
<Card className="bg-gradient-to-r from-emerald-600 to-teal-700 text-white relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/10 to-transparent"></div>
|
||||
<CardContent className="p-6 text-center relative z-10">
|
||||
<div className="text-3xl font-bold mb-1">€{results.monthlySavings}</div>
|
||||
<div className="opacity-90">Monatliche Einsparungen</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-r from-teal-600 to-cyan-700 text-white relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/10 to-transparent"></div>
|
||||
<CardContent className="p-6 text-center relative z-10">
|
||||
<div className="text-3xl font-bold mb-1">€{results.yearlySavings.toLocaleString()}</div>
|
||||
<div className="opacity-90">Jährliche Einsparungen</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-r from-cyan-600 to-blue-700 text-white relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-white/10 to-transparent"></div>
|
||||
<CardContent className="p-6 text-center relative z-10">
|
||||
<div className="text-3xl font-bold mb-1">€{results.lifetimeSavings.toLocaleString()}</div>
|
||||
<div className="opacity-90">20-Jahre Einsparungen</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-emerald-50 to-green-100 border-green-200">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-gray-800">Windenergie-System Aufschlüsselung</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between py-2 border-b border-green-200">
|
||||
<span>Anlagengröße:</span>
|
||||
<span className="font-semibold">{results.turbineSize} kW</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-green-200">
|
||||
<span>Installationskosten:</span>
|
||||
<span className="font-semibold">€{results.installCost.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-green-200">
|
||||
<span>Förderung/Zuschüsse (25%):</span>
|
||||
<span className="font-semibold text-green-600">-€{results.taxCredit.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 border-b border-green-200">
|
||||
<span>Nettokosten nach Förderung:</span>
|
||||
<span className="font-semibold">€{results.netCost.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between py-2 font-bold text-emerald-600">
|
||||
<span>Amortisationsdauer:</span>
|
||||
<span>{results.paybackPeriod} Jahre</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gradient-to-br from-emerald-50 to-green-100 border-green-200">
|
||||
<CardContent className="p-6">
|
||||
<h4 className="font-semibold text-center mb-4 text-gray-800">
|
||||
Windenergie-Einsparungen über die Zeit
|
||||
</h4>
|
||||
{createSavingsChart()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-gray-500 py-12">
|
||||
<Wind className="w-16 h-16 mx-auto mb-4 text-emerald-400 animate-spin" />
|
||||
<div className="text-lg mb-2">Bereit zur Berechnung</div>
|
||||
<div className="text-sm">Füllen Sie das Formular aus und klicken Sie auf "Berechnen"</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WindCalculator;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Canonical URL component for Answer Engine Optimization (AEO)
|
||||
* Ensures single canonical URL per page for better AI indexing
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
interface CanonicalProps {
|
||||
url?: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
const Canonical: React.FC<CanonicalProps> = ({
|
||||
url,
|
||||
baseUrl = 'https://energie-profis.de'
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
// Remove any existing canonical links
|
||||
const existingCanonical = document.querySelectorAll('link[rel="canonical"]');
|
||||
existingCanonical.forEach(link => link.remove());
|
||||
|
||||
// Generate canonical URL
|
||||
const canonicalUrl = url || `${baseUrl.replace(/\/$/, '')}${location.pathname}`;
|
||||
|
||||
// Create new canonical link
|
||||
const canonical = document.createElement('link');
|
||||
canonical.rel = 'canonical';
|
||||
canonical.href = canonicalUrl;
|
||||
document.head.appendChild(canonical);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (canonical.parentNode) {
|
||||
canonical.parentNode.removeChild(canonical);
|
||||
}
|
||||
};
|
||||
}, [url, baseUrl, location.pathname]);
|
||||
|
||||
return null; // This component doesn't render anything
|
||||
};
|
||||
|
||||
export default Canonical;
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* Content metadata component with visible last updated and author information
|
||||
* Supports sitemap synchronization for accurate lastmod dates
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Calendar, User, Clock } from 'lucide-react';
|
||||
|
||||
interface ContentMetaProps {
|
||||
lastUpdated: Date;
|
||||
authorName?: string;
|
||||
authorUrl?: string;
|
||||
publishedDate?: Date;
|
||||
readTime?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ContentMeta: React.FC<ContentMetaProps> = ({
|
||||
lastUpdated,
|
||||
authorName = 'EnergieProfis Redaktion',
|
||||
authorUrl = 'https://energie-profis.de/team',
|
||||
publishedDate,
|
||||
readTime,
|
||||
className = ''
|
||||
}) => {
|
||||
const formatDate = (date: Date): string => {
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
const formatDateTime = (date: Date): string => {
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-wrap items-center gap-4 text-sm text-muted-foreground border-t pt-4 mt-6 ${className}`}>
|
||||
{/* Last Updated - Most important for AEO */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>
|
||||
<strong>Zuletzt aktualisiert:</strong>
|
||||
<time
|
||||
dateTime={formatDateTime(lastUpdated)}
|
||||
className="ml-1 font-medium"
|
||||
>
|
||||
{formatDate(lastUpdated)}
|
||||
</time>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Author Information */}
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
<span>
|
||||
<strong>Autor:</strong>
|
||||
{authorUrl ? (
|
||||
<a
|
||||
href={authorUrl}
|
||||
className="ml-1 font-medium hover:text-primary transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{authorName}
|
||||
</a>
|
||||
) : (
|
||||
<span className="ml-1 font-medium">{authorName}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Published Date (if different from last updated) */}
|
||||
{publishedDate && publishedDate.getTime() !== lastUpdated.getTime() && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>
|
||||
<strong>Veröffentlicht:</strong>
|
||||
<time
|
||||
dateTime={formatDateTime(publishedDate)}
|
||||
className="ml-1"
|
||||
>
|
||||
{formatDate(publishedDate)}
|
||||
</time>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Read Time */}
|
||||
{readTime && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>{readTime} Lesezeit</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentMeta;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* JSON-LD components for Answer Engine Optimization (AEO)
|
||||
* Optimized for ChatGPT, Perplexity, and other AI answer engines
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
interface JsonLdProps {
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
const JsonLd: React.FC<JsonLdProps> = ({ data }) => {
|
||||
useEffect(() => {
|
||||
// Remove existing script with same @type to prevent duplicates
|
||||
const existingScripts = document.querySelectorAll('script[type="application/ld+json"]');
|
||||
existingScripts.forEach(script => {
|
||||
try {
|
||||
const scriptData = JSON.parse(script.textContent || '');
|
||||
if (scriptData['@type'] === data['@type']) {
|
||||
script.remove();
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore invalid JSON
|
||||
}
|
||||
});
|
||||
|
||||
// Create new script tag
|
||||
const script = document.createElement('script');
|
||||
script.type = 'application/ld+json';
|
||||
script.textContent = JSON.stringify(data, null, 2);
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default JsonLd;
|
||||
|
|
@ -21,12 +21,8 @@ const buttonVariants = cva(
|
|||
// Energy-specific variants
|
||||
solar: "bg-gradient-solar text-white hover:shadow-solar transform hover:scale-105",
|
||||
wind: "bg-gradient-wind text-white hover:shadow-wind transform hover:scale-105",
|
||||
geo: "bg-gradient-geo text-white hover:shadow-geo transform hover:scale-105",
|
||||
battery: "bg-gradient-battery text-white hover:shadow-battery transform hover:scale-105",
|
||||
"solar-outline": "border-2 border-solar bg-transparent text-solar hover:bg-solar hover:text-white",
|
||||
"wind-outline": "border-2 border-wind bg-transparent text-wind hover:bg-wind hover:text-white",
|
||||
"geo-outline": "border-2 border-geo bg-transparent text-geo hover:bg-geo hover:text-white",
|
||||
"battery-outline": "border-2 border-battery bg-transparent text-battery hover:bg-battery hover:text-white",
|
||||
hero: "bg-gradient-hero text-white hover:shadow-lg transform hover:scale-105 border border-white/20",
|
||||
},
|
||||
size: {
|
||||
|
|
|
|||
|
|
@ -73,12 +73,20 @@ const SelectContent = React.forwardRef<
|
|||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md",
|
||||
"!transition-none !duration-0 !animate-none",
|
||||
"data-[state=open]:!animate-none data-[state=closed]:!animate-none",
|
||||
"data-[side=bottom]:!translate-y-0 data-[side=left]:!translate-x-0 data-[side=right]:!translate-x-0 data-[side=top]:!translate-y-0",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
style={{
|
||||
animation: 'none !important',
|
||||
transition: 'none !important',
|
||||
transform: 'none !important'
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
import * as React from "react"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface SimpleSelectProps {
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
placeholder?: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SimpleSelect = React.forwardRef<HTMLDivElement, SimpleSelectProps>(
|
||||
({ value, onValueChange, placeholder, children, className, ...props }, ref) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
const [selectedValue, setSelectedValue] = React.useState(value)
|
||||
const [selectedLabel, setSelectedLabel] = React.useState("")
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelectedValue(value)
|
||||
// Find the selected option label
|
||||
const selectedOption = React.Children.toArray(children).find(
|
||||
(child) => React.isValidElement(child) && child.props.value === value
|
||||
)
|
||||
if (selectedOption && React.isValidElement(selectedOption)) {
|
||||
setSelectedLabel(selectedOption.props.children)
|
||||
}
|
||||
}, [value, children])
|
||||
|
||||
const handleSelect = (newValue: string, newLabel: string) => {
|
||||
setSelectedValue(newValue)
|
||||
setSelectedLabel(newLabel)
|
||||
setIsOpen(false)
|
||||
onValueChange(newValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn("relative", className)} {...props}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
)}
|
||||
>
|
||||
<span className={selectedValue ? "text-foreground" : "text-muted-foreground"}>
|
||||
{selectedLabel || placeholder}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 w-full mt-1 bg-popover border border-border rounded-md shadow-md">
|
||||
<div className="p-1">
|
||||
{React.Children.map(children, (child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
return (
|
||||
<button
|
||||
key={child.props.value}
|
||||
type="button"
|
||||
onClick={() => handleSelect(child.props.value, child.props.children)}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
||||
selectedValue === child.props.value && "bg-accent text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
{selectedValue === child.props.value && (
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
{child.props.children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return child
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Click outside to close */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SimpleSelect.displayName = "SimpleSelect"
|
||||
|
||||
interface SimpleSelectItemProps {
|
||||
value: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SimpleSelectItem = ({ value, children }: SimpleSelectItemProps) => {
|
||||
return <div data-value={value}>{children}</div>
|
||||
}
|
||||
|
||||
SimpleSelectItem.displayName = "SimpleSelectItem"
|
||||
|
||||
export { SimpleSelect, SimpleSelectItem }
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { useEffect } from 'react'
|
||||
import { posthog } from '../lib/posthog'
|
||||
|
||||
export const usePostHog = () => {
|
||||
useEffect(() => {
|
||||
// PostHog is already initialized in main.tsx
|
||||
return () => {
|
||||
// Cleanup if needed
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
capture: (event: string, properties?: Record<string, any>) => {
|
||||
posthog.capture(event, properties)
|
||||
},
|
||||
identify: (userId: string, properties?: Record<string, any>) => {
|
||||
posthog.identify(userId, properties)
|
||||
},
|
||||
reset: () => {
|
||||
posthog.reset()
|
||||
},
|
||||
isFeatureEnabled: (flag: string) => {
|
||||
return posthog.isFeatureEnabled(flag)
|
||||
},
|
||||
getFeatureFlag: (flag: string) => {
|
||||
return posthog.getFeatureFlag(flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,18 +26,6 @@
|
|||
--wind-light: 207 100% 85%;
|
||||
--wind-dark: 207 80% 41%;
|
||||
|
||||
/* Energy Type Colors - Geothermal Green */
|
||||
--geo-primary: 122 39% 49%;
|
||||
--geo-secondary: 122 40% 56%;
|
||||
--geo-light: 122 45% 80%;
|
||||
--geo-dark: 122 35% 36%;
|
||||
|
||||
/* Energy Type Colors - Battery Purple */
|
||||
--battery-primary: 291 64% 42%;
|
||||
--battery-secondary: 291 64% 49%;
|
||||
--battery-light: 291 70% 80%;
|
||||
--battery-dark: 291 60% 29%;
|
||||
|
||||
/* Brand Colors */
|
||||
--primary: 218 47% 18%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
|
@ -58,8 +46,6 @@
|
|||
/* Energy Gradients */
|
||||
--gradient-solar: linear-gradient(135deg, hsl(var(--solar-primary)) 0%, hsl(var(--solar-secondary)) 100%);
|
||||
--gradient-wind: linear-gradient(135deg, hsl(var(--wind-primary)) 0%, hsl(var(--wind-secondary)) 100%);
|
||||
--gradient-geo: linear-gradient(135deg, hsl(var(--geo-primary)) 0%, hsl(var(--geo-secondary)) 100%);
|
||||
--gradient-battery: linear-gradient(135deg, hsl(var(--battery-primary)) 0%, hsl(var(--battery-secondary)) 100%);
|
||||
--gradient-hero: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(218 47% 25%) 100%);
|
||||
|
||||
/* Shadows */
|
||||
|
|
@ -68,8 +54,6 @@
|
|||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-solar: 0 8px 32px hsl(var(--solar-primary) / 0.25);
|
||||
--shadow-wind: 0 8px 32px hsl(var(--wind-primary) / 0.25);
|
||||
--shadow-geo: 0 8px 32px hsl(var(--geo-primary) / 0.25);
|
||||
--shadow-battery: 0 8px 32px hsl(var(--battery-primary) / 0.25);
|
||||
|
||||
/* Animations */
|
||||
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
|
@ -189,17 +173,28 @@
|
|||
background-clip: text;
|
||||
}
|
||||
|
||||
.gradient-text-geo {
|
||||
background: var(--gradient-geo);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
/* Solar Calculator Animations */
|
||||
@keyframes float {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(-60px, -60px); }
|
||||
}
|
||||
|
||||
.gradient-text-battery {
|
||||
background: var(--gradient-battery);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@keyframes growUp {
|
||||
from { height: 0; }
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: fade-in 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Wind Calculator Animations */
|
||||
@keyframes windFlow {
|
||||
0% { transform: translate(0, 0) rotate(0deg); }
|
||||
100% { transform: translate(-60px, -60px) rotate(360deg); }
|
||||
}
|
||||
}
|
||||
|
|
@ -398,7 +398,7 @@ export type Database = {
|
|||
}
|
||||
}
|
||||
Enums: {
|
||||
energy_type: "solar" | "wind" | "geothermal" | "battery"
|
||||
energy_type: "solar" | "wind"
|
||||
installer_status: "active" | "inactive" | "pending" | "suspended"
|
||||
quote_status: "pending" | "accepted" | "rejected" | "expired"
|
||||
}
|
||||
|
|
@ -528,7 +528,7 @@ export type CompositeTypes<
|
|||
export const Constants = {
|
||||
public: {
|
||||
Enums: {
|
||||
energy_type: ["solar", "wind", "geothermal", "battery"],
|
||||
energy_type: ["solar", "wind"],
|
||||
installer_status: ["active", "inactive", "pending", "suspended"],
|
||||
quote_status: ["pending", "accepted", "rejected", "expired"],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { comprehensiveInstallers } from "./comprehensiveSeedData";
|
||||
|
||||
export const cleanAndReseedDatabase = async () => {
|
||||
console.log('Cleaning existing installer data...');
|
||||
|
||||
try {
|
||||
// First, delete all existing installers
|
||||
const { error: deleteError } = await supabase
|
||||
.from('installers')
|
||||
.delete()
|
||||
.not('id', 'is', null); // Delete all records
|
||||
|
||||
if (deleteError) {
|
||||
console.error('Error deleting existing data:', deleteError);
|
||||
throw deleteError;
|
||||
}
|
||||
|
||||
console.log('Existing data cleaned successfully!');
|
||||
|
||||
// Insert the comprehensive German installer data
|
||||
const { data, error: insertError } = await supabase
|
||||
.from('installers')
|
||||
.insert(comprehensiveInstallers)
|
||||
.select();
|
||||
|
||||
if (insertError) {
|
||||
console.error('Error inserting German installer data:', insertError);
|
||||
console.error('Insert error details:', {
|
||||
message: insertError.message,
|
||||
details: insertError.details,
|
||||
code: insertError.code,
|
||||
hint: insertError.hint
|
||||
});
|
||||
throw insertError;
|
||||
}
|
||||
|
||||
console.log('German installer data inserted successfully:', data?.length, 'installers added');
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in cleanAndReseedDatabase:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,246 @@
|
|||
import React from "react";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import type { Database } from "@/integrations/supabase/types";
|
||||
|
||||
type Installer = Database['public']['Tables']['installers']['Row'];
|
||||
type InstallerInsert = Database['public']['Tables']['installers']['Insert'];
|
||||
type Quote = Database['public']['Tables']['quotes']['Row'];
|
||||
type QuoteInsert = Database['public']['Tables']['quotes']['Insert'];
|
||||
type ContactClick = Database['public']['Tables']['contact_clicks']['Insert'];
|
||||
type AnalyticsEvent = Database['public']['Tables']['analytics_events']['Insert'];
|
||||
|
||||
// Installer Services
|
||||
export const installerService = {
|
||||
// Get all installers with optional filters
|
||||
async getInstallers(filters?: {
|
||||
energyType?: string;
|
||||
bundesland?: string;
|
||||
searchTerm?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
// First, get the total count
|
||||
let countQuery = supabase
|
||||
.from('installers')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('status', 'active');
|
||||
|
||||
if (filters?.energyType && filters.energyType !== 'all') {
|
||||
countQuery = countQuery.eq('energy_type', filters.energyType);
|
||||
}
|
||||
|
||||
if (filters?.bundesland && filters.bundesland !== 'all') {
|
||||
countQuery = countQuery.ilike('location', `%${filters.bundesland}%`);
|
||||
}
|
||||
|
||||
if (filters?.searchTerm) {
|
||||
countQuery = countQuery.or(`name.ilike.%${filters.searchTerm}%,description.ilike.%${filters.searchTerm}%,specialties.cs.{${filters.searchTerm}}`);
|
||||
}
|
||||
|
||||
const { count, error: countError } = await countQuery;
|
||||
|
||||
if (countError) {
|
||||
console.error('Error fetching installer count:', countError);
|
||||
throw countError;
|
||||
}
|
||||
|
||||
// Then get the actual data with pagination
|
||||
let dataQuery = supabase
|
||||
.from('installers')
|
||||
.select('*')
|
||||
.eq('status', 'active')
|
||||
.order('rating', { ascending: false });
|
||||
|
||||
if (filters?.energyType && filters.energyType !== 'all') {
|
||||
dataQuery = dataQuery.eq('energy_type', filters.energyType);
|
||||
}
|
||||
|
||||
if (filters?.bundesland && filters.bundesland !== 'all') {
|
||||
dataQuery = dataQuery.ilike('location', `%${filters.bundesland}%`);
|
||||
}
|
||||
|
||||
if (filters?.searchTerm) {
|
||||
dataQuery = dataQuery.or(`name.ilike.%${filters.searchTerm}%,description.ilike.%${filters.searchTerm}%,specialties.cs.{${filters.searchTerm}}`);
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
if (filters?.limit) {
|
||||
dataQuery = dataQuery.limit(filters.limit);
|
||||
}
|
||||
if (filters?.offset) {
|
||||
dataQuery = dataQuery.range(filters.offset, filters.offset + (filters.limit || 10) - 1);
|
||||
}
|
||||
|
||||
const { data, error } = await dataQuery;
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching installers:', error);
|
||||
console.error('Error details:', {
|
||||
message: error.message,
|
||||
details: error.details,
|
||||
code: error.code,
|
||||
hint: error.hint
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { data: data || [], totalCount: count || 0 };
|
||||
},
|
||||
|
||||
// Get installer by ID
|
||||
async getInstaller(id: string) {
|
||||
const { data, error } = await supabase
|
||||
.from('installers')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching installer:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
// Add new installer
|
||||
async createInstaller(installer: InstallerInsert) {
|
||||
const { data, error } = await supabase
|
||||
.from('installers')
|
||||
.insert(installer)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating installer:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
// Quote Services
|
||||
export const quoteService = {
|
||||
// Submit new quote request
|
||||
async submitQuote(quote: QuoteInsert) {
|
||||
const { data, error } = await supabase
|
||||
.from('quotes')
|
||||
.insert(quote)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error submitting quote:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
// Get quotes for an installer
|
||||
async getInstallerQuotes(installerId: string) {
|
||||
const { data, error } = await supabase
|
||||
.from('quotes')
|
||||
.select('*')
|
||||
.eq('assigned_installer_id', installerId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching installer quotes:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
// Analytics Services
|
||||
export const analyticsService = {
|
||||
// Track contact click
|
||||
async trackContactClick(contactClick: ContactClick) {
|
||||
const { data, error } = await supabase
|
||||
.from('contact_clicks')
|
||||
.insert(contactClick);
|
||||
|
||||
if (error) {
|
||||
console.error('Error tracking contact click:', error);
|
||||
// Don't throw error for analytics - just log it
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
// Track general analytics event
|
||||
async trackEvent(event: AnalyticsEvent) {
|
||||
const { data, error } = await supabase
|
||||
.from('analytics_events')
|
||||
.insert(event);
|
||||
|
||||
if (error) {
|
||||
console.error('Error tracking analytics event:', error);
|
||||
// Don't throw error for analytics - just log it
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
export const dbUtils = {
|
||||
// Get user's IP address (simplified)
|
||||
async getUserIP(): Promise<string> {
|
||||
try {
|
||||
const response = await fetch('https://api.ipify.org?format=json');
|
||||
const data = await response.json();
|
||||
return data.ip;
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
},
|
||||
|
||||
// Get user agent
|
||||
getUserAgent(): string {
|
||||
return navigator.userAgent;
|
||||
},
|
||||
|
||||
// Generate session ID
|
||||
generateSessionId(): string {
|
||||
return 'session_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
// Hook for getting installers with loading state
|
||||
export const useInstallers = () => {
|
||||
const [installers, setInstallers] = React.useState<Installer[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const fetchInstallers = async (filters?: {
|
||||
energyType?: string;
|
||||
bundesland?: string;
|
||||
searchTerm?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const result = await installerService.getInstallers(filters);
|
||||
setInstallers(result.data || []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
setInstallers([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
installers,
|
||||
loading,
|
||||
error,
|
||||
fetchInstallers,
|
||||
refetch: fetchInstallers
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import { supabase } from "@/integrations/supabase/client";
|
||||
|
||||
export const testConnection = async () => {
|
||||
console.log('=== TESTING SUPABASE CONNECTION ===');
|
||||
|
||||
try {
|
||||
// Test basic connection
|
||||
const { data, error } = await supabase
|
||||
.from('installers')
|
||||
.select('count', { count: 'exact', head: true });
|
||||
|
||||
if (error) {
|
||||
console.error('Connection test failed:', error);
|
||||
console.error('Error details:', {
|
||||
message: error.message,
|
||||
details: error.details,
|
||||
code: error.code,
|
||||
hint: error.hint
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('✅ Connection successful! Table has', data?.length || 0, 'records');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Connection test exception:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const debugDatabase = async () => {
|
||||
console.log('=== DATABASE DEBUG ===');
|
||||
|
||||
try {
|
||||
// Get all installers
|
||||
const { data, error } = await supabase
|
||||
.from('installers')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching installers:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${data?.length || 0} installers in database:`);
|
||||
|
||||
data?.forEach((installer, index) => {
|
||||
console.log(`\n${index + 1}. ${installer.name}`);
|
||||
console.log(` Company: ${installer.company_name}`);
|
||||
console.log(` Location: ${installer.location}`);
|
||||
console.log(` Energy Type: ${installer.energy_type}`);
|
||||
console.log(` Created: ${installer.created_at}`);
|
||||
console.log(` ID: ${installer.id}`);
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Debug error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const forceDeleteAll = async () => {
|
||||
console.log('=== FORCE DELETE ALL INSTALLERS ===');
|
||||
|
||||
try {
|
||||
// First, let's see what's there
|
||||
const { data: allData } = await supabase
|
||||
.from('installers')
|
||||
.select('*');
|
||||
|
||||
console.log('Records before deletion:', allData?.length || 0);
|
||||
allData?.forEach((record, index) => {
|
||||
console.log(`${index + 1}. ${record.name} (ID: ${record.id})`);
|
||||
});
|
||||
|
||||
// Delete ALL records without any conditions
|
||||
const { data, error } = await supabase
|
||||
.from('installers')
|
||||
.delete()
|
||||
.not('id', 'is', null); // This will delete all records
|
||||
|
||||
if (error) {
|
||||
console.error('Error in force delete:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log('All installers deleted successfully');
|
||||
|
||||
// Verify deletion
|
||||
const { data: remainingData } = await supabase
|
||||
.from('installers')
|
||||
.select('*');
|
||||
|
||||
console.log('Records after deletion:', remainingData?.length || 0);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Force delete error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
// Simple email service for form submissions
|
||||
// This uses a free webhook service to send emails
|
||||
|
||||
export const sendCompanyRegistrationEmail = async (formData: any) => {
|
||||
const emailData = {
|
||||
to: 'knuth.timo@gmail.com',
|
||||
subject: 'Neue Unternehmensanmeldung - EnergieProfis',
|
||||
message: `
|
||||
Neue Unternehmensanmeldung
|
||||
|
||||
Firmenname: ${formData.companyName}
|
||||
Ansprechpartner: ${formData.contactPerson}
|
||||
E-Mail: ${formData.email}
|
||||
Telefon: ${formData.phone}
|
||||
Website: ${formData.website || 'Nicht angegeben'}
|
||||
PLZ: ${formData.zipCode}
|
||||
Stadt: ${formData.city}
|
||||
Energiearten: ${formData.energyTypes.join(', ')}
|
||||
Leistungen: ${formData.services.join(', ')}
|
||||
Jahre Erfahrung: ${formData.experience}
|
||||
Einzugsgebiet: ${formData.coverageArea || 'Nicht angegeben'}
|
||||
Kontaktpräferenz: ${formData.contactPreference}
|
||||
Newsletter: ${formData.newsletter ? 'Ja' : 'Nein'}
|
||||
|
||||
Unternehmensbeschreibung:
|
||||
${formData.description}
|
||||
`
|
||||
};
|
||||
|
||||
try {
|
||||
// Log the data for now (this will show in console)
|
||||
console.log('=== NEUE UNTERNEHMENSANMELDUNG ===');
|
||||
console.log('📧 An: knuth.timo@gmail.com');
|
||||
console.log('📋 Betreff: Neue Unternehmensanmeldung - EnergieProfis');
|
||||
console.log('📄 Daten:', formData);
|
||||
console.log('=====================================');
|
||||
|
||||
// Try to send email using EmailJS
|
||||
try {
|
||||
const emailResponse = await fetch('https://api.emailjs.com/api/v1.0/email/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
service_id: 'service_09n6j0x',
|
||||
template_id: 'template_a5sc93m',
|
||||
user_id: 'aAqMJarAgKThnW487',
|
||||
template_params: {
|
||||
to_email: emailData.to,
|
||||
subject: emailData.subject,
|
||||
message: emailData.message,
|
||||
company_name: formData.companyName,
|
||||
contact_person: formData.contactPerson,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
website: formData.website || 'Nicht angegeben',
|
||||
zip_code: formData.zipCode,
|
||||
city: formData.city,
|
||||
energy_types: formData.energyTypes.join(', '),
|
||||
services: formData.services.join(', '),
|
||||
experience: formData.experience,
|
||||
coverage_area: formData.coverageArea || 'Nicht angegeben',
|
||||
contact_preference: formData.contactPreference,
|
||||
newsletter: formData.newsletter ? 'Ja' : 'Nein',
|
||||
description: formData.description
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (emailResponse.ok) {
|
||||
console.log('✅ E-Mail erfolgreich an knuth.timo@gmail.com gesendet!');
|
||||
return { success: true };
|
||||
} else {
|
||||
const errorText = await emailResponse.text();
|
||||
console.log('⚠️ E-Mail-Service Fehler:', emailResponse.status, errorText);
|
||||
console.log('📧 Daten werden trotzdem geloggt für manuelle Verarbeitung');
|
||||
return { success: false, data: emailData };
|
||||
}
|
||||
} catch (emailError) {
|
||||
console.log('⚠️ E-Mail-Service nicht verfügbar, aber Daten wurden geloggt');
|
||||
return { success: false, data: emailData };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
console.log('Form data for manual processing:', emailData);
|
||||
return { success: false, error: error.message, data: emailData };
|
||||
}
|
||||
};
|
||||
|
||||
// Alternative: Simple console logging for development
|
||||
export const logCompanyRegistration = (formData: any) => {
|
||||
console.log('=== NEUE UNTERNEHMENSANMELDUNG ===');
|
||||
console.log('Firmenname:', formData.companyName);
|
||||
console.log('Ansprechpartner:', formData.contactPerson);
|
||||
console.log('E-Mail:', formData.email);
|
||||
console.log('Telefon:', formData.phone);
|
||||
console.log('Website:', formData.website || 'Nicht angegeben');
|
||||
console.log('PLZ:', formData.zipCode);
|
||||
console.log('Stadt:', formData.city);
|
||||
console.log('Energiearten:', formData.energyTypes.join(', '));
|
||||
console.log('Leistungen:', formData.services.join(', '));
|
||||
console.log('Jahre Erfahrung:', formData.experience);
|
||||
console.log('Einzugsgebiet:', formData.coverageArea || 'Nicht angegeben');
|
||||
console.log('Kontaktpräferenz:', formData.contactPreference);
|
||||
console.log('Newsletter:', formData.newsletter ? 'Ja' : 'Nein');
|
||||
console.log('Unternehmensbeschreibung:', formData.description);
|
||||
console.log('=====================================');
|
||||
};
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* IndexNow Integration for Answer Engine Optimization (AEO)
|
||||
* Supports PerplexityBot, GPTBot, and other AI crawlers
|
||||
*/
|
||||
|
||||
interface IndexNowConfig {
|
||||
host: string;
|
||||
key: string;
|
||||
keyLocation: string;
|
||||
}
|
||||
|
||||
interface QueuedUrl {
|
||||
url: string;
|
||||
timestamp: number;
|
||||
attempts: number;
|
||||
}
|
||||
|
||||
class IndexNowClient {
|
||||
private config: IndexNowConfig;
|
||||
private queue: Map<string, QueuedUrl> = new Map();
|
||||
private isProcessing = false;
|
||||
private maxRetries = 3;
|
||||
private baseDelay = 1000; // 1 second
|
||||
|
||||
constructor() {
|
||||
const key = import.meta.env.VITE_INDEXNOW_KEY;
|
||||
const host = import.meta.env.VITE_SITE_HOST || 'energie-profis.de';
|
||||
|
||||
if (!key) {
|
||||
console.warn('VITE_INDEXNOW_KEY not found. IndexNow disabled.');
|
||||
this.config = { host: '', key: '', keyLocation: '' };
|
||||
return;
|
||||
}
|
||||
|
||||
this.config = {
|
||||
host,
|
||||
key,
|
||||
keyLocation: `https://${host}/${key}.txt`
|
||||
};
|
||||
|
||||
// Auto-process queue periodically
|
||||
setInterval(() => this.processQueue(), 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue URLs for IndexNow submission with deduplication
|
||||
*/
|
||||
queueUrls(urls: string[]): void {
|
||||
if (!this.config.key) return;
|
||||
|
||||
const validUrls = urls.filter(url => this.isValidUrl(url));
|
||||
|
||||
for (const url of validUrls) {
|
||||
const canonical = this.getCanonicalUrl(url);
|
||||
if (!this.queue.has(canonical)) {
|
||||
this.queue.set(canonical, {
|
||||
url: canonical,
|
||||
timestamp: Date.now(),
|
||||
attempts: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (validUrls.length > 0) {
|
||||
console.log(`Queued ${validUrls.length} URLs for IndexNow`);
|
||||
this.processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a single URL
|
||||
*/
|
||||
queueUrl(url: string): void {
|
||||
this.queueUrls([url]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the queue with exponential backoff retry logic
|
||||
*/
|
||||
private async processQueue(): Promise<void> {
|
||||
if (this.isProcessing || this.queue.size === 0 || !this.config.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessing = true;
|
||||
|
||||
try {
|
||||
const urlsToSubmit: QueuedUrl[] = [];
|
||||
const urlsToRetry: QueuedUrl[] = [];
|
||||
const urlsToRemove: string[] = [];
|
||||
|
||||
// Categorize URLs by their status
|
||||
for (const [key, queuedUrl] of this.queue) {
|
||||
if (queuedUrl.attempts >= this.maxRetries) {
|
||||
urlsToRemove.push(key);
|
||||
console.warn(`Removing URL after ${this.maxRetries} attempts: ${queuedUrl.url}`);
|
||||
} else if (queuedUrl.attempts === 0) {
|
||||
urlsToSubmit.push(queuedUrl);
|
||||
} else {
|
||||
// Check if enough time has passed for retry
|
||||
const delay = this.baseDelay * Math.pow(2, queuedUrl.attempts - 1);
|
||||
if (Date.now() - queuedUrl.timestamp > delay) {
|
||||
urlsToRetry.push(queuedUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove failed URLs
|
||||
urlsToRemove.forEach(key => this.queue.delete(key));
|
||||
|
||||
// Submit new URLs
|
||||
if (urlsToSubmit.length > 0) {
|
||||
const success = await this.submitToIndexNow(
|
||||
urlsToSubmit.map(u => u.url)
|
||||
);
|
||||
|
||||
if (success) {
|
||||
// Remove successful submissions
|
||||
urlsToSubmit.forEach(u => this.queue.delete(u.url));
|
||||
} else {
|
||||
// Mark for retry
|
||||
urlsToSubmit.forEach(u => {
|
||||
u.attempts++;
|
||||
u.timestamp = Date.now();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Retry failed URLs
|
||||
if (urlsToRetry.length > 0) {
|
||||
const success = await this.submitToIndexNow(
|
||||
urlsToRetry.map(u => u.url)
|
||||
);
|
||||
|
||||
if (success) {
|
||||
urlsToRetry.forEach(u => this.queue.delete(u.url));
|
||||
} else {
|
||||
urlsToRetry.forEach(u => {
|
||||
u.attempts++;
|
||||
u.timestamp = Date.now();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('IndexNow queue processing error:', error);
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit URLs to IndexNow API
|
||||
*/
|
||||
private async submitToIndexNow(urls: string[]): Promise<boolean> {
|
||||
if (urls.length === 0) return true;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
host: this.config.host,
|
||||
key: this.config.key,
|
||||
keyLocation: this.config.keyLocation,
|
||||
urlList: urls
|
||||
};
|
||||
|
||||
const response = await fetch('https://api.indexnow.org/indexnow', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log(`IndexNow: Successfully submitted ${urls.length} URLs`);
|
||||
return true;
|
||||
} else {
|
||||
console.error(`IndexNow submission failed: ${response.status} ${response.statusText}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('IndexNow submission error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate URL format and domain
|
||||
*/
|
||||
private isValidUrl(url: string): boolean {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.hostname === this.config.host ||
|
||||
urlObj.hostname === `www.${this.config.host}` ||
|
||||
(urlObj.hostname.includes('localhost') && import.meta.env.DEV);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canonical URL (removes fragments, normalizes)
|
||||
*/
|
||||
private getCanonicalUrl(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
urlObj.hash = '';
|
||||
urlObj.search = urlObj.search.replace(/[?&]utm_[^&]*/g, '');
|
||||
return urlObj.toString();
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current queue status (for debugging)
|
||||
*/
|
||||
getQueueStatus(): { size: number; urls: string[] } {
|
||||
return {
|
||||
size: this.queue.size,
|
||||
urls: Array.from(this.queue.keys())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const indexNowClient = new IndexNowClient();
|
||||
|
||||
/**
|
||||
* Convenience functions for common use cases
|
||||
*/
|
||||
|
||||
export function queueIndexNowPing(urls: string[]): void {
|
||||
indexNowClient.queueUrls(urls);
|
||||
}
|
||||
|
||||
export function queueCurrentPage(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
indexNowClient.queueUrl(window.location.href);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for React components to easily queue current page
|
||||
*/
|
||||
export function useIndexNow() {
|
||||
const queueCurrentPage = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
indexNowClient.queueUrl(window.location.href);
|
||||
}
|
||||
};
|
||||
|
||||
return { queueCurrentPage, queueUrls: queueIndexNowPing };
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual ping function for scripts
|
||||
*/
|
||||
export async function pingIndexNowManually(urls: string[]): Promise<boolean> {
|
||||
indexNowClient.queueUrls(urls);
|
||||
// Wait for processing
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const status = indexNowClient.getQueueStatus();
|
||||
return status.size === 0; // Success if queue is empty
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import posthog from 'posthog-js'
|
||||
|
||||
const POSTHOG_API_KEY = import.meta.env.VITE_POSTHOG_API_KEY || 'phc_jIkj0hQSY670vRaUVjSRSDOqmLCDGkL6GJy44iqE84M'
|
||||
|
||||
export const initPostHog = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
posthog.init(POSTHOG_API_KEY, {
|
||||
api_host: 'https://app.posthog.com',
|
||||
person_profiles: 'identified_only',
|
||||
capture_pageview: true,
|
||||
capture_pageleave: true,
|
||||
loaded: (posthog) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('PostHog loaded successfully')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export { posthog }
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
/**
|
||||
* Schema.org JSON-LD builders for Answer Engine Optimization (AEO)
|
||||
* Optimized for AI answer engines like ChatGPT and Perplexity
|
||||
*/
|
||||
|
||||
interface Organization {
|
||||
name: string;
|
||||
description?: string;
|
||||
url: string;
|
||||
logo: string;
|
||||
contactEmail?: string;
|
||||
socialProfiles?: string[];
|
||||
}
|
||||
|
||||
interface Person {
|
||||
name: string;
|
||||
url?: string;
|
||||
jobTitle?: string;
|
||||
organization?: string;
|
||||
}
|
||||
|
||||
interface Article {
|
||||
headline: string;
|
||||
description: string;
|
||||
datePublished: Date;
|
||||
dateModified: Date;
|
||||
author: Person;
|
||||
publisher: Organization;
|
||||
images: string[];
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface FAQ {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface HowToStep {
|
||||
name: string;
|
||||
text: string;
|
||||
image?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface HowTo {
|
||||
name: string;
|
||||
description: string;
|
||||
steps: HowToStep[];
|
||||
totalTime?: string;
|
||||
estimatedCost?: string;
|
||||
supply?: string[];
|
||||
tool?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Organization schema for the main company
|
||||
*/
|
||||
export function buildOrganizationJsonLd(org: Partial<Organization> = {}): Record<string, any> {
|
||||
const defaultOrg: Organization = {
|
||||
name: 'EnergieProfis',
|
||||
description: 'Führendes Verzeichnis für Solar- und Wind-Installateure in Deutschland',
|
||||
url: 'https://energie-profis.de',
|
||||
logo: 'https://energie-profis.de/favicon.ico',
|
||||
contactEmail: 'info@energie-profis.de',
|
||||
socialProfiles: [
|
||||
'https://www.linkedin.com/company/energieprofis',
|
||||
'https://twitter.com/energieprofis'
|
||||
]
|
||||
};
|
||||
|
||||
const organization = { ...defaultOrg, ...org };
|
||||
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: organization.name,
|
||||
description: organization.description,
|
||||
url: organization.url,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: organization.logo
|
||||
},
|
||||
contactPoint: {
|
||||
'@type': 'ContactPoint',
|
||||
email: organization.contactEmail,
|
||||
contactType: 'customer service',
|
||||
areaServed: 'DE',
|
||||
availableLanguage: 'German'
|
||||
},
|
||||
sameAs: organization.socialProfiles
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build WebSite schema with search action
|
||||
*/
|
||||
export function buildWebSiteJsonLd(baseUrl: string = 'https://energie-profis.de'): Record<string, any> {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: 'EnergieProfis',
|
||||
description: 'Finden Sie qualifizierte Fachbetriebe für Solar- und Windenergie in Deutschland',
|
||||
url: baseUrl,
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: `${baseUrl}/installateur-finden?search={search_term_string}&type={energy_type}&location={location}`
|
||||
},
|
||||
'query-input': 'required name=search_term_string name=energy_type name=location'
|
||||
},
|
||||
publisher: buildOrganizationJsonLd()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Article schema for content pages
|
||||
*/
|
||||
export function buildArticleJsonLd(article: Article): Record<string, any> {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: article.headline,
|
||||
description: article.description,
|
||||
image: article.images,
|
||||
datePublished: article.datePublished.toISOString(),
|
||||
dateModified: article.dateModified.toISOString(),
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: article.author.name,
|
||||
url: article.author.url,
|
||||
jobTitle: article.author.jobTitle
|
||||
},
|
||||
publisher: buildOrganizationJsonLd(),
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': article.url
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build FAQPage schema for Q&A sections
|
||||
*/
|
||||
export function buildFAQPageJsonLd(faqs: FAQ[], pageUrl?: string): Record<string, any> {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.map(faq => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: faq.answer
|
||||
}
|
||||
})),
|
||||
...(pageUrl && {
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': pageUrl
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build HowTo schema for step-by-step guides
|
||||
*/
|
||||
export function buildHowToJsonLd(howTo: HowTo, pageUrl?: string): Record<string, any> {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'HowTo',
|
||||
name: howTo.name,
|
||||
description: howTo.description,
|
||||
step: howTo.steps.map((step, index) => ({
|
||||
'@type': 'HowToStep',
|
||||
position: index + 1,
|
||||
name: step.name,
|
||||
text: step.text,
|
||||
...(step.image && {
|
||||
image: {
|
||||
'@type': 'ImageObject',
|
||||
url: step.image
|
||||
}
|
||||
}),
|
||||
...(step.url && { url: step.url })
|
||||
})),
|
||||
...(howTo.totalTime && { totalTime: howTo.totalTime }),
|
||||
...(howTo.estimatedCost && {
|
||||
estimatedCost: {
|
||||
'@type': 'MonetaryAmount',
|
||||
currency: 'EUR',
|
||||
value: howTo.estimatedCost
|
||||
}
|
||||
}),
|
||||
...(howTo.supply && {
|
||||
supply: howTo.supply.map(item => ({
|
||||
'@type': 'HowToSupply',
|
||||
name: item
|
||||
}))
|
||||
}),
|
||||
...(howTo.tool && {
|
||||
tool: howTo.tool.map(item => ({
|
||||
'@type': 'HowToTool',
|
||||
name: item
|
||||
}))
|
||||
}),
|
||||
...(pageUrl && {
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': pageUrl
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build BreadcrumbList schema for navigation
|
||||
*/
|
||||
export function buildBreadcrumbJsonLd(
|
||||
breadcrumbs: Array<{ name: string; url: string }>
|
||||
): Record<string, any> {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: breadcrumbs.map((crumb, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: crumb.name,
|
||||
item: crumb.url
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Example usage for renewable energy content
|
||||
*/
|
||||
|
||||
export const exampleFAQs: FAQ[] = [
|
||||
{
|
||||
question: 'Wie finde ich den besten Installateur in meiner Region?',
|
||||
answer: 'Nutzen Sie unseren Vergleich: Bewertungen, Erfahrung, Zertifizierungen und regionale Expertise vergleichen. Lokale Installateure kennen die regionalen Vorgaben und Fördermöglichkeiten am besten.'
|
||||
},
|
||||
{
|
||||
question: 'Welche Förderungen gibt es 2025 für erneuerbare Energien?',
|
||||
answer: 'BAFA-Förderung für Balkonkraftwerke bis 500€, KfW-Kredite mit Tilgungszuschuss, EEG-Vergütung für Überschussstrom und kommunale Zuschüsse je nach Bundesland.'
|
||||
},
|
||||
{
|
||||
question: 'Was kostet eine Solar- oder Windanlage?',
|
||||
answer: 'Balkonkraftwerk: 400-800€, komplette Dachanlage: 8.000-15.000€ je kWp, Kleinwindanlage: 3.000-15.000€. Preise variieren je nach Region und Ausstattung.'
|
||||
}
|
||||
];
|
||||
|
||||
export const exampleHowTo: HowTo = {
|
||||
name: 'Solaranlage installieren lassen - Schritt für Schritt',
|
||||
description: 'So finden Sie den richtigen Installateur und lassen Ihre Solaranlage professionell installieren.',
|
||||
steps: [
|
||||
{
|
||||
name: 'Energiebedarf ermitteln',
|
||||
text: 'Berechnen Sie Ihren jährlichen Stromverbrauch und prüfen Sie die Dachfläche auf Eignung für Solarmodule.'
|
||||
},
|
||||
{
|
||||
name: 'Qualifizierte Installateure vergleichen',
|
||||
text: 'Nutzen Sie EnergieProfis.de um zertifizierte Fachbetriebe in Ihrer Region zu finden und Angebote einzuholen.'
|
||||
},
|
||||
{
|
||||
name: 'Angebote prüfen und Finanzierung klären',
|
||||
text: 'Vergleichen Sie Preise, Garantieleistungen und prüfen Sie verfügbare Fördermittel und Finanzierungsoptionen.'
|
||||
},
|
||||
{
|
||||
name: 'Installation beauftragen und überwachen',
|
||||
text: 'Beauftragen Sie den gewählten Installateur und lassen Sie sich über den Installationsfortschritt informieren.'
|
||||
}
|
||||
],
|
||||
totalTime: 'P2W',
|
||||
estimatedCost: '8000-15000',
|
||||
supply: ['Solarmodule', 'Wechselrichter', 'Montagesystem', 'Verkabelung'],
|
||||
tool: ['Nicht erforderlich - wird vom Installateur bereitgestellt']
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// Re-export the comprehensive seed data
|
||||
export { seedComprehensiveDatabase as seedDatabase, comprehensiveInstallers as sampleInstallers } from './comprehensiveSeedData';
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
/**
|
||||
* Sitemap generator for Vite React app with proper lastmod for AEO
|
||||
*/
|
||||
|
||||
interface SitemapUrl {
|
||||
loc: string;
|
||||
lastmod: string;
|
||||
changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
interface PageMetadata {
|
||||
path: string;
|
||||
lastModified?: Date;
|
||||
changeFreq?: SitemapUrl['changefreq'];
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
class SitemapGenerator {
|
||||
private readonly baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string = 'https://energie-profis.de') {
|
||||
this.baseUrl = baseUrl.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate complete sitemap XML
|
||||
*/
|
||||
generateSitemap(): string {
|
||||
const urls = this.getStaticPages();
|
||||
|
||||
const urlElements = urls.map(url => `
|
||||
<url>
|
||||
<loc>${this.escapeXml(url.loc)}</loc>
|
||||
<lastmod>${url.lastmod}</lastmod>
|
||||
${url.changefreq ? `<changefreq>${url.changefreq}</changefreq>` : ''}
|
||||
${url.priority ? `<priority>${url.priority}</priority>` : ''}
|
||||
</url>`).join('');
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${urlElements}
|
||||
</urlset>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate news sitemap for recent content updates
|
||||
*/
|
||||
generateNewsSitemap(): string {
|
||||
const recentUrls = this.getStaticPages()
|
||||
.filter(url => {
|
||||
const lastMod = new Date(url.lastmod);
|
||||
const threeDaysAgo = new Date();
|
||||
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
|
||||
return lastMod > threeDaysAgo;
|
||||
});
|
||||
|
||||
if (recentUrls.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const newsItems = recentUrls.map(url => `
|
||||
<url>
|
||||
<loc>${this.escapeXml(url.loc)}</loc>
|
||||
<news:news>
|
||||
<news:publication>
|
||||
<news:name>EnergieProfis</news:name>
|
||||
<news:language>de</news:language>
|
||||
</news:publication>
|
||||
<news:publication_date>${url.lastmod}</news:publication_date>
|
||||
<news:title>${this.getPageTitle(url.loc)}</news:title>
|
||||
</news:news>
|
||||
</url>`).join('');
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
|
||||
${newsItems}
|
||||
</urlset>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get static page metadata with realistic last modified dates
|
||||
*/
|
||||
private getStaticPages(): SitemapUrl[] {
|
||||
const now = new Date();
|
||||
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const pages: PageMetadata[] = [
|
||||
{
|
||||
path: '/',
|
||||
lastModified: oneWeekAgo,
|
||||
changeFreq: 'weekly',
|
||||
priority: 1.0
|
||||
},
|
||||
{
|
||||
path: '/solar',
|
||||
lastModified: oneWeekAgo,
|
||||
changeFreq: 'weekly',
|
||||
priority: 0.9
|
||||
},
|
||||
{
|
||||
path: '/wind',
|
||||
lastModified: oneWeekAgo,
|
||||
changeFreq: 'weekly',
|
||||
priority: 0.9
|
||||
},
|
||||
{
|
||||
path: '/installateur-finden',
|
||||
lastModified: new Date(), // Updated frequently with new installers
|
||||
changeFreq: 'daily',
|
||||
priority: 0.8
|
||||
},
|
||||
{
|
||||
path: '/kostenlose-beratung',
|
||||
lastModified: oneMonthAgo,
|
||||
changeFreq: 'monthly',
|
||||
priority: 0.7
|
||||
},
|
||||
{
|
||||
path: '/unternehmen-listen',
|
||||
lastModified: new Date(), // Business listings updated daily
|
||||
changeFreq: 'daily',
|
||||
priority: 0.6
|
||||
}
|
||||
];
|
||||
|
||||
return pages.map(page => ({
|
||||
loc: `${this.baseUrl}${page.path}`,
|
||||
lastmod: (page.lastModified || now).toISOString().split('T')[0],
|
||||
changefreq: page.changeFreq,
|
||||
priority: page.priority
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page title for news sitemap
|
||||
*/
|
||||
private getPageTitle(url: string): string {
|
||||
const path = url.replace(this.baseUrl, '');
|
||||
const titles: Record<string, string> = {
|
||||
'/': 'Solar- und Wind-Installateure finden & vergleichen',
|
||||
'/solar': 'Solarenergie: Photovoltaik-Anlagen und Solarthermie',
|
||||
'/wind': 'Windenergie: Kleinwind- und Windkraftanlagen',
|
||||
'/installateur-finden': 'Qualifizierte Installateure in Ihrer Region finden',
|
||||
'/kostenlose-beratung': 'Kostenlose Beratung für erneuerbare Energien',
|
||||
'/unternehmen-listen': 'Ihr Unternehmen bei EnergieProfis listen'
|
||||
};
|
||||
return titles[path] || 'EnergieProfis - Erneuerbare Energien';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape XML special characters
|
||||
*/
|
||||
private escapeXml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const sitemapGenerator = new SitemapGenerator();
|
||||
|
||||
/**
|
||||
* Generate and serve sitemap for development/build
|
||||
*/
|
||||
export function generateSitemapXml(): string {
|
||||
return sitemapGenerator.generateSitemap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate news sitemap if there are recent updates
|
||||
*/
|
||||
export function generateNewsSitemapXml(): string {
|
||||
return sitemapGenerator.generateNewsSitemap();
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import { initPostHog } from './lib/posthog'
|
||||
|
||||
// Initialize PostHog
|
||||
initPostHog()
|
||||
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
const CookieEinstellungen = () => {
|
||||
const [necessaryCookies, setNecessaryCookies] = useState(true); // Always true, can't be disabled
|
||||
const [analyticsCookies, setAnalyticsCookies] = useState(false);
|
||||
const [marketingCookies, setMarketingCookies] = useState(false);
|
||||
|
||||
const handleSavePreferences = () => {
|
||||
// Save cookie preferences to localStorage
|
||||
localStorage.setItem('cookiePreferences', JSON.stringify({
|
||||
necessary: necessaryCookies,
|
||||
analytics: analyticsCookies,
|
||||
marketing: marketingCookies
|
||||
}));
|
||||
|
||||
// Show success message
|
||||
alert('Ihre Cookie-Einstellungen wurden gespeichert!');
|
||||
};
|
||||
|
||||
const handleAcceptAll = () => {
|
||||
setAnalyticsCookies(true);
|
||||
setMarketingCookies(true);
|
||||
localStorage.setItem('cookiePreferences', JSON.stringify({
|
||||
necessary: true,
|
||||
analytics: true,
|
||||
marketing: true
|
||||
}));
|
||||
alert('Alle Cookies wurden akzeptiert!');
|
||||
};
|
||||
|
||||
const handleRejectAll = () => {
|
||||
setAnalyticsCookies(false);
|
||||
setMarketingCookies(false);
|
||||
localStorage.setItem('cookiePreferences', JSON.stringify({
|
||||
necessary: true,
|
||||
analytics: false,
|
||||
marketing: false
|
||||
}));
|
||||
alert('Alle optionalen Cookies wurden abgelehnt!');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 py-16 mt-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-4xl font-bold text-foreground mb-8">Cookie-Einstellungen</h1>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Was sind Cookies?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground">
|
||||
Cookies sind kleine Textdateien, die auf Ihrem Gerät gespeichert werden, wenn Sie unsere Website besuchen.
|
||||
Sie helfen uns dabei, Ihre Präferenzen zu speichern und die Website für Sie zu verbessern.
|
||||
Wir verwenden verschiedene Arten von Cookies für unterschiedliche Zwecke.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Cookie-Kategorien</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Necessary Cookies */}
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-lg mb-2">Notwendige Cookies</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Diese Cookies sind für das Funktionieren der Website unerlässlich. Sie können nicht deaktiviert werden.
|
||||
Sie werden normalerweise nur als Reaktion auf Aktionen gesetzt, die Sie ausführen und die einer Anfrage nach Diensten entsprechen.
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={necessaryCookies} disabled />
|
||||
</div>
|
||||
|
||||
{/* Analytics Cookies */}
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-lg mb-2">Analyse-Cookies</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Diese Cookies ermöglichen es uns, die Anzahl der Besucher zu zählen und zu verstehen, wie Besucher mit unserer Website interagieren.
|
||||
Alle Informationen, die diese Cookies sammeln, werden aggregiert und sind daher anonym.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={analyticsCookies}
|
||||
onCheckedChange={setAnalyticsCookies}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Marketing Cookies */}
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-lg mb-2">Marketing-Cookies</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Diese Cookies werden verwendet, um Besuchern auf Webseiten zu folgen. Die Absicht ist, Anzeigen zu zeigen,
|
||||
die relevant und ansprechend für den einzelnen Benutzer sind und daher wertvoller für Publisher und Drittanbieter sind.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={marketingCookies}
|
||||
onCheckedChange={setMarketingCookies}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Detaillierte Cookie-Informationen</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Notwendige Cookies</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
<span className="font-medium">session_id</span>
|
||||
<span className="text-muted-foreground">Speichert Ihre Session-ID</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
<span className="font-medium">cookie_preferences</span>
|
||||
<span className="text-muted-foreground">Speichert Ihre Cookie-Einstellungen</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Analyse-Cookies</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
<span className="font-medium">_ga</span>
|
||||
<span className="text-muted-foreground">Google Analytics - Unterscheidet Benutzer</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
<span className="font-medium">_gid</span>
|
||||
<span className="text-muted-foreground">Google Analytics - Unterscheidet Benutzer</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Marketing-Cookies</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
||||
<span className="font-medium">_fbp</span>
|
||||
<span className="text-muted-foreground">Facebook Pixel - Verfolgt Besucher</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Ihre Rechte</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Sie haben das Recht, Ihre Cookie-Einstellungen jederzeit zu ändern. Sie können auch Cookies,
|
||||
die bereits auf Ihrem Gerät gespeichert sind, über Ihren Browser löschen.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p><strong>Chrome:</strong> Einstellungen → Erweitert → Datenschutz und Sicherheit → Cookies und andere Websitedaten</p>
|
||||
<p><strong>Firefox:</strong> Einstellungen → Datenschutz & Sicherheit → Cookies und Website-Daten</p>
|
||||
<p><strong>Safari:</strong> Einstellungen → Datenschutz → Cookies und Website-Daten verwalten</p>
|
||||
<p><strong>Edge:</strong> Einstellungen → Cookies und Websiteberechtigungen → Cookies und gespeicherte Daten</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
onClick={handleSavePreferences}
|
||||
className="bg-primary hover:bg-primary/90"
|
||||
>
|
||||
Einstellungen speichern
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAcceptAll}
|
||||
variant="outline"
|
||||
>
|
||||
Alle akzeptieren
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleRejectAll}
|
||||
variant="outline"
|
||||
>
|
||||
Alle ablehnen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CookieEinstellungen;
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
const Datenschutz = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 py-16 mt-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-4xl font-bold text-foreground mb-8">Datenschutzerklärung</h1>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>1. Datenschutz auf einen Blick</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Allgemeine Hinweise</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können. Ausführliche Informationen zum Thema Datenschutz entnehmen Sie unserer unter diesem Text aufgeführten Datenschutzerklärung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Datenerfassung auf dieser Website</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten können Sie dem Abschnitt „Hinweis zur Verantwortlichen Stelle" in dieser Datenschutzerklärung entnehmen.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>2. Allgemeine Hinweise und Pflichtinformationen</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Datenschutz</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Hinweis zur verantwortlichen Stelle</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist der Betreiber dieser Website.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Speicherdauer</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Soweit innerhalb dieser Datenschutzerklärung keine speziellere Speicherdauer genannt wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck für die Datenverarbeitung entfällt. Wenn Sie ein berechtigtes Löschersuchen geltend machen oder eine Einwilligung zur Datenverarbeitung widerrufen, werden Ihre Daten gelöscht, sofern wir keine anderen rechtlich zulässigen Gründe für die Speicherung Ihrer personenbezogenen Daten haben.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>3. Datenerfassung auf dieser Website</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Server-Log-Dateien</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Der Provider der Seiten erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-muted-foreground mt-2 space-y-1">
|
||||
<li>Browsertyp und Browserversion</li>
|
||||
<li>verwendetes Betriebssystem</li>
|
||||
<li>Referrer URL</li>
|
||||
<li>Hostname des zugreifenden Rechners</li>
|
||||
<li>Uhrzeit der Serveranfrage</li>
|
||||
<li>IP-Adresse</li>
|
||||
</ul>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen. Die Erfassung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Kontaktformular</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Wenn Sie uns per Kontaktformular Anfragen zukommen lassen, werden Ihre Angaben aus dem Anfrageformular inklusive der von Ihnen dort angegebenen Kontaktdaten zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen bei uns gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung weiter.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>4. Analyse-Tools und Tools von Drittanbietern</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Google Analytics</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Diese Website nutzt Funktionen des Webanalysedienstes Google Analytics. Anbieter ist die Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland.
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Google Analytics verwendet so genannte "Cookies". Das sind Textdateien, die auf Ihrem Computer gespeichert werden und die eine Analyse der Benutzung der Website durch Sie ermöglichen. Die durch das Cookie erzeugten Informationen über Ihre Benutzung dieser Website werden in der Regel an einen Server von Google in den USA übertragen und dort gespeichert.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>5. Ihre Rechte</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Widerruf Ihrer Einwilligung zur Datenverarbeitung</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Viele Datenverarbeitungsvorgänge sind nur mit Ihrer ausdrücklichen Einwilligung möglich. Sie können eine bereits erteilte Einwilligung jederzeit widerrufen. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom Widerruf unberührt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Recht auf Datenübertragbarkeit</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Sie haben das Recht, Daten, die wir auf Grundlage Ihrer Einwilligung oder in Erfüllung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in einem gängigen, maschinenlesbaren Format aushändigen zu lassen. Sofern Sie die direkte Übertragung der Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch machbar ist.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">SSL- bzw. TLS-Verschlüsselung</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Diese Seite nutzt aus Sicherheitsgründen und zum Schutz der Übertragung vertraulicher Inhalte, wie zum Beispiel Bestellungen oder Anfragen, die Sie an uns als Seitenbetreiber senden, eine SSL- bzw. TLS-Verschlüsselung. Eine verschlüsselte Verbindung erkennen Sie daran, dass die Adresszeile des Browsers von "http://" auf "https://" wechselt und an dem Schloss-Symbol in Ihrer Browserzeile.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>6. Kontakt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground">
|
||||
Bei Fragen zum Datenschutz können Sie uns über die auf dieser Website verfügbaren Kontaktmöglichkeiten erreichen.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Datenschutz;
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
const Impressum = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 py-16 mt-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-4xl font-bold text-foreground mb-8">Impressum</h1>
|
||||
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Angaben gemäß § 5 TMG</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Betreiber dieser Website</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Diese Website wird von einem privaten Betreiber geführt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Haftungsausschluss</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Die Inhalte dieser Website dienen der allgemeinen Information. Wir übernehmen keine Gewähr für die Vollständigkeit, Richtigkeit oder Aktualität der bereitgestellten Informationen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2">Urheberrecht</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Die Inhalte dieser Website unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Impressum;
|
||||
|
|
@ -1,17 +1,331 @@
|
|||
import { useEffect } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import HeroSection from "@/components/HeroSection";
|
||||
import EnergyTypesSection from "@/components/EnergyTypesSection";
|
||||
import CalculatorNavigation from "@/components/CalculatorNavigation";
|
||||
import WhyChooseUsSection from "@/components/WhyChooseUsSection";
|
||||
import Footer from "@/components/Footer";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calculator, Award, Shield, TrendingUp, Map, Users, CheckCircle, ArrowRight, Search, MapPin, Zap } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const Index = () => {
|
||||
// SEO: Update page title and meta description
|
||||
useEffect(() => {
|
||||
const title = "EnergieProfis - Solar- und Wind-Installateure finden & vergleichen";
|
||||
const description = "Finden Sie qualifizierte Fachbetriebe für erneuerbare Energien in Ihrer Region. Solar- und Wind-Installateure vergleichen, Angebote einholen & Kosten sparen. Kostenlos & unverbindlich.";
|
||||
|
||||
document.title = title;
|
||||
|
||||
// Update or create meta description
|
||||
let metaDesc = document.querySelector('meta[name="description"]');
|
||||
if (!metaDesc) {
|
||||
metaDesc = document.createElement('meta');
|
||||
metaDesc.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDesc);
|
||||
}
|
||||
metaDesc.setAttribute('content', description);
|
||||
|
||||
// Add canonical URL
|
||||
let canonical = document.querySelector('link[rel="canonical"]');
|
||||
if (!canonical) {
|
||||
canonical = document.createElement('link');
|
||||
canonical.setAttribute('rel', 'canonical');
|
||||
document.head.appendChild(canonical);
|
||||
}
|
||||
canonical.setAttribute('href', window.location.href);
|
||||
|
||||
// Add Open Graph tags
|
||||
const ogTags = [
|
||||
{ property: 'og:title', content: title },
|
||||
{ property: 'og:description', content: description },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:url', content: window.location.href },
|
||||
{ property: 'og:site_name', content: 'EnergieProfis' },
|
||||
{ property: 'og:image', content: `${window.location.origin}/sun_flow_banner.png` }
|
||||
];
|
||||
|
||||
ogTags.forEach(tag => {
|
||||
let meta = document.querySelector(`meta[property="${tag.property}"]`);
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.setAttribute('property', tag.property);
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.setAttribute('content', tag.content);
|
||||
});
|
||||
|
||||
// Add Twitter Card tags
|
||||
const twitterTags = [
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:title', content: title },
|
||||
{ name: 'twitter:description', content: description },
|
||||
{ name: 'twitter:image', content: `${window.location.origin}/sun_flow_banner.png` }
|
||||
];
|
||||
|
||||
twitterTags.forEach(tag => {
|
||||
let meta = document.querySelector(`meta[name="${tag.name}"]`);
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.setAttribute('name', tag.name);
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.setAttribute('content', tag.content);
|
||||
});
|
||||
|
||||
// Add structured data for the homepage
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "EnergieProfis",
|
||||
"description": "Führendes Verzeichnis für Solar- und Wind-Installateure in Deutschland",
|
||||
"url": window.location.origin,
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": {
|
||||
"@type": "EntryPoint",
|
||||
"urlTemplate": `${window.location.origin}/installateur-finden?search={search_term_string}&type={energy_type}&location={location}`
|
||||
},
|
||||
"query-input": "required name=search_term_string name=energy_type name=location"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "EnergieProfis",
|
||||
"description": "Fachverzeichnis für erneuerbare Energien",
|
||||
"url": window.location.origin,
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": `${window.location.origin}/favicon.ico`
|
||||
}
|
||||
},
|
||||
"mainEntity": {
|
||||
"@type": "ItemList",
|
||||
"name": "Solar- und Wind-Installateure",
|
||||
"description": "Qualifizierte Fachbetriebe für erneuerbare Energien",
|
||||
"numberOfItems": 500,
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"item": {
|
||||
"@type": "Service",
|
||||
"name": "Solar-Installation",
|
||||
"description": "Photovoltaik-Anlagen, Solarthermie und komplette Energiesysteme",
|
||||
"url": `${window.location.origin}/solar`
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"item": {
|
||||
"@type": "Service",
|
||||
"name": "Wind-Installation",
|
||||
"description": "Kleinwindanlagen und Windkraftanlagen für private und gewerbliche Nutzung",
|
||||
"url": `${window.location.origin}/wind`
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 3,
|
||||
"item": {
|
||||
"@type": "Service",
|
||||
"name": "Installateur-Vergleich",
|
||||
"description": "Fachbetriebe finden, Angebote vergleichen und Kosten sparen",
|
||||
"url": `${window.location.origin}/installateur-finden`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Insert structured data into the page
|
||||
const script = document.createElement('script');
|
||||
script.type = 'application/ld+json';
|
||||
script.text = JSON.stringify(structuredData);
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (script.parentNode) script.parentNode.removeChild(script);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
<main>
|
||||
<HeroSection />
|
||||
|
||||
{/* SEO: Rich content section for better indexing */}
|
||||
<section className="py-16 bg-gradient-to-b from-background to-secondary/10">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12 max-w-4xl mx-auto">
|
||||
{/* SEO Content Block 1: Installateur finden */}
|
||||
<Card className="border-l-4 border-l-orange-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-orange-700">
|
||||
<Search className="w-5 h-5" />
|
||||
Installateur finden
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Qualifizierte Fachbetriebe für Solar- und Windenergie in Ihrer Region finden.
|
||||
Kostenlos vergleichen und Angebote einholen.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>500+ verifizierte Installateure</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Kostenlose Angebote</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Direkter Vergleich</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SEO Content Block 2: Regionale Expertise */}
|
||||
<Card className="border-l-4 border-l-blue-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-blue-700">
|
||||
<Map className="w-5 h-5" />
|
||||
Regionale Expertise
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Lokale Installateure kennen die regionalen Gegebenheiten, Förderprogramme
|
||||
und Genehmigungsverfahren in Ihrem Bundesland.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Lokale Genehmigungsrichtlinien</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Regionale Preisvergleiche</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Persönliche Beratung vor Ort</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<EnergyTypesSection />
|
||||
<CalculatorNavigation />
|
||||
<WhyChooseUsSection />
|
||||
|
||||
{/* SEO: FAQ Section for better content depth */}
|
||||
<section className="py-16 bg-gray-50">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Häufige Fragen zu erneuerbaren Energien</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Wie finde ich den besten Installateur in meiner Region?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Nutzen Sie unseren Vergleich: Bewertungen, Erfahrung, Zertifizierungen und
|
||||
regionale Expertise vergleichen. Lokale Installateure kennen die regionalen Vorgaben.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Was kostet eine Solar- oder Windanlage?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Balkonkraftwerk: 400-800€, Komplette Dachanlage: 8.000-15.000€ je kWp,
|
||||
Kleinwindanlage: 3.000-15.000€. Preise variieren je nach Region und Ausstattung.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Brauche ich eine Genehmigung?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Solaranlagen sind meist genehmigungsfrei. Windanlagen unter 15m Mast sind in den
|
||||
meisten Bundesländern genehmigungsfrei. Ab 15m ist ein Bauantrag erforderlich.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SEO: Internal linking section */}
|
||||
<section className="py-16 bg-background">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Weitere nützliche Tools</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<Card className="text-center p-8 hover:shadow-lg transition-shadow">
|
||||
<Calculator className="w-16 h-16 mx-auto mb-4 text-orange-600" />
|
||||
<h3 className="text-xl font-semibold mb-2">Solar-Einsparungsrechner</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Berechnen Sie Ihre Solareinsparungen und vergleichen Sie Angebote
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link
|
||||
to="/solar"
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
const calculatorElement = document.querySelector('[data-calculator]') ||
|
||||
document.querySelector('.calculator') ||
|
||||
document.querySelector('h2');
|
||||
if (calculatorElement) {
|
||||
calculatorElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
Zum Solarrechner
|
||||
</Link>
|
||||
</Button>
|
||||
</Card>
|
||||
<Card className="text-center p-8 hover:shadow-lg transition-shadow">
|
||||
<TrendingUp className="w-16 h-16 mx-auto mb-4 text-blue-600" />
|
||||
<h3 className="text-xl font-semibold mb-2">Windenergie-Rechner</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Windenergie berechnen: Ertrag, Kosten & Angebote vergleichen
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link
|
||||
to="/wind"
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
const calculatorElement = document.querySelector('[data-calculator]') ||
|
||||
document.querySelector('.calculator') ||
|
||||
document.querySelector('h2');
|
||||
if (calculatorElement) {
|
||||
calculatorElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
Zum Windrechner
|
||||
</Link>
|
||||
</Button>
|
||||
</Card>
|
||||
<Card className="text-center p-8 hover:shadow-lg transition-shadow">
|
||||
<Search className="w-16 h-16 mx-auto mb-4 text-green-600" />
|
||||
<h3 className="text-xl font-semibold mb-2">Installateur finden</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Qualifizierte Fachbetriebe in Ihrer Region finden und vergleichen
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link to="/installateur-finden">Installateure suchen</Link>
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,901 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useSearchParams, Link } from "react-router-dom";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Search, MapPin, Star, Phone, Mail, Globe, Filter, AlertCircle, Calculator, Award, Shield, TrendingUp, Map, Users, Clock, CheckCircle } from "lucide-react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import { installerService, analyticsService } from "@/lib/database";
|
||||
import { cleanAndReseedDatabase } from "@/lib/cleanDatabase";
|
||||
import { debugDatabase, forceDeleteAll, testConnection } from "@/lib/debugDatabase";
|
||||
import type { Database } from "@/integrations/supabase/types";
|
||||
|
||||
type Installer = Database['public']['Tables']['installers']['Row'];
|
||||
|
||||
const InstallateurFinden = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [installers, setInstallers] = useState<Installer[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState(searchParams.get("search") || "");
|
||||
const [energyType, setEnergyType] = useState(searchParams.get("type") || "all");
|
||||
const [bundesland, setBundesland] = useState(searchParams.get("bundesland") || "all");
|
||||
|
||||
// Pagination state
|
||||
const [displayedInstallers, setDisplayedInstallers] = useState<Installer[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [totalInstallers, setTotalInstallers] = useState(0);
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
// SEO: Add structured data for the page
|
||||
useEffect(() => {
|
||||
// Add structured data for the installer directory
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ItemList",
|
||||
"name": "Solar- und Wind-Installateure in Deutschland",
|
||||
"description": "Qualifizierte Fachbetriebe für Photovoltaik und Windenergie finden und vergleichen",
|
||||
"url": window.location.href,
|
||||
"numberOfItems": displayedInstallers.length,
|
||||
"itemListElement": displayedInstallers.map((installer, index) => ({
|
||||
"@type": "ListItem",
|
||||
"position": index + 1,
|
||||
"item": {
|
||||
"@type": "LocalBusiness",
|
||||
"name": installer.name,
|
||||
"description": installer.description,
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": installer.location
|
||||
},
|
||||
"telephone": installer.phone,
|
||||
"email": installer.email,
|
||||
"url": installer.website ? `https://${installer.website}` : undefined,
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": installer.rating,
|
||||
"reviewCount": installer.review_count || 0
|
||||
},
|
||||
"hasOfferCatalog": {
|
||||
"@type": "OfferCatalog",
|
||||
"name": `${installer.energy_type === 'solar' ? 'Solar' : 'Wind'}-Installation`,
|
||||
"itemListElement": installer.specialties?.map(specialty => ({
|
||||
"@type": "Offer",
|
||||
"itemOffered": {
|
||||
"@type": "Service",
|
||||
"name": specialty
|
||||
}
|
||||
})) || []
|
||||
}
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
// Add breadcrumb structured data
|
||||
const breadcrumbData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Startseite",
|
||||
"item": window.location.origin
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Installateur finden",
|
||||
"item": window.location.href
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Insert structured data into the page
|
||||
const script1 = document.createElement('script');
|
||||
script1.type = 'application/ld+json';
|
||||
script1.text = JSON.stringify(structuredData);
|
||||
document.head.appendChild(script1);
|
||||
|
||||
const script2 = document.createElement('script');
|
||||
script2.type = 'application/ld+json';
|
||||
script2.text = JSON.stringify(breadcrumbData);
|
||||
document.head.appendChild(script2);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (script1.parentNode) script1.parentNode.removeChild(script1);
|
||||
if (script2.parentNode) script2.parentNode.removeChild(script2);
|
||||
};
|
||||
}, [installers]);
|
||||
|
||||
// SEO: Update page title and meta description
|
||||
useEffect(() => {
|
||||
const title = "Solar- und Wind-Installateur finden – EnergieProfis Vergleich";
|
||||
const description = "Qualifizierte Solar- und Windbetriebe direkt vergleichen. Jetzt Anfrage starten! Fachbetriebe finden, Angebote vergleichen & Kosten sparen – Echtzeit-Vergleich online.";
|
||||
|
||||
document.title = title;
|
||||
|
||||
// Update or create meta description
|
||||
let metaDesc = document.querySelector('meta[name="description"]');
|
||||
if (!metaDesc) {
|
||||
metaDesc = document.createElement('meta');
|
||||
metaDesc.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDesc);
|
||||
}
|
||||
metaDesc.setAttribute('content', description);
|
||||
|
||||
// Add canonical URL
|
||||
let canonical = document.querySelector('link[rel="canonical"]');
|
||||
if (!canonical) {
|
||||
canonical = document.createElement('link');
|
||||
canonical.setAttribute('rel', 'canonical');
|
||||
document.head.appendChild(canonical);
|
||||
}
|
||||
canonical.setAttribute('href', window.location.href);
|
||||
|
||||
// Add Open Graph tags
|
||||
const ogTags = [
|
||||
{ property: 'og:title', content: title },
|
||||
{ property: 'og:description', content: description },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:url', content: window.location.href },
|
||||
{ property: 'og:site_name', content: 'EnergieProfis' }
|
||||
];
|
||||
|
||||
ogTags.forEach(tag => {
|
||||
let meta = document.querySelector(`meta[property="${tag.property}"]`);
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.setAttribute('property', tag.property);
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.setAttribute('content', tag.content);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Load installers from database
|
||||
const loadInstallers = async (resetPagination = true) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
if (resetPagination) {
|
||||
setCurrentPage(0);
|
||||
setShowAll(false);
|
||||
}
|
||||
|
||||
const filters = {
|
||||
energyType: energyType && energyType !== "all" ? energyType : undefined,
|
||||
bundesland: bundesland && bundesland !== "all" ? bundesland : undefined,
|
||||
searchTerm: searchTerm || undefined,
|
||||
limit: showAll ? undefined : 10,
|
||||
offset: showAll ? undefined : (currentPage * 10)
|
||||
};
|
||||
|
||||
const result = await installerService.getInstallers(filters);
|
||||
const data = result.data;
|
||||
const totalCount = result.totalCount;
|
||||
|
||||
// If no installers found, automatically seed the database
|
||||
if (!data || data.length === 0) {
|
||||
console.log('No installers found, seeding database...');
|
||||
try {
|
||||
await cleanAndReseedDatabase();
|
||||
// Try to load installers again after seeding
|
||||
const reseededResult = await installerService.getInstallers(filters);
|
||||
if (resetPagination) {
|
||||
setInstallers(reseededResult.data || []);
|
||||
setDisplayedInstallers(reseededResult.data || []);
|
||||
setTotalInstallers(reseededResult.totalCount || 0);
|
||||
} else {
|
||||
const newInstallers = [...displayedInstallers, ...(reseededResult.data || [])];
|
||||
setDisplayedInstallers(newInstallers);
|
||||
setTotalInstallers(reseededResult.totalCount || 0);
|
||||
}
|
||||
} catch (seedError) {
|
||||
console.error('Error seeding database:', seedError);
|
||||
setError('Datenbank wird initialisiert. Bitte versuchen Sie es in wenigen Sekunden erneut.');
|
||||
setInstallers([]);
|
||||
setDisplayedInstallers([]);
|
||||
setTotalInstallers(0);
|
||||
}
|
||||
} else {
|
||||
if (resetPagination) {
|
||||
setInstallers(data);
|
||||
setDisplayedInstallers(data);
|
||||
setTotalInstallers(totalCount);
|
||||
} else {
|
||||
// Append new data for pagination
|
||||
const newInstallers = [...displayedInstallers, ...data];
|
||||
setDisplayedInstallers(newInstallers);
|
||||
setTotalInstallers(totalCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Track search event
|
||||
if (searchTerm || energyType !== "all" || bundesland !== "all") {
|
||||
analyticsService.trackEvent({
|
||||
event_type: 'installer_search',
|
||||
page_url: window.location.pathname,
|
||||
event_data: filters,
|
||||
user_agent: navigator.userAgent,
|
||||
session_id: sessionStorage.getItem('session_id') || 'anonymous'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading installers:', err);
|
||||
setError('Fehler beim Laden der Installateure. Bitte versuchen Sie es später erneut.');
|
||||
setInstallers([]);
|
||||
setDisplayedInstallers([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadInstallers();
|
||||
}, []);
|
||||
|
||||
// Reload when filters change
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
loadInstallers(true); // Reset pagination when filters change
|
||||
}, 500); // Debounce search
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [searchTerm, energyType, bundesland]);
|
||||
|
||||
// Update URL params when filters change
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams();
|
||||
if (searchTerm) params.set("search", searchTerm);
|
||||
if (energyType && energyType !== "all") params.set("type", energyType);
|
||||
if (bundesland && bundesland !== "all") params.set("bundesland", bundesland);
|
||||
setSearchParams(params);
|
||||
}, [searchTerm, energyType, bundesland, setSearchParams]);
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchTerm("");
|
||||
setEnergyType("all");
|
||||
setBundesland("all");
|
||||
};
|
||||
|
||||
// Handle "Show More" button click
|
||||
const handleShowMore = async (e: React.MouseEvent) => {
|
||||
e.preventDefault(); // Prevent default button behavior
|
||||
e.stopPropagation(); // Stop event bubbling
|
||||
|
||||
// Hard cut at 69 installers - don't load more after that
|
||||
if (displayedInstallers.length >= 69) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load next 10 installers, but limit to 69 total
|
||||
const nextPage = currentPage + 1;
|
||||
setCurrentPage(nextPage);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const filters = {
|
||||
energyType: energyType && energyType !== "all" ? energyType : undefined,
|
||||
bundesland: bundesland && bundesland !== "all" ? bundesland : undefined,
|
||||
searchTerm: searchTerm || undefined,
|
||||
limit: 10,
|
||||
offset: nextPage * 10
|
||||
};
|
||||
|
||||
const result = await installerService.getInstallers(filters);
|
||||
const newInstallers = [...displayedInstallers, ...(result.data || [])];
|
||||
|
||||
// Hard cut at 69 installers
|
||||
const limitedInstallers = newInstallers.slice(0, 69);
|
||||
setDisplayedInstallers(limitedInstallers);
|
||||
setTotalInstallers(Math.max(limitedInstallers.length, result.totalCount || 0));
|
||||
} catch (err) {
|
||||
console.error('Error loading more installers:', err);
|
||||
setError('Fehler beim Laden weiterer Installateure. Bitte versuchen Sie es später erneut.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Test connection
|
||||
const handleTestConnection = async () => {
|
||||
await testConnection();
|
||||
};
|
||||
|
||||
// Debug database
|
||||
const handleDebugDatabase = async () => {
|
||||
await debugDatabase();
|
||||
};
|
||||
|
||||
// Force delete all and reseed
|
||||
const handleForceReseed = async () => {
|
||||
try {
|
||||
await forceDeleteAll();
|
||||
await cleanAndReseedDatabase();
|
||||
alert('Database forcefully reseeded!');
|
||||
loadInstallers();
|
||||
} catch (error) {
|
||||
console.error('Error in force reseed:', error);
|
||||
alert('Error in force reseed. Check console.');
|
||||
}
|
||||
};
|
||||
|
||||
// Clean and reseed with proper German data
|
||||
const handleSeedDatabase = async () => {
|
||||
try {
|
||||
await cleanAndReseedDatabase();
|
||||
alert('Deutsche Installateure erfolgreich geladen!');
|
||||
loadInstallers(true); // Reload the data with pagination reset
|
||||
} catch (error) {
|
||||
console.error('Error seeding database:', error);
|
||||
alert('Fehler beim Laden der Installateure. Details in der Konsole.');
|
||||
}
|
||||
};
|
||||
|
||||
// Track contact clicks
|
||||
const handleContactClick = async (installerId: string, contactType: string) => {
|
||||
try {
|
||||
await analyticsService.trackContactClick({
|
||||
installer_id: installerId,
|
||||
contact_type: contactType,
|
||||
page_url: window.location.pathname,
|
||||
user_agent: navigator.userAgent,
|
||||
ip_address: await getClientIP()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error tracking contact click:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Simple IP detection (fallback)
|
||||
const getClientIP = async (): Promise<string> => {
|
||||
try {
|
||||
const response = await fetch('https://api.ipify.org?format=json');
|
||||
const data = await response.json();
|
||||
return data.ip;
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-4 text-muted-foreground">Installateure werden geladen...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="flex items-center justify-center py-32">
|
||||
<div className="text-center max-w-md mx-auto px-4">
|
||||
<AlertCircle className="w-16 h-16 text-destructive mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-foreground mb-2">Fehler beim Laden</h2>
|
||||
<p className="text-muted-foreground mb-6">{error}</p>
|
||||
<Button onClick={() => loadInstallers()} className="mb-4">
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header Navigation */}
|
||||
<Header />
|
||||
|
||||
{/* SEO-Optimized Page Header */}
|
||||
<div className="bg-gradient-to-r from-primary to-primary/80 text-white py-16 mt-16">
|
||||
<div className="container mx-auto px-4">
|
||||
{/* H1: Primary SEO heading */}
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
Finden Sie den besten Solar- oder Wind-Installateur in Ihrer Region
|
||||
</h1>
|
||||
<p className="text-xl text-white/90 max-w-3xl mb-6">
|
||||
Qualifizierte Fachbetriebe für erneuerbare Energien in Ihrer Nähe finden und vergleichen
|
||||
</p>
|
||||
|
||||
{/* SEO: Key benefits and CTAs */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-8">
|
||||
<div className="text-center p-4 bg-white/10 rounded-lg">
|
||||
<Calculator className="w-8 h-8 mx-auto mb-2" />
|
||||
<h3 className="font-semibold">Angebote vergleichen</h3>
|
||||
<p className="text-sm text-white/80">Kostenlos & unverbindlich</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-white/10 rounded-lg">
|
||||
<Award className="w-8 h-8 mx-auto mb-2" />
|
||||
<h3 className="font-semibold">Fördermöglichkeiten</h3>
|
||||
<p className="text-sm text-white/80">BAFA, KfW & kommunale Zuschüsse</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-white/10 rounded-lg">
|
||||
<Shield className="w-8 h-8 mx-auto mb-2" />
|
||||
<h3 className="font-semibold">Verifizierte Experten</h3>
|
||||
<p className="text-sm text-white/80">Zertifizierte Fachbetriebe</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SEO: Rich content section for better indexing */}
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
|
||||
{/* SEO Content Block 1: Solar Installation */}
|
||||
<Card className="border-l-4 border-l-orange-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-orange-700">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
Solar-Installation
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Photovoltaik-Anlagen für Ihr Zuhause: Von Balkonkraftwerken bis zur kompletten Dachanlage.
|
||||
Aktuelle Förderungen 2025, Kostenvergleich und qualifizierte Installateure in Ihrer Region.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>BAFA-Förderung bis 500€</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>KfW-Kredite mit Tilgungszuschuss</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>EEG-Vergütung für Überschuss</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SEO Content Block 2: Wind Energy */}
|
||||
<Card className="border-l-4 border-l-blue-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-blue-700">
|
||||
<Map className="w-5 h-5" />
|
||||
Windenergie
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Kleinwindanlagen für Privatgrundstücke: Genehmigungsverfahren, Schallemission,
|
||||
Mast-Höhen und aktuelle Förderprogramme. Fachbetriebe mit Windenergie-Expertise.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Genehmigung unter 15m Mast</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Bundesland-spezifische Vorgaben</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Schallemissions-Gutachten</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SEO Content Block 3: Regional Benefits */}
|
||||
<Card className="border-l-4 border-l-green-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-green-700">
|
||||
<Users className="w-5 h-5" />
|
||||
Regionale Vorteile
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Lokale Installateure kennen die regionalen Gegebenheiten, Förderprogramme und
|
||||
Genehmigungsverfahren in Ihrem Bundesland. Persönliche Beratung vor Ort.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Kommunale Förderprogramme</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Lokale Genehmigungsrichtlinien</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Regionale Preisvergleiche</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Filter className="w-5 h-5" />
|
||||
Suche & Filter
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
|
||||
<Input
|
||||
placeholder="Installateur oder Spezialität suchen..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={energyType}
|
||||
onChange={(e) => setEnergyType(e.target.value)}
|
||||
className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
>
|
||||
<option value="all">Alle Energiearten</option>
|
||||
<option value="solar">Solar</option>
|
||||
<option value="wind">Wind</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={bundesland}
|
||||
onChange={(e) => setBundesland(e.target.value)}
|
||||
className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
>
|
||||
<option value="all">Alle Bundesländer</option>
|
||||
<option value="Baden-Württemberg">Baden-Württemberg</option>
|
||||
<option value="Bayern">Bayern</option>
|
||||
<option value="Berlin">Berlin</option>
|
||||
<option value="Brandenburg">Brandenburg</option>
|
||||
<option value="Bremen">Bremen</option>
|
||||
<option value="Hamburg">Hamburg</option>
|
||||
<option value="Hessen">Hessen</option>
|
||||
<option value="Mecklenburg-Vorpommern">Mecklenburg-Vorpommern</option>
|
||||
<option value="Niedersachsen">Niedersachsen</option>
|
||||
<option value="Nordrhein-Westfalen">Nordrhein-Westfalen</option>
|
||||
<option value="Rheinland-Pfalz">Rheinland-Pfalz</option>
|
||||
<option value="Saarland">Saarland</option>
|
||||
<option value="Sachsen">Sachsen</option>
|
||||
<option value="Sachsen-Anhalt">Sachsen-Anhalt</option>
|
||||
<option value="Schleswig-Holstein">Schleswig-Holstein</option>
|
||||
<option value="Thüringen">Thüringen</option>
|
||||
</select>
|
||||
|
||||
<Button onClick={handleReset} variant="outline" className="w-full">
|
||||
Filter zurücksetzen
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Debug Section - Only show in development */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<Card className="mb-6 border-orange-200 bg-orange-50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-orange-800">Debug Tools (Development Only)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={handleTestConnection} variant="outline" size="sm">
|
||||
Test Connection
|
||||
</Button>
|
||||
<Button onClick={handleDebugDatabase} variant="outline" size="sm">
|
||||
Debug Database
|
||||
</Button>
|
||||
<Button onClick={handleSeedDatabase} variant="outline" size="sm">
|
||||
Seed Database
|
||||
</Button>
|
||||
<Button onClick={handleForceReseed} variant="outline" size="sm">
|
||||
Force Reseed
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Card className="mb-6 border-red-200 bg-red-50">
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-red-800 mb-2">Fehler beim Laden</h3>
|
||||
<p className="text-red-700 mb-4">{error}</p>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button onClick={() => loadInstallers(true)} variant="outline" className="border-red-300 text-red-700 hover:bg-red-100">
|
||||
Erneut versuchen
|
||||
</Button>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<Button onClick={handleSeedDatabase} variant="outline" className="border-orange-300 text-orange-700 hover:bg-orange-100">
|
||||
Datenbank initialisieren
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{loading && (
|
||||
<Card className="mb-6 border-blue-200 bg-blue-50">
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-blue-700">Installateure werden geladen...</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
<div className="mb-4">
|
||||
<p className="text-muted-foreground">
|
||||
{displayedInstallers.length} von {Math.max(displayedInstallers.length, totalInstallers)} Installateur{Math.max(displayedInstallers.length, totalInstallers) !== 1 ? 'e' : ''} angezeigt
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Installer Cards */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-16">
|
||||
{displayedInstallers.map((installer) => (
|
||||
<Card key={installer.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle className="text-xl mb-2">{installer.name}</CardTitle>
|
||||
<div className="flex items-center gap-2 text-muted-foreground mb-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span>{installer.location}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="w-4 h-4 fill-yellow-400 text-yellow-400" />
|
||||
<span className="font-semibold">{installer.rating}</span>
|
||||
<span className="text-muted-foreground">({installer.review_count || 0})</span>
|
||||
</div>
|
||||
<Badge variant="secondary">{installer.experience_years || 0} Jahre Erfahrung</Badge>
|
||||
{installer.verified && (
|
||||
<Badge variant="default" className="bg-green-100 text-green-800">
|
||||
Verifiziert
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge
|
||||
variant={installer.energy_type === 'solar' ? 'default' : 'secondary'}
|
||||
className={installer.energy_type === 'solar' ? 'bg-gradient-solar' : 'bg-gradient-wind'}
|
||||
>
|
||||
{installer.energy_type === 'solar' ? 'Solar' : 'Wind'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">{installer.description}</p>
|
||||
|
||||
<div className="mb-4">
|
||||
<h4 className="font-semibold mb-2">Spezialitäten:</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{installer.specialties?.map((specialty, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{specialty}
|
||||
</Badge>
|
||||
)) || (
|
||||
<span className="text-muted-foreground text-sm">Keine Spezialitäten angegeben</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{installer.certifications && installer.certifications.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h4 className="font-semibold mb-2">Zertifizierungen:</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{installer.certifications.map((cert, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs bg-blue-50 text-blue-700">
|
||||
{cert}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
handleContactClick(installer.id, 'phone');
|
||||
window.location.href = `tel:${installer.phone}`;
|
||||
}}
|
||||
>
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
Anrufen
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
handleContactClick(installer.id, 'email');
|
||||
window.location.href = `mailto:${installer.email}`;
|
||||
}}
|
||||
>
|
||||
<Mail className="w-4 h-4 mr-2" />
|
||||
E-Mail
|
||||
</Button>
|
||||
{installer.website && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
handleContactClick(installer.id, 'website');
|
||||
window.open(`https://${installer.website}`, '_blank');
|
||||
}}
|
||||
>
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
Website
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Show More Button */}
|
||||
{displayedInstallers.length > 0 && displayedInstallers.length < 69 && displayedInstallers.length < totalInstallers && (
|
||||
<div className="text-center mt-12 mb-16">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleShowMore}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="px-12 py-3 text-lg font-semibold"
|
||||
>
|
||||
Mehr anzeigen
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{displayedInstallers.length === 0 && (
|
||||
<Card className="text-center py-12">
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground text-lg mb-4">
|
||||
Keine Installateure gefunden, die Ihren Kriterien entsprechen.
|
||||
</p>
|
||||
<Button onClick={handleReset} variant="outline">
|
||||
Alle Filter zurücksetzen
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* SEO: FAQ Section for better content depth */}
|
||||
<Card className="mb-8">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Clock className="w-5 h-5" />
|
||||
Häufige Fragen zu Solar- und Wind-Installation
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="border-b pb-4">
|
||||
<h3 className="font-semibold mb-2">Welche Förderungen gibt es 2025 für Solaranlagen?</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
BAFA-Förderung für Balkonkraftwerke (bis 500€), KfW-Kredite mit Tilgungszuschuss,
|
||||
EEG-Vergütung für Überschussstrom und kommunale Zuschüsse je nach Bundesland.
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-b pb-4">
|
||||
<h3 className="font-semibold mb-2">Brauche ich eine Genehmigung für eine Kleinwindanlage?</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Mast-Höhen unter 15m sind in den meisten Bundesländern genehmigungsfrei.
|
||||
Ab 15m ist ein Bauantrag erforderlich. Schallemissions-Gutachten können nötig sein.
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-b pb-4">
|
||||
<h3 className="font-semibold mb-2">Wie finde ich den besten Installateur in meiner Region?</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Nutzen Sie unseren Vergleich: Bewertungen, Erfahrung, Zertifizierungen und
|
||||
regionale Expertise vergleichen. Lokale Installateure kennen die regionalen Vorgaben.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Was kostet eine Solaranlage 2025?</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Balkonkraftwerk: 400-800€, Komplette Dachanlage: 8.000-15.000€ je kWp.
|
||||
Preise variieren je nach Region, Anbieter und Ausstattung. Nutzen Sie unseren Kostenrechner.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SEO: Internal linking section */}
|
||||
<div className="mt-12 p-6 bg-gray-50 rounded-lg">
|
||||
<h2 className="text-2xl font-bold mb-4 text-center">Weitere nützliche Tools</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card className="text-center p-6 hover:shadow-md transition-shadow">
|
||||
<Calculator className="w-12 h-12 mx-auto mb-4 text-orange-600" />
|
||||
<h3 className="text-xl font-semibold mb-2">Solar-Einsparungsrechner</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Berechnen Sie Ihre Solareinsparungen und vergleichen Sie Angebote
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link
|
||||
to="/solar"
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
const calculatorElement = document.querySelector('[data-calculator]') ||
|
||||
document.querySelector('.calculator') ||
|
||||
document.querySelector('h2');
|
||||
if (calculatorElement) {
|
||||
calculatorElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
Zum Solarrechner
|
||||
</Link>
|
||||
</Button>
|
||||
</Card>
|
||||
<Card className="text-center p-6 hover:shadow-md transition-shadow">
|
||||
<TrendingUp className="w-12 h-12 mx-auto mb-4 text-blue-600" />
|
||||
<h3 className="text-xl font-semibold mb-2">Windenergie-Rechner</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Windenergie berechnen: Ertrag, Kosten & Angebote vergleichen
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" asChild>
|
||||
<Link
|
||||
to="/wind"
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
const calculatorElement = document.querySelector('[data-calculator]') ||
|
||||
document.querySelector('.calculator') ||
|
||||
document.querySelector('h2');
|
||||
if (calculatorElement) {
|
||||
calculatorElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
Zum Windrechner
|
||||
</Link>
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstallateurFinden;
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Phone, Mail, MapPin, Calendar, Clock, CheckCircle } from "lucide-react";
|
||||
import Header from "@/components/Header";
|
||||
|
||||
const KostenloseBeratung = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
zipCode: "",
|
||||
city: "",
|
||||
energyType: "",
|
||||
projectType: "",
|
||||
description: "",
|
||||
budget: "",
|
||||
timeline: "",
|
||||
contactPreference: "email",
|
||||
newsletter: false,
|
||||
privacyPolicy: false
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
const handleInputChange = (field: string, value: string | boolean) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
setIsSubmitting(false);
|
||||
setIsSubmitted(true);
|
||||
};
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<Card className="max-w-md text-center">
|
||||
<CardContent className="pt-6">
|
||||
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold mb-2">Beratungsanfrage gesendet!</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Vielen Dank für Ihr Interesse. Wir werden uns innerhalb von 24 Stunden bei Ihnen melden.
|
||||
</p>
|
||||
<Button onClick={() => window.location.href = "/"} className="w-full">
|
||||
Zurück zur Startseite
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header Navigation */}
|
||||
<Header />
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="bg-gradient-to-r from-primary to-primary/80 text-white py-16 mt-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
Kostenlose Beratung
|
||||
</h1>
|
||||
<p className="text-xl text-white/90 max-w-3xl">
|
||||
Lassen Sie sich von unseren Experten beraten und erhalten Sie ein maßgeschneidertes Angebot
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Form */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ihre Beratungsanfrage</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Personal Information */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">Vorname *</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleInputChange("firstName", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName">Nachname *</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleInputChange("lastName", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">E-Mail *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Telefon</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange("phone", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="zipCode">PLZ *</Label>
|
||||
<Input
|
||||
id="zipCode"
|
||||
value={formData.zipCode}
|
||||
onChange={(e) => handleInputChange("zipCode", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="city">Stadt *</Label>
|
||||
<Input
|
||||
id="city"
|
||||
value={formData.city}
|
||||
onChange={(e) => handleInputChange("city", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Information */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="energyType">Energieart *</Label>
|
||||
<Select value={formData.energyType} onValueChange={(value) => handleInputChange("energyType", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Energieart wählen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="solar">Solar</SelectItem>
|
||||
<SelectItem value="wind">Wind</SelectItem>
|
||||
<SelectItem value="both">Beide</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="projectType">Projekttyp *</Label>
|
||||
<Select value={formData.projectType} onValueChange={(value) => handleInputChange("projectType", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Projekttyp wählen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="private">Privathaushalt</SelectItem>
|
||||
<SelectItem value="commercial">Gewerbe</SelectItem>
|
||||
<SelectItem value="industrial">Industrie</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="budget">Budget</Label>
|
||||
<Select value={formData.budget} onValueChange={(value) => handleInputChange("budget", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Budget wählen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="under-10k">Unter 10.000 €</SelectItem>
|
||||
<SelectItem value="10k-25k">10.000 - 25.000 €</SelectItem>
|
||||
<SelectItem value="25k-50k">25.000 - 50.000 €</SelectItem>
|
||||
<SelectItem value="over-50k">Über 50.000 €</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="timeline">Zeitplan</Label>
|
||||
<Select value={formData.timeline} onValueChange={(value) => handleInputChange("timeline", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Zeitplan wählen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="immediate">Sofort</SelectItem>
|
||||
<SelectItem value="3months">Innerhalb 3 Monate</SelectItem>
|
||||
<SelectItem value="6months">Innerhalb 6 Monate</SelectItem>
|
||||
<SelectItem value="1year">Innerhalb 1 Jahr</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Projektbeschreibung</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Beschreiben Sie Ihr Projekt, Ihre Anforderungen und Ziele..."
|
||||
value={formData.description}
|
||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Kontaktpräferenz</Label>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="email-pref"
|
||||
name="contactPreference"
|
||||
value="email"
|
||||
checked={formData.contactPreference === "email"}
|
||||
onChange={(e) => handleInputChange("contactPreference", e.target.value)}
|
||||
/>
|
||||
<Label htmlFor="email-pref">E-Mail</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="phone-pref"
|
||||
name="contactPreference"
|
||||
value="phone"
|
||||
checked={formData.contactPreference === "phone"}
|
||||
onChange={(e) => handleInputChange("contactPreference", e.target.value)}
|
||||
/>
|
||||
<Label htmlFor="phone-pref">Telefon</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="newsletter"
|
||||
checked={formData.newsletter}
|
||||
onCheckedChange={(checked) => handleInputChange("newsletter", checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="newsletter">
|
||||
Ich möchte den Newsletter mit Tipps zu erneuerbaren Energien erhalten
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="privacyPolicy"
|
||||
checked={formData.privacyPolicy}
|
||||
onCheckedChange={(checked) => handleInputChange("privacyPolicy", checked as boolean)}
|
||||
required
|
||||
/>
|
||||
<Label htmlFor="privacyPolicy">
|
||||
Ich akzeptiere die <a href="/datenschutz" className="text-primary hover:underline">Datenschutzerklärung</a> *
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Wird gesendet..." : "Beratungsanfrage senden"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Benefits */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ihre Vorteile</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Kostenlose Beratung</h4>
|
||||
<p className="text-sm text-muted-foreground">Unverbindliche Erstberatung ohne Kosten</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Expertenwissen</h4>
|
||||
<p className="text-sm text-muted-foreground">Beratung durch zertifizierte Fachleute</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Maßgeschneiderte Lösungen</h4>
|
||||
<p className="text-sm text-muted-foreground">Individuelle Beratung für Ihr Projekt</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Förderung & Finanzierung</h4>
|
||||
<p className="text-sm text-muted-foreground">Informationen zu staatlichen Förderungen</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contact Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Kontakt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Phone className="w-5 h-5 text-primary" />
|
||||
<span>+49 (0) 800 123 4567</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Mail className="w-5 h-5 text-primary" />
|
||||
<span>beratung@energieprofis.de</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<MapPin className="w-5 h-5 text-primary" />
|
||||
<span>München, Deutschland</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock className="w-5 h-5 text-primary" />
|
||||
<span>Mo-Fr: 8:00 - 18:00</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KostenloseBeratung;
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { useEffect } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import SolarCalculator from "@/components/SolarCalculator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Sun, Zap, TrendingUp, Shield, ArrowRight } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Sun, Zap, TrendingUp, Shield, ArrowRight, Calculator, Award, CheckCircle, MapPin, Users, Clock, Star } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import solarImage from "@/assets/solar-installation.jpg";
|
||||
|
||||
const Solar = () => {
|
||||
const benefits = [
|
||||
|
|
@ -30,16 +31,118 @@ const Solar = () => {
|
|||
}
|
||||
];
|
||||
|
||||
// SEO: Update page title and meta description
|
||||
useEffect(() => {
|
||||
const title = "Solar-Einsparungen berechnen – EnergieProfis Rechner";
|
||||
const description = "Berechnen Sie Ihre Solareinsparungen und vergleichen Sie Angebote – jetzt online! Förderungen, Kosten & Potenziale im Solarrechner entdecken – gratis & unverbindlich.";
|
||||
|
||||
document.title = title;
|
||||
|
||||
// Update or create meta description
|
||||
let metaDesc = document.querySelector('meta[name="description"]');
|
||||
if (!metaDesc) {
|
||||
metaDesc = document.createElement('meta');
|
||||
metaDesc.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDesc);
|
||||
}
|
||||
metaDesc.setAttribute('content', description);
|
||||
|
||||
// Add canonical URL
|
||||
let canonical = document.querySelector('link[rel="canonical"]');
|
||||
if (!canonical) {
|
||||
canonical = document.createElement('link');
|
||||
canonical.setAttribute('rel', 'canonical');
|
||||
document.head.appendChild(canonical);
|
||||
}
|
||||
canonical.setAttribute('href', window.location.href);
|
||||
|
||||
// Add Open Graph tags
|
||||
const ogTags = [
|
||||
{ property: 'og:title', content: title },
|
||||
{ property: 'og:description', content: description },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:url', content: window.location.href },
|
||||
{ property: 'og:site_name', content: 'EnergieProfis' },
|
||||
{ property: 'og:image', content: `${window.location.origin}/solar_banner.png` }
|
||||
];
|
||||
|
||||
ogTags.forEach(tag => {
|
||||
let meta = document.querySelector(`meta[property="${tag.property}"]`);
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.setAttribute('property', tag.property);
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.setAttribute('content', tag.content);
|
||||
});
|
||||
|
||||
// Add structured data for the Solar page
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "Solar-Einsparungen berechnen",
|
||||
"description": "Solarrechner für Photovoltaik-Anlagen: Kosten, Förderungen und Ertrag berechnen",
|
||||
"url": window.location.href,
|
||||
"breadcrumb": {
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Startseite",
|
||||
"item": window.location.origin
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Solar",
|
||||
"item": window.location.href
|
||||
}
|
||||
]
|
||||
},
|
||||
"mainEntity": {
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Solar-Einsparungsrechner",
|
||||
"description": "Berechnen Sie Ihre Solareinsparungen und vergleichen Sie Angebote",
|
||||
"applicationCategory": "BusinessApplication",
|
||||
"operatingSystem": "Web Browser",
|
||||
"url": window.location.href,
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "EUR",
|
||||
"description": "Kostenloser Solarrechner"
|
||||
}
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "EnergieProfis",
|
||||
"url": window.location.origin
|
||||
}
|
||||
};
|
||||
|
||||
// Insert structured data into the page
|
||||
const script = document.createElement('script');
|
||||
script.type = 'application/ld+json';
|
||||
script.text = JSON.stringify(structuredData);
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (script.parentNode) script.parentNode.removeChild(script);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
{/* Hero Section */}
|
||||
{/* SEO-Optimized Hero Section */}
|
||||
<section className="relative min-h-[500px] flex items-center overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={solarImage}
|
||||
alt="Solar Installation"
|
||||
src="/solar_banner.png"
|
||||
alt="Solar Banner - Photovoltaik Installation und Solartechnik"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-solar/80 via-solar/60 to-transparent"></div>
|
||||
|
|
@ -47,13 +150,14 @@ const Solar = () => {
|
|||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<div className="max-w-2xl text-white">
|
||||
{/* H1: Primary SEO heading */}
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6 leading-tight">
|
||||
Solar-Installateure in Ihrer Nähe
|
||||
Solar-Einsparungen und Angebote berechnen
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-white/90 mb-8 leading-relaxed">
|
||||
Finden Sie qualifizierte Solartechnik-Experten für Photovoltaik-Anlagen,
|
||||
Solarthermie und komplette Energiesysteme.
|
||||
Solarthermie und komplette Energiesysteme. Kostenlos vergleichen & sparen.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
|
|
@ -62,16 +166,108 @@ const Solar = () => {
|
|||
Solar-Installateur Finden
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="solar-outline" size="xl" className="border-white/30 text-white hover:bg-white hover:text-solar">
|
||||
<Link to="/kostenlose-beratung?type=solar">
|
||||
Kostenlose Beratung
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SEO: Rich content section for better indexing */}
|
||||
<section className="py-16 bg-gradient-to-b from-background to-solar-light/10">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
|
||||
{/* SEO Content Block 1: Förderoptionen */}
|
||||
<Card className="border-l-4 border-l-orange-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-orange-700">
|
||||
<Award className="w-5 h-5" />
|
||||
Förderoptionen im Bundesland
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Aktuelle Förderprogramme 2025: BAFA-Zuschüsse für Balkonkraftwerke,
|
||||
KfW-Kredite mit Tilgungszuschuss und kommunale Förderungen.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>BAFA bis 500€ für Balkonkraftwerke</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>KfW 270 mit 10% Tilgungszuschuss</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>EEG-Vergütung für Überschussstrom</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SEO Content Block 2: Kosten und Wirtschaftlichkeit */}
|
||||
<Card className="border-l-4 border-l-green-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-green-700">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
Kosten und Wirtschaftlichkeit
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Transparente Kostenaufstellung: Von Balkonkraftwerken bis zur kompletten
|
||||
Dachanlage. Amortisation in 7-12 Jahren möglich.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Balkonkraftwerk: 400-800€</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Dachanlage: 8.000-15.000€/kWp</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Speicher: 1.200-1.800€/kWh</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SEO Content Block 3: Ertragsprognose */}
|
||||
<Card className="border-l-4 border-l-blue-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-blue-700">
|
||||
<Calculator className="w-5 h-5" />
|
||||
Ertragsprognose für Ihre Region
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Standort-spezifische Ertragsberechnung: Sonneneinstrahlung, Dachneigung,
|
||||
Ausrichtung und lokale Wetterdaten berücksichtigt.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Dachneigung 30-35° optimal</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Süd-Ausrichtung bevorzugt</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>800-1.200 kWh/kWp pro Jahr</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Benefits Section */}
|
||||
<section className="py-20 bg-gradient-to-b from-background to-solar-light/10">
|
||||
<div className="container mx-auto px-4">
|
||||
|
|
@ -106,6 +302,48 @@ const Solar = () => {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* Solar Calculator Section */}
|
||||
<SolarCalculator />
|
||||
|
||||
{/* SEO: FAQ Section for better content depth */}
|
||||
<section className="py-16 bg-gray-50">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Häufige Fragen zu Solar-Installationen</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Welche Förderungen gibt es 2025 für Solaranlagen?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
BAFA-Förderung für Balkonkraftwerke (bis 500€), KfW-Kredite mit Tilgungszuschuss,
|
||||
EEG-Vergütung für Überschussstrom und kommunale Zuschüsse je nach Bundesland.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Was kostet eine Solaranlage 2025?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Balkonkraftwerk: 400-800€, Komplette Dachanlage: 8.000-15.000€ je kWp.
|
||||
Preise variieren je nach Region, Anbieter und Ausstattung.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Wie lange dauert die Amortisation?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Bei aktuellen Strompreisen amortisiert sich eine Solaranlage in 7-12 Jahren.
|
||||
Mit Förderungen und steigenden Energiekosten kann sich die Zeit verkürzen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Brauche ich einen Stromspeicher?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Ein Speicher ist nicht zwingend erforderlich, erhöht aber den Eigenverbrauch
|
||||
von 30% auf 60-80%. Die Wirtschaftlichkeit hängt von Ihrem Verbrauchsprofil ab.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-20 bg-gradient-solar text-white">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
|
|
@ -118,8 +356,8 @@ const Solar = () => {
|
|||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button variant="hero" size="xl" className="bg-white text-solar hover:bg-white/90" asChild>
|
||||
<Link to="/kostenlose-beratung?type=solar">
|
||||
Jetzt kostenlose Beratung anfordern
|
||||
<Link to="/installateur-finden?type=solar">
|
||||
Solar-Installateure finden
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,513 @@
|
|||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { SimpleSelect, SimpleSelectItem } from "@/components/ui/simple-select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Building2, MapPin, Phone, Mail, Globe, CheckCircle, Users, Award, Clock } from "lucide-react";
|
||||
import Header from "@/components/Header";
|
||||
import { usePostHog } from "@/hooks/usePostHog";
|
||||
import { sendCompanyRegistrationEmail } from "@/lib/emailService";
|
||||
|
||||
const UnternehmenListen = () => {
|
||||
const posthog = usePostHog();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
companyName: "",
|
||||
contactPerson: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
website: "",
|
||||
zipCode: "",
|
||||
city: "",
|
||||
energyTypes: [] as string[],
|
||||
services: [] as string[],
|
||||
description: "",
|
||||
experience: "",
|
||||
certifications: [] as string[],
|
||||
coverageArea: "",
|
||||
contactPreference: "email",
|
||||
newsletter: false,
|
||||
privacyPolicy: false
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
const handleInputChange = (field: string, value: string | boolean | string[]) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
|
||||
// Track form interactions
|
||||
if (field === 'experience') {
|
||||
posthog.capture('experience_selected', {
|
||||
experience_level: value,
|
||||
form_section: 'company_registration'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnergyTypeChange = (type: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
energyTypes: [...prev.energyTypes, type]
|
||||
}));
|
||||
} else {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
energyTypes: prev.energyTypes.filter(t => t !== type)
|
||||
}));
|
||||
}
|
||||
|
||||
// Track energy type selection
|
||||
posthog.capture('energy_type_selected', {
|
||||
energy_type: type,
|
||||
selected: checked,
|
||||
form_section: 'company_registration'
|
||||
});
|
||||
};
|
||||
|
||||
const handleServiceChange = (service: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
services: [...prev.services, service]
|
||||
}));
|
||||
} else {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
services: prev.services.filter(s => s !== service)
|
||||
}));
|
||||
}
|
||||
|
||||
// Track service selection
|
||||
posthog.capture('service_selected', {
|
||||
service: service,
|
||||
selected: checked,
|
||||
form_section: 'company_registration'
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Track form submission in PostHog
|
||||
posthog.capture('company_registration_started', {
|
||||
company_name: formData.companyName,
|
||||
energy_types: formData.energyTypes,
|
||||
services: formData.services,
|
||||
experience: formData.experience,
|
||||
location: `${formData.zipCode} ${formData.city}`
|
||||
});
|
||||
|
||||
try {
|
||||
// Send email notification via webhook
|
||||
const result = await sendCompanyRegistrationEmail(formData);
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ E-Mail erfolgreich an Webhook gesendet!');
|
||||
console.log('📧 Daten wurden an knuth.timo@gmail.com weitergeleitet');
|
||||
|
||||
// Track successful submission
|
||||
posthog.capture('company_registration_completed', {
|
||||
company_name: formData.companyName,
|
||||
energy_types: formData.energyTypes,
|
||||
services: formData.services,
|
||||
experience: formData.experience,
|
||||
location: `${formData.zipCode} ${formData.city}`
|
||||
});
|
||||
} else {
|
||||
console.log('⚠️ Webhook-Fehler, aber Daten wurden geloggt:', result.data);
|
||||
|
||||
// Track failed submission
|
||||
posthog.capture('company_registration_failed', {
|
||||
company_name: formData.companyName,
|
||||
error: result.error || 'Webhook failed'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Senden der E-Mail:', error);
|
||||
|
||||
// Track failed submission
|
||||
posthog.capture('company_registration_failed', {
|
||||
company_name: formData.companyName,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
setIsSubmitted(true);
|
||||
};
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<Card className="max-w-md text-center">
|
||||
<CardContent className="pt-6">
|
||||
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold mb-2">Anmeldung erfolgreich!</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Vielen Dank für Ihre Anmeldung. Wir werden Ihr Unternehmen innerhalb von 48 Stunden prüfen und freischalten.
|
||||
</p>
|
||||
<Button onClick={() => window.location.href = "/"} className="w-full">
|
||||
Zurück zur Startseite
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header Navigation */}
|
||||
<Header />
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="bg-gradient-to-r from-primary to-primary/80 text-white py-16 mt-16">
|
||||
<div className="container mx-auto px-4">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
Unternehmen Listen
|
||||
</h1>
|
||||
<p className="text-xl text-white/90 max-w-3xl">
|
||||
Werden Sie Teil unseres Netzwerks und erreichen Sie neue Kunden für erneuerbare Energien
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Form */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Unternehmensanmeldung</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Company Information */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="companyName">Firmenname *</Label>
|
||||
<Input
|
||||
id="companyName"
|
||||
value={formData.companyName}
|
||||
onChange={(e) => handleInputChange("companyName", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="contactPerson">Ansprechpartner *</Label>
|
||||
<Input
|
||||
id="contactPerson"
|
||||
value={formData.contactPerson}
|
||||
onChange={(e) => handleInputChange("contactPerson", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">E-Mail *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Telefon *</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange("phone", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="website">Website</Label>
|
||||
<Input
|
||||
id="website"
|
||||
type="url"
|
||||
value={formData.website}
|
||||
onChange={(e) => handleInputChange("website", e.target.value)}
|
||||
placeholder="https://www.example.de"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="zipCode">PLZ *</Label>
|
||||
<Input
|
||||
id="zipCode"
|
||||
value={formData.zipCode}
|
||||
onChange={(e) => handleInputChange("zipCode", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="city">Stadt *</Label>
|
||||
<Input
|
||||
id="city"
|
||||
value={formData.city}
|
||||
onChange={(e) => handleInputChange("city", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Energy Types */}
|
||||
<div className="space-y-2">
|
||||
<Label>Energiearten *</Label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="solar"
|
||||
checked={formData.energyTypes.includes("solar")}
|
||||
onCheckedChange={(checked) => handleEnergyTypeChange("solar", checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="solar">Solar</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="wind"
|
||||
checked={formData.energyTypes.includes("wind")}
|
||||
onCheckedChange={(checked) => handleEnergyTypeChange("wind", checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="wind">Wind</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Services */}
|
||||
<div className="space-y-2">
|
||||
<Label>Angebotene Leistungen *</Label>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="planning"
|
||||
checked={formData.services.includes("planning")}
|
||||
onCheckedChange={(checked) => handleServiceChange("planning", checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="planning">Planung & Beratung</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="installation"
|
||||
checked={formData.services.includes("installation")}
|
||||
onCheckedChange={(checked) => handleServiceChange("installation", checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="installation">Installation</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="maintenance"
|
||||
checked={formData.services.includes("maintenance")}
|
||||
onCheckedChange={(checked) => handleServiceChange("maintenance", checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="maintenance">Wartung & Service</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="financing"
|
||||
checked={formData.services.includes("financing")}
|
||||
onCheckedChange={(checked) => handleServiceChange("financing", checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="financing">Finanzierung</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="experience">Jahre Erfahrung *</Label>
|
||||
<SimpleSelect
|
||||
value={formData.experience}
|
||||
onValueChange={(value) => handleInputChange("experience", value)}
|
||||
placeholder="Erfahrung wählen"
|
||||
>
|
||||
<SimpleSelectItem value="0-2">0-2 Jahre</SimpleSelectItem>
|
||||
<SimpleSelectItem value="3-5">3-5 Jahre</SimpleSelectItem>
|
||||
<SimpleSelectItem value="6-10">6-10 Jahre</SimpleSelectItem>
|
||||
<SimpleSelectItem value="10+">Über 10 Jahre</SimpleSelectItem>
|
||||
</SimpleSelect>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="coverageArea">Einzugsgebiet</Label>
|
||||
<Input
|
||||
id="coverageArea"
|
||||
value={formData.coverageArea}
|
||||
onChange={(e) => handleInputChange("coverageArea", e.target.value)}
|
||||
placeholder="z.B. Bayern, 50km Umkreis"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Unternehmensbeschreibung *</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Beschreiben Sie Ihr Unternehmen, Ihre Spezialitäten und warum Kunden Sie wählen sollten..."
|
||||
value={formData.description}
|
||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Kontaktpräferenz</Label>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="email-pref"
|
||||
name="contactPreference"
|
||||
value="email"
|
||||
checked={formData.contactPreference === "email"}
|
||||
onChange={(e) => handleInputChange("contactPreference", e.target.value)}
|
||||
/>
|
||||
<Label htmlFor="email-pref">E-Mail</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="phone-pref"
|
||||
name="contactPreference"
|
||||
value="phone"
|
||||
checked={formData.contactPreference === "phone"}
|
||||
onChange={(e) => handleInputChange("contactPreference", e.target.value)}
|
||||
/>
|
||||
<Label htmlFor="phone-pref">Telefon</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="newsletter"
|
||||
checked={formData.newsletter}
|
||||
onCheckedChange={(checked) => handleInputChange("newsletter", checked as boolean)}
|
||||
/>
|
||||
<Label htmlFor="newsletter">
|
||||
Ich möchte über neue Funktionen und Marketing-Möglichkeiten informiert werden
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="privacyPolicy"
|
||||
checked={formData.privacyPolicy}
|
||||
onCheckedChange={(checked) => handleInputChange("privacyPolicy", checked as boolean)}
|
||||
required
|
||||
/>
|
||||
<Label htmlFor="privacyPolicy">
|
||||
Ich akzeptiere die <a href="/datenschutz" className="text-primary hover:underline">Datenschutzerklärung</a> *
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Wird gesendet..." : "Unternehmen anmelden"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Benefits */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ihre Vorteile</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Users className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Neue Kunden gewinnen</h4>
|
||||
<p className="text-sm text-muted-foreground">Erreichen Sie potenzielle Kunden in Ihrer Region</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Award className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Vertrauen aufbauen</h4>
|
||||
<p className="text-sm text-muted-foreground">Präsentieren Sie sich als verifizierter Experte</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Clock className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Zeit sparen</h4>
|
||||
<p className="text-sm text-muted-foreground">Kunden finden Sie automatisch über unsere Plattform</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Building2 className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold">Professioneller Auftritt</h4>
|
||||
<p className="text-sm text-muted-foreground">Präsentieren Sie Ihr Unternehmen professionell</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Process */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ablauf</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 bg-primary text-white rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0">1</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">Anmeldung</h4>
|
||||
<p className="text-sm text-muted-foreground">Füllen Sie das Formular aus</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 bg-primary text-white rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0">2</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">Prüfung</h4>
|
||||
<p className="text-sm text-muted-foreground">Wir prüfen Ihre Angaben (48h)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 bg-primary text-white rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0">3</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">Freischaltung</h4>
|
||||
<p className="text-sm text-muted-foreground">Ihr Profil wird veröffentlicht</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-6 h-6 bg-primary text-white rounded-full flex items-center justify-center text-sm font-bold flex-shrink-0">4</div>
|
||||
<div>
|
||||
<h4 className="font-semibold">Kundenkontakte</h4>
|
||||
<p className="text-sm text-muted-foreground">Erhalten Sie Anfragen von Kunden</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnternehmenListen;
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { useEffect } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import WindCalculator from "@/components/WindCalculator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Wind, Zap, TrendingUp, Shield, ArrowRight } from "lucide-react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Wind, Zap, TrendingUp, Shield, ArrowRight, Calculator, Award, CheckCircle, MapPin, Users, Clock, Star, Map } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import windImage from "@/assets/wind-turbines.jpg";
|
||||
|
||||
const WindPage = () => {
|
||||
const benefits = [
|
||||
|
|
@ -30,30 +31,133 @@ const WindPage = () => {
|
|||
}
|
||||
];
|
||||
|
||||
// SEO: Update page title and meta description
|
||||
useEffect(() => {
|
||||
const title = "Windenergie-Ertrag berechnen – EnergieProfis Rechner";
|
||||
const description = "Windenergie berechnen: Ertrag, Kosten & Angebote vergleichen – jetzt kostenlos! Förderungen und Potenzial für Ihre Windanlage im Rechner entdecken.";
|
||||
|
||||
document.title = title;
|
||||
|
||||
// Update or create meta description
|
||||
let metaDesc = document.querySelector('meta[name="description"]');
|
||||
if (!metaDesc) {
|
||||
metaDesc = document.createElement('meta');
|
||||
metaDesc.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDesc);
|
||||
}
|
||||
metaDesc.setAttribute('content', description);
|
||||
|
||||
// Add canonical URL
|
||||
let canonical = document.querySelector('link[rel="canonical"]');
|
||||
if (!canonical) {
|
||||
canonical = document.createElement('link');
|
||||
canonical.setAttribute('rel', 'canonical');
|
||||
document.head.appendChild(canonical);
|
||||
}
|
||||
canonical.setAttribute('href', window.location.href);
|
||||
|
||||
// Add Open Graph tags
|
||||
const ogTags = [
|
||||
{ property: 'og:title', content: title },
|
||||
{ property: 'og:description', content: description },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:url', content: window.location.href },
|
||||
{ property: 'og:site_name', content: 'EnergieProfis' },
|
||||
{ property: 'og:image', content: `${window.location.origin}/wind_banner.png` }
|
||||
];
|
||||
|
||||
ogTags.forEach(tag => {
|
||||
let meta = document.querySelector(`meta[property="${tag.property}"]`);
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.setAttribute('property', tag.property);
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
meta.setAttribute('content', tag.content);
|
||||
});
|
||||
|
||||
// Add structured data for the Wind page
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "Windenergie-Ertrag berechnen",
|
||||
"description": "Windenergie-Rechner für Kleinwindanlagen: Ertrag, Kosten und Genehmigungen berechnen",
|
||||
"url": window.location.href,
|
||||
"breadcrumb": {
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Startseite",
|
||||
"item": window.location.origin
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Windenergie",
|
||||
"item": window.location.href
|
||||
}
|
||||
]
|
||||
},
|
||||
"mainEntity": {
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Windenergie-Rechner",
|
||||
"description": "Windenergie berechnen: Ertrag, Kosten & Angebote vergleichen",
|
||||
"applicationCategory": "BusinessApplication",
|
||||
"operatingSystem": "Web Browser",
|
||||
"url": window.location.href,
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "EUR",
|
||||
"description": "Kostenloser Windenergie-Rechner"
|
||||
}
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "EnergieProfis",
|
||||
"url": window.location.origin
|
||||
}
|
||||
};
|
||||
|
||||
// Insert structured data into the page
|
||||
const script = document.createElement('script');
|
||||
script.type = 'application/ld+json';
|
||||
script.text = JSON.stringify(structuredData);
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (script.parentNode) script.parentNode.removeChild(script);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
{/* Hero Section */}
|
||||
{/* SEO-Optimized Hero Section */}
|
||||
<section className="relative min-h-[500px] flex items-center overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={windImage}
|
||||
alt="Wind Turbines"
|
||||
className="w-full h-full object-cover"
|
||||
src="/wind_banner.png"
|
||||
alt="Wind Banner - Windkraftanlagen und Kleinwindanlagen Installation"
|
||||
className="w-full h-full object-cover object-bottom"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-wind/80 via-wind/60 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<div className="max-w-2xl text-white">
|
||||
{/* H1: Primary SEO heading */}
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6 leading-tight">
|
||||
Wind-Installateure für Ihre Region
|
||||
Windkraft-Ertrag und Angebote berechnen
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-white/90 mb-8 leading-relaxed">
|
||||
Entdecken Sie die Kraft des Windes mit modernen Windkraftanlagen
|
||||
für private und gewerbliche Nutzung.
|
||||
für private und gewerbliche Nutzung. Kostenlos vergleichen & sparen.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
|
|
@ -62,16 +166,108 @@ const WindPage = () => {
|
|||
Wind-Installateur Finden
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="wind-outline" size="xl" className="border-white/30 text-white hover:bg-white hover:text-wind">
|
||||
<Link to="/kostenlose-beratung?type=wind">
|
||||
Kostenlose Beratung
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SEO: Rich content section for better indexing */}
|
||||
<section className="py-16 bg-gradient-to-b from-background to-wind-light/10">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
|
||||
{/* SEO Content Block 1: Fördermittel und Genehmigungen */}
|
||||
<Card className="border-l-4 border-l-blue-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-blue-700">
|
||||
<Award className="w-5 h-5" />
|
||||
Fördermittel und Genehmigungen
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Bundesland-spezifische Förderprogramme und Genehmigungsverfahren:
|
||||
Mast-Höhen unter 15m sind in den meisten Bundesländern genehmigungsfrei.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Genehmigung unter 15m Mast</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Bundesland-spezifische Vorgaben</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Schallemissions-Gutachten</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SEO Content Block 2: Kosten und Rendite */}
|
||||
<Card className="border-l-4 border-l-green-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-green-700">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
Kosten und Rendite hier kalkulieren
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Transparente Kostenaufstellung: Von Kleinwindanlagen bis zu größeren
|
||||
Systemen. Amortisation in 8-15 Jahren möglich.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Kleinwindanlage: 3.000-15.000€</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Mast und Fundament: 1.500-5.000€</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Installation: 2.000-8.000€</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SEO Content Block 3: Angebote vergleichen */}
|
||||
<Card className="border-l-4 border-l-purple-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-purple-700">
|
||||
<Map className="w-5 h-5" />
|
||||
Angebote für Windanlagen vergleichen
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Qualifizierte Wind-Installateure in Ihrer Region finden und
|
||||
Angebote direkt vergleichen. Kostenlos und unverbindlich.
|
||||
</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Lokale Fachbetriebe</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Kostenlose Angebote</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Direkter Vergleich</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Benefits Section */}
|
||||
<section className="py-20 bg-gradient-to-b from-background to-wind-light/10">
|
||||
<div className="container mx-auto px-4">
|
||||
|
|
@ -106,6 +302,48 @@ const WindPage = () => {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* Wind Calculator Section */}
|
||||
<WindCalculator />
|
||||
|
||||
{/* SEO: FAQ Section for better content depth */}
|
||||
<section className="py-16 bg-gray-50">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Häufige Fragen zu Windkraft-Installationen</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Brauche ich eine Genehmigung für eine Kleinwindanlage?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Mast-Höhen unter 15m sind in den meisten Bundesländern genehmigungsfrei.
|
||||
Ab 15m ist ein Bauantrag erforderlich. Schallemissions-Gutachten können nötig sein.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Was kostet eine Windkraftanlage 2025?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Kleinwindanlage: 3.000-15.000€, Mast und Fundament: 1.500-5.000€,
|
||||
Installation: 2.000-8.000€. Preise variieren je nach Größe und Region.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Welche Windgeschwindigkeit ist optimal?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Moderne Anlagen starten bereits bei 2-3 m/s Windgeschwindigkeit.
|
||||
Optimal sind 5-8 m/s. Ab 12 m/s wird die Leistung begrenzt.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm">
|
||||
<h3 className="font-semibold mb-3 text-lg">Wie laut ist eine Kleinwindanlage?</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Moderne Anlagen sind sehr leise. Bei 50m Entfernung liegt der Schallpegel
|
||||
bei 35-45 dB(A), vergleichbar mit leisen Gesprächen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-20 bg-gradient-wind text-white">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
|
|
@ -118,8 +356,8 @@ const WindPage = () => {
|
|||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button variant="hero" size="xl" className="bg-white text-wind hover:bg-white/90" asChild>
|
||||
<Link to="/kostenlose-beratung?type=wind">
|
||||
Jetzt kostenlose Beratung anfordern
|
||||
<Link to="/installateur-finden?type=wind">
|
||||
Wind-Installateure finden
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,244 @@
|
|||
/**
|
||||
* Example page demonstrating Answer Engine Optimization (AEO) implementation
|
||||
* This shows how to integrate all AEO components for maximum AI citation potential
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import Canonical from '@/components/seo/Canonical';
|
||||
import JsonLd from '@/components/seo/JsonLd';
|
||||
import ContentMeta from '@/components/seo/ContentMeta';
|
||||
import {
|
||||
buildArticleJsonLd,
|
||||
buildFAQPageJsonLd,
|
||||
buildHowToJsonLd,
|
||||
buildBreadcrumbJsonLd,
|
||||
exampleFAQs,
|
||||
exampleHowTo
|
||||
} from '@/lib/schema';
|
||||
import { queueCurrentPage } from '@/lib/indexnow';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { CheckCircle, ArrowRight, Info } from 'lucide-react';
|
||||
|
||||
const AeoExample: React.FC = () => {
|
||||
// Page metadata for Article schema
|
||||
const articleData = {
|
||||
headline: 'Solaranlage installieren lassen - Vollständiger Leitfaden 2025',
|
||||
description: 'Schritt-für-Schritt Anleitung: Wie Sie die richtige Solaranlage finden, Installateure vergleichen und Förderungen optimal nutzen.',
|
||||
datePublished: new Date('2024-12-01'),
|
||||
dateModified: new Date('2025-01-04'),
|
||||
author: {
|
||||
name: 'EnergieProfis Redaktion',
|
||||
url: 'https://energie-profis.de/team',
|
||||
jobTitle: 'Energie-Experten',
|
||||
organization: 'EnergieProfis'
|
||||
},
|
||||
publisher: {
|
||||
name: 'EnergieProfis',
|
||||
description: 'Führendes Verzeichnis für erneuerbare Energien',
|
||||
url: 'https://energie-profis.de',
|
||||
logo: 'https://energie-profis.de/favicon.ico'
|
||||
},
|
||||
images: [
|
||||
'https://energie-profis.de/solar_banner.png',
|
||||
'https://energie-profis.de/sun_flow_banner.png'
|
||||
],
|
||||
url: 'https://energie-profis.de/solaranlage-installieren-lassen-leitfaden'
|
||||
};
|
||||
|
||||
// Breadcrumb navigation for better structure
|
||||
const breadcrumbs = [
|
||||
{ name: 'Home', url: 'https://energie-profis.de/' },
|
||||
{ name: 'Solar', url: 'https://energie-profis.de/solar' },
|
||||
{ name: 'Installation Guide', url: 'https://energie-profis.de/solaranlage-installieren-lassen-leitfaden' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Queue page for IndexNow submission
|
||||
queueCurrentPage();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* AEO Components - These don't render visible content */}
|
||||
<Canonical />
|
||||
<JsonLd data={buildArticleJsonLd(articleData)} />
|
||||
<JsonLd data={buildFAQPageJsonLd(exampleFAQs, articleData.url)} />
|
||||
<JsonLd data={buildHowToJsonLd(exampleHowTo, articleData.url)} />
|
||||
<JsonLd data={buildBreadcrumbJsonLd(breadcrumbs)} />
|
||||
|
||||
{/* Visible Page Content */}
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||
|
||||
{/* Breadcrumb Navigation */}
|
||||
<nav className="text-sm text-muted-foreground mb-6">
|
||||
<ol className="flex items-center space-x-2">
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<li key={index} className="flex items-center">
|
||||
<a href={crumb.url} className="hover:text-primary transition-colors">
|
||||
{crumb.name}
|
||||
</a>
|
||||
{index < breadcrumbs.length - 1 && (
|
||||
<ArrowRight className="w-4 h-4 mx-2" />
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{/* Article Header */}
|
||||
<header className="mb-8">
|
||||
<h1 className="text-4xl font-bold mb-4">
|
||||
{articleData.headline}
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-6">
|
||||
{articleData.description}
|
||||
</p>
|
||||
|
||||
{/* Content Metadata - Visible to users and AI */}
|
||||
<ContentMeta
|
||||
lastUpdated={articleData.dateModified}
|
||||
publishedDate={articleData.datePublished}
|
||||
authorName={articleData.author.name}
|
||||
authorUrl={articleData.author.url}
|
||||
readTime="8 Minuten"
|
||||
/>
|
||||
</header>
|
||||
|
||||
{/* Main Article Content */}
|
||||
<article className="prose prose-lg max-w-none">
|
||||
|
||||
{/* Introduction with rich context */}
|
||||
<section className="mb-8">
|
||||
<h2>Warum eine professionelle Solaranlage-Installation?</h2>
|
||||
<p>
|
||||
Die Installation einer Solaranlage ist eine der wichtigsten Investitionen für nachhaltige
|
||||
Energieversorgung. Mit steigenden Strompreisen und verbesserten Förderprogrammen wird
|
||||
Solarenergie 2025 noch attraktiver. Dieser Leitfaden hilft Ihnen dabei, die richtige
|
||||
Entscheidung zu treffen.
|
||||
</p>
|
||||
|
||||
<Card className="mt-6 border-l-4 border-l-green-500">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Info className="w-5 h-5 text-green-600" />
|
||||
Wichtige Vorteile 2025
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>BAFA-Förderung bis 500€ für Balkonkraftwerke</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>KfW-Kredite mit günstigen Zinsen</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Wegfall der EEG-Umlage für Eigenverbrauch</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* HowTo Section - Structured for AI understanding */}
|
||||
<section className="mb-8">
|
||||
<h2>{exampleHowTo.name}</h2>
|
||||
<p className="mb-6">{exampleHowTo.description}</p>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{exampleHowTo.steps.map((step, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">
|
||||
Schritt {index + 1}: {step.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{step.text}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-muted rounded-lg">
|
||||
<h3 className="font-semibold mb-2">Geschätzte Kosten und Zeitaufwand</h3>
|
||||
<p><strong>Kosten:</strong> {exampleHowTo.estimatedCost}€</p>
|
||||
<p><strong>Zeitaufwand:</strong> 2 Wochen (von Planung bis Installation)</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section - Optimized for AI answers */}
|
||||
<section className="mb-8">
|
||||
<h2>Häufig gestellte Fragen zur Solar-Installation</h2>
|
||||
<div className="space-y-6">
|
||||
{exampleFAQs.map((faq, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{faq.question}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{faq.answer}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Expert Insights for Authority */}
|
||||
<section className="mb-8">
|
||||
<h2>Expertentipps für 2025</h2>
|
||||
<div className="bg-gradient-to-r from-orange-50 to-yellow-50 p-6 rounded-lg">
|
||||
<h3 className="font-semibold text-orange-800 mb-3">
|
||||
Aktuelle Marktentwicklungen
|
||||
</h3>
|
||||
<ul className="space-y-2 text-orange-700">
|
||||
<li>• Modulpreise sind 2025 um 20% gefallen</li>
|
||||
<li>• Neue Speichertechnologien mit längerer Lebensdauer</li>
|
||||
<li>• Vereinfachte Anmeldeverfahren in allen Bundesländern</li>
|
||||
<li>• Smart Home Integration wird Standard</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* Call to Action */}
|
||||
<div className="mt-12 text-center">
|
||||
<Card className="max-w-2xl mx-auto">
|
||||
<CardContent className="pt-6">
|
||||
<h3 className="text-2xl font-bold mb-4">
|
||||
Bereit für Ihre Solaranlage?
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Finden Sie qualifizierte Installateure in Ihrer Region und
|
||||
erhalten Sie kostenlose Angebote.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<a
|
||||
href="/installateur-finden"
|
||||
className="inline-block bg-primary text-primary-foreground px-8 py-3 rounded-lg font-semibold hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Installateure finden
|
||||
</a>
|
||||
<br />
|
||||
<a
|
||||
href="/kostenlose-beratung"
|
||||
className="inline-block text-primary hover:underline"
|
||||
>
|
||||
Kostenlose Beratung vereinbaren
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AeoExample;
|
||||
|
|
@ -65,18 +65,6 @@ export default {
|
|||
light: 'hsl(var(--wind-light))',
|
||||
dark: 'hsl(var(--wind-dark))'
|
||||
},
|
||||
geo: {
|
||||
DEFAULT: 'hsl(var(--geo-primary))',
|
||||
secondary: 'hsl(var(--geo-secondary))',
|
||||
light: 'hsl(var(--geo-light))',
|
||||
dark: 'hsl(var(--geo-dark))'
|
||||
},
|
||||
battery: {
|
||||
DEFAULT: 'hsl(var(--battery-primary))',
|
||||
secondary: 'hsl(var(--battery-secondary))',
|
||||
light: 'hsl(var(--battery-light))',
|
||||
dark: 'hsl(var(--battery-dark))'
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
|
|
@ -91,15 +79,11 @@ export default {
|
|||
backgroundImage: {
|
||||
'gradient-solar': 'var(--gradient-solar)',
|
||||
'gradient-wind': 'var(--gradient-wind)',
|
||||
'gradient-geo': 'var(--gradient-geo)',
|
||||
'gradient-battery': 'var(--gradient-battery)',
|
||||
'gradient-hero': 'var(--gradient-hero)'
|
||||
},
|
||||
boxShadow: {
|
||||
'solar': 'var(--shadow-solar)',
|
||||
'wind': 'var(--shadow-wind)',
|
||||
'geo': 'var(--shadow-geo)',
|
||||
'battery': 'var(--shadow-battery)'
|
||||
'wind': 'var(--shadow-wind)'
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
'smooth': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Canonical URL Validation', () => {
|
||||
test('homepage should have exactly one canonical link', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const canonicalLinks = await page.locator('link[rel="canonical"]').all();
|
||||
expect(canonicalLinks.length).toBe(1);
|
||||
|
||||
const href = await canonicalLinks[0].getAttribute('href');
|
||||
expect(href).toBe('https://energie-profis.de/');
|
||||
});
|
||||
|
||||
test('all main pages should have proper canonical URLs', async ({ page }) => {
|
||||
const pages = [
|
||||
{ path: '/', expected: 'https://energie-profis.de/' },
|
||||
{ path: '/solar', expected: 'https://energie-profis.de/solar' },
|
||||
{ path: '/wind', expected: 'https://energie-profis.de/wind' },
|
||||
{ path: '/installateur-finden', expected: 'https://energie-profis.de/installateur-finden' },
|
||||
{ path: '/kostenlose-beratung', expected: 'https://energie-profis.de/kostenlose-beratung' },
|
||||
{ path: '/unternehmen-listen', expected: 'https://energie-profis.de/unternehmen-listen' }
|
||||
];
|
||||
|
||||
for (const { path, expected } of pages) {
|
||||
await page.goto(path);
|
||||
|
||||
// Should have exactly one canonical link
|
||||
const canonicalLinks = await page.locator('link[rel="canonical"]').all();
|
||||
expect(canonicalLinks.length).toBe(1);
|
||||
|
||||
// Should have the correct href
|
||||
const href = await canonicalLinks[0].getAttribute('href');
|
||||
expect(href).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test('canonical should not have query parameters or fragments', async ({ page }) => {
|
||||
// Test with query parameters
|
||||
await page.goto('/?utm_source=test&ref=social');
|
||||
|
||||
const canonicalLinks = await page.locator('link[rel="canonical"]').all();
|
||||
expect(canonicalLinks.length).toBe(1);
|
||||
|
||||
const href = await canonicalLinks[0].getAttribute('href');
|
||||
expect(href).toBe('https://energie-profis.de/');
|
||||
expect(href).not.toContain('utm_source');
|
||||
expect(href).not.toContain('ref=');
|
||||
});
|
||||
|
||||
test('canonical should be absolute URL with https', async ({ page }) => {
|
||||
await page.goto('/solar');
|
||||
|
||||
const canonicalLink = await page.locator('link[rel="canonical"]').first();
|
||||
const href = await canonicalLink.getAttribute('href');
|
||||
|
||||
expect(href).toMatch(/^https:\/\//);
|
||||
expect(href).toContain('energie-profis.de');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Robots.txt AEO Compliance', () => {
|
||||
test('should allow PerplexityBot and GPTBot', async ({ page }) => {
|
||||
const response = await page.goto('/robots.txt');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
const content = await page.textContent('pre') || await page.textContent('body');
|
||||
|
||||
// Check for PerplexityBot
|
||||
expect(content).toContain('User-agent: PerplexityBot');
|
||||
expect(content).toContain('Allow: /');
|
||||
|
||||
// Check for GPTBot
|
||||
expect(content).toContain('User-agent: GPTBot');
|
||||
expect(content).toContain('Allow: /');
|
||||
|
||||
// Check for sitemap reference
|
||||
expect(content).toContain('Sitemap: https://energie-profis.de/sitemap.xml');
|
||||
|
||||
// Ensure no blanket disallow that would block AI bots
|
||||
expect(content).not.toMatch(/User-agent: \*[\s\S]*?Disallow: \//);
|
||||
});
|
||||
|
||||
test('should maintain existing bot permissions', async ({ page }) => {
|
||||
const response = await page.goto('/robots.txt');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
const content = await page.textContent('pre') || await page.textContent('body');
|
||||
|
||||
// Check that existing bots are still allowed
|
||||
expect(content).toContain('User-agent: Googlebot');
|
||||
expect(content).toContain('User-agent: Bingbot');
|
||||
expect(content).toContain('User-agent: Twitterbot');
|
||||
expect(content).toContain('User-agent: facebookexternalhit');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('JSON-LD Schema Validation', () => {
|
||||
test('homepage should have valid Organization and WebSite schema', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Find JSON-LD script tags
|
||||
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all();
|
||||
expect(jsonLdScripts.length).toBeGreaterThan(0);
|
||||
|
||||
let hasWebSite = false;
|
||||
let hasOrganization = false;
|
||||
|
||||
for (const script of jsonLdScripts) {
|
||||
const content = await script.textContent();
|
||||
if (content) {
|
||||
const data = JSON.parse(content);
|
||||
|
||||
if (data['@type'] === 'WebSite') {
|
||||
hasWebSite = true;
|
||||
expect(data.name).toBe('EnergieProfis');
|
||||
expect(data.url).toMatch(/energie-profis\.de/);
|
||||
expect(data.potentialAction).toBeDefined();
|
||||
expect(data.potentialAction['@type']).toBe('SearchAction');
|
||||
}
|
||||
|
||||
if (data['@type'] === 'Organization') {
|
||||
hasOrganization = true;
|
||||
expect(data.name).toBe('EnergieProfis');
|
||||
expect(data.logo).toBeDefined();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(hasWebSite).toBe(true);
|
||||
expect(hasOrganization).toBe(true);
|
||||
});
|
||||
|
||||
test('FAQ sections should have valid FAQPage schema', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Look for FAQ content on homepage
|
||||
const faqSection = await page.locator('text=Häufige Fragen').first();
|
||||
if (await faqSection.isVisible()) {
|
||||
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all();
|
||||
|
||||
let hasFAQPage = false;
|
||||
for (const script of jsonLdScripts) {
|
||||
const content = await script.textContent();
|
||||
if (content) {
|
||||
const data = JSON.parse(content);
|
||||
|
||||
if (data['@type'] === 'FAQPage') {
|
||||
hasFAQPage = true;
|
||||
expect(data.mainEntity).toBeInstanceOf(Array);
|
||||
expect(data.mainEntity.length).toBeGreaterThan(0);
|
||||
|
||||
// Validate each FAQ item
|
||||
for (const item of data.mainEntity) {
|
||||
expect(item['@type']).toBe('Question');
|
||||
expect(item.name).toBeDefined();
|
||||
expect(item.acceptedAnswer).toBeDefined();
|
||||
expect(item.acceptedAnswer['@type']).toBe('Answer');
|
||||
expect(item.acceptedAnswer.text).toBeDefined();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FAQ schema should exist if FAQ content is present
|
||||
if (await faqSection.isVisible()) {
|
||||
expect(hasFAQPage).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('all JSON-LD should be valid JSON', async ({ page }) => {
|
||||
const pages = ['/', '/solar', '/wind', '/installateur-finden'];
|
||||
|
||||
for (const pagePath of pages) {
|
||||
await page.goto(pagePath);
|
||||
|
||||
const jsonLdScripts = await page.locator('script[type="application/ld+json"]').all();
|
||||
|
||||
for (const script of jsonLdScripts) {
|
||||
const content = await script.textContent();
|
||||
if (content) {
|
||||
// This should not throw if JSON is valid
|
||||
expect(() => JSON.parse(content)).not.toThrow();
|
||||
|
||||
const data = JSON.parse(content);
|
||||
expect(data['@context']).toBe('https://schema.org');
|
||||
expect(data['@type']).toBeDefined();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Sitemap XML Validation', () => {
|
||||
test('should serve valid sitemap.xml', async ({ page }) => {
|
||||
const response = await page.goto('/sitemap.xml');
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(response?.headers()['content-type']).toMatch(/xml/);
|
||||
|
||||
const content = await page.textContent('urlset') || await page.textContent('body');
|
||||
|
||||
// Should be valid XML
|
||||
expect(content).toContain('<?xml version="1.0" encoding="UTF-8"?>');
|
||||
expect(content).toContain('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">');
|
||||
expect(content).toContain('</urlset>');
|
||||
});
|
||||
|
||||
test('should include all main pages with lastmod', async ({ page }) => {
|
||||
await page.goto('/sitemap.xml');
|
||||
|
||||
const content = await page.textContent('urlset') || await page.textContent('body');
|
||||
|
||||
const expectedUrls = [
|
||||
'https://energie-profis.de/',
|
||||
'https://energie-profis.de/solar',
|
||||
'https://energie-profis.de/wind',
|
||||
'https://energie-profis.de/installateur-finden',
|
||||
'https://energie-profis.de/kostenlose-beratung',
|
||||
'https://energie-profis.de/unternehmen-listen'
|
||||
];
|
||||
|
||||
for (const url of expectedUrls) {
|
||||
expect(content).toContain(`<loc>${url}</loc>`);
|
||||
}
|
||||
|
||||
// Check for lastmod tags
|
||||
expect(content).toMatch(/<lastmod>\d{4}-\d{2}-\d{2}<\/lastmod>/);
|
||||
});
|
||||
|
||||
test('should have proper priority and changefreq values', async ({ page }) => {
|
||||
await page.goto('/sitemap.xml');
|
||||
|
||||
const content = await page.textContent('urlset') || await page.textContent('body');
|
||||
|
||||
// Homepage should have highest priority
|
||||
expect(content).toMatch(/https:\/\/energie-profis\.de\/<\/loc>[\s\S]*?<priority>1\.0<\/priority>/);
|
||||
|
||||
// Should have valid changefreq values
|
||||
const validChangeFreqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'];
|
||||
const changefreqMatches = content.match(/<changefreq>([^<]+)<\/changefreq>/g) || [];
|
||||
|
||||
for (const match of changefreqMatches) {
|
||||
const value = match.replace(/<\/?changefreq>/g, '');
|
||||
expect(validChangeFreqs).toContain(value);
|
||||
}
|
||||
});
|
||||
|
||||
test('should have valid date format for lastmod', async ({ page }) => {
|
||||
await page.goto('/sitemap.xml');
|
||||
|
||||
const content = await page.textContent('urlset') || await page.textContent('body');
|
||||
|
||||
// Extract all lastmod dates
|
||||
const lastmodMatches = content.match(/<lastmod>([^<]+)<\/lastmod>/g) || [];
|
||||
expect(lastmodMatches.length).toBeGreaterThan(0);
|
||||
|
||||
for (const match of lastmodMatches) {
|
||||
const dateString = match.replace(/<\/?lastmod>/g, '');
|
||||
|
||||
// Should be in YYYY-MM-DD format (ISO date)
|
||||
expect(dateString).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
|
||||
// Should be a valid date
|
||||
const date = new Date(dateString);
|
||||
expect(date.toString()).not.toBe('Invalid Date');
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue