main repo

This commit is contained in:
Basilosaurusrex
2025-11-24 18:09:40 +01:00
parent b636ee5e70
commit f027651f9b
34146 changed files with 4436636 additions and 0 deletions

BIN
app/.DS_Store vendored Normal file

Binary file not shown.

107
app/admin-login/page.tsx Normal file
View File

@@ -0,0 +1,107 @@
"use client";
import { useState } from 'react';
import { supabase } from '@/lib/supabase';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useRouter } from 'next/navigation';
import { useToast } from '@/hooks/use-toast';
export default function AdminLoginPage() {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const { toast } = useToast();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${window.location.origin}/kunden-projekte`
}
});
if (error) {
console.error('❌ Login error:', error);
toast({
title: "Fehler",
description: "Login fehlgeschlagen. Bitte versuchen Sie es erneut.",
variant: "destructive",
});
return;
}
toast({
title: "E-Mail gesendet",
description: "Prüfen Sie Ihre E-Mail für den Login-Link.",
});
} catch (error) {
console.error('❌ Error:', error);
toast({
title: "Fehler",
description: "Ein unerwarteter Fehler ist aufgetreten.",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
return (
<div className="container mx-auto p-6">
<div className="max-w-md mx-auto">
<div className="mb-6 text-center">
<h1 className="text-3xl font-bold mb-2">Admin Login</h1>
<p className="text-gray-600">
Melden Sie sich an, um auf die Admin-Funktionen zuzugreifen
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Administrator-Zugang</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">E-Mail-Adresse</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="admin@beispiel.de"
required
/>
</div>
<Button
type="submit"
disabled={loading}
className="w-full"
>
{loading ? 'Sende E-Mail...' : 'E-Mail senden'}
</Button>
</form>
<div className="mt-4 text-center">
<Button
variant="outline"
onClick={() => router.push('/kunden-projekte')}
className="w-full"
>
Direkt zu Kunden-Projekte (Test-Modus)
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

65
app/agb/page.tsx Normal file
View File

@@ -0,0 +1,65 @@
"use client";
import WebklarLogoHeader from '@/components/WebklarLogoHeader';
import WebklarFooter from '@/components/WebklarFooter';
export default function AgbPage() {
return (
<div className="min-h-screen flex flex-col bg-[#FEFAE0]">
<WebklarLogoHeader />
<div className="flex-1 flex items-center justify-center p-4">
<div className="max-w-3xl w-full bg-white/80 rounded-2xl shadow-2xl p-8 mx-auto">
<h1 className="text-2xl font-bold mb-6 text-center">Allgemeine Geschäftsbedingungen (AGB)</h1>
<div className="space-y-6 text-gray-800 text-sm">
<div>
<h2 className="text-lg font-semibold mb-2">Geltung</h2>
<p>Diese AGB gelten für alle Dienstleistungen, die Webklar gegenüber Geschäftskunden (B2B) erbringt insbesondere Webentwicklung, Hosting, Wartung und SEO.</p>
</div>
<div>
<h2 className="text-lg font-semibold mb-2">Vertrag</h2>
<p>Ein Vertrag entsteht durch schriftliche oder digitale Bestätigung (z. B. E-Mail). Absprachen sind verbindlich, sobald sie von beiden Seiten akzeptiert wurden.</p>
</div>
<div>
<h2 className="text-lg font-semibold mb-2">Leistungen</h2>
<p>Die Leistungen richten sich nach individueller Absprache. Änderungen oder Zusatzleistungen müssen gesondert vereinbart werden.</p>
</div>
<div>
<h2 className="text-lg font-semibold mb-2">Zahlung</h2>
<p>Zahlungen sind innerhalb von 14 Tagen nach Rechnungsstellung fällig, sofern nichts anderes vereinbart wurde. Alle Preise zzgl. gesetzlicher MwSt.</p>
</div>
<div>
<h2 className="text-lg font-semibold mb-2">Inhalte</h2>
<p>Für bereitgestellte Inhalte (Texte, Bilder etc.) ist der Kunde verantwortlich. Webklar übernimmt keine Haftung für etwaige Urheberrechtsverletzungen durch Kundenmaterial.</p>
</div>
<div>
<h2 className="text-lg font-semibold mb-2">Haftung</h2>
<p>Webklar haftet nur für grob fahrlässige oder vorsätzliche Pflichtverletzungen. Für technische Störungen wird keine Garantie übernommen, es sei denn, sie wurden durch Webklar verursacht.</p>
</div>
<div>
<h2 className="text-lg font-semibold mb-2">Rechte</h2>
<p>Die Rechte an entwickelten Designs und Quellcodes bleiben bis zur vollständigen Bezahlung bei Webklar. Danach erhält der Kunde ein einfaches Nutzungsrecht.</p>
</div>
<div>
<h2 className="text-lg font-semibold mb-2">Datenschutz</h2>
<p>Kundendaten werden nur im Rahmen der DSGVO verarbeitet. Eine separate Datenschutzerklärung wird zur Verfügung gestellt.</p>
</div>
<div>
<h2 className="text-lg font-semibold mb-2">Gerichtsstand</h2>
<p>Gerichtsstand ist, soweit gesetzlich zulässig, der Sitz des Unternehmens Webklar.</p>
</div>
</div>
</div>
</div>
<WebklarFooter />
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { prompt } = body;
const response = await fetch('https://codestral.mistral.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ZMW8yHscLYI6iND4dh7cGuTmpO9Guotm'
},
body: JSON.stringify({
model: "mistral-small",
messages: [
{
role: "user",
content: prompt
}
],
temperature: 0.7,
max_tokens: 400
})
});
if (!response.ok) {
const errorText = await response.text();
console.error('Mistral API Error:', errorText);
return NextResponse.json(
{ error: `API request failed: ${response.status} - ${errorText}` },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error('Proxy error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,49 @@
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
const next = requestUrl.searchParams.get('next') ?? '/success'
if (code) {
const supabase = createRouteHandlerClient({ cookies })
try {
// Exchange the code for a session
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
if (error) {
console.error('Auth callback error:', error)
// Redirect to auth page with error
return NextResponse.redirect(
`${requestUrl.origin}/auth?error=auth_callback_error&error_description=${encodeURIComponent(error.message)}`
)
}
if (data.session) {
// Successfully authenticated
console.log('User authenticated successfully:', data.user?.email)
// Check if this is an appointment booking verification
const userMetadata = data.user?.user_metadata
if (userMetadata?.appointment_booking) {
// Redirect to success page for appointment booking
return NextResponse.redirect(`${requestUrl.origin}/success`)
} else {
// Redirect to admin dashboard or default page
return NextResponse.redirect(`${requestUrl.origin}/kunden-projekte`)
}
}
} catch (error) {
console.error('Unexpected error in auth callback:', error)
return NextResponse.redirect(
`${requestUrl.origin}/auth?error=unexpected_error&error_description=${encodeURIComponent('Ein unerwarteter Fehler ist aufgetreten.')}`
)
}
}
// If no code, redirect to auth page
return NextResponse.redirect(`${requestUrl.origin}/auth`)
}

43
app/auth/page.tsx Normal file
View File

@@ -0,0 +1,43 @@
"use client";
import { useSearchParams } from 'next/navigation'
import EmailAuth from '@/components/MagicLinkAuth'
import { AlertCircle } from 'lucide-react'
import { colors } from '@/lib/colors'
export default function AuthPage() {
const searchParams = useSearchParams()
const error = searchParams.get('error')
const errorDescription = searchParams.get('error_description')
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="w-full max-w-md">
{/* Error Display */}
{error && (
<div className="mb-6 p-4 rounded-xl border-2 border-red-500 bg-red-50">
<div className="flex items-center space-x-3 mb-2">
<AlertCircle className="w-5 h-5 text-red-600" />
<h3 className="font-semibold text-red-700">
Authentifizierungsfehler
</h3>
</div>
<p className="text-sm text-red-600 mb-2">
{errorDescription || 'Ein Fehler ist bei der Authentifizierung aufgetreten.'}
</p>
<div className="text-xs text-red-500">
<p><strong>Fehler:</strong> {error}</p>
{error === 'otp_expired' && (
<p className="mt-2">
💡 <strong>Tipp:</strong> Der E-Mail-Link ist abgelaufen. Bitte fordern Sie einen neuen Link an.
</p>
)}
</div>
</div>
)}
<EmailAuth />
</div>
</div>
)
}

25
app/datenschutz/page.tsx Normal file
View File

@@ -0,0 +1,25 @@
"use client";
import WebklarLogoHeader from '@/components/WebklarLogoHeader';
import WebklarFooter from '@/components/WebklarFooter';
export default function DatenschutzPage() {
return (
<div className="min-h-screen flex flex-col bg-[#FEFAE0]">
<WebklarLogoHeader />
<div className="flex-1 flex items-center justify-center p-4">
<div className="max-w-xl w-full bg-white/80 rounded-2xl shadow-2xl p-8 mx-auto">
<h1 className="text-2xl font-bold mb-6 text-center">Datenschutzerklärung</h1>
<div className="space-y-4 text-gray-800 text-sm">
<p><b>1. Allgemeines</b><br/>Wir nehmen den Schutz Ihrer Daten ernst. Diese Website verarbeitet personenbezogene Daten nur, soweit dies technisch und organisatorisch notwendig ist.</p>
<p><b>2. Kontaktformular</b><br/>Wenn Sie uns per Formular Anfragen zukommen lassen, werden Ihre Angaben aus dem Formular inklusive der von Ihnen dort angegebenen Kontaktdaten zur Bearbeitung der Anfrage und für den Fall von Anschlussfragen bei uns gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung weiter.</p>
<p><b>3. Hosting</b><br/>Unsere Website wird bei Netlify gehostet. Netlify verarbeitet personenbezogene Daten im Rahmen der Auftragsverarbeitung. Weitere Informationen finden Sie in der Datenschutzerklärung von Netlify.</p>
<p><b>4. Cookies & externe Fonts</b><br/>Wir verwenden keine Cookies und keine externen Schriftarten.</p>
<p><b>5. Trackingtools</b><br/>Optional: Wir nutzen datenschutzfreundliche Analyse-Tools wie Plausible. Es werden keine personenbezogenen Daten gespeichert.</p>
<p><b>6. Ihre Rechte</b><br/>Sie haben jederzeit das Recht auf Auskunft, Berichtigung oder Löschung Ihrer gespeicherten Daten.</p>
</div>
</div>
</div>
<WebklarFooter />
</div>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

261
app/globals.css Normal file
View File

@@ -0,0 +1,261 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--secondary-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/* Enhanced Glassmorphism */
.glass-enhanced {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 8px 32px 0 rgba(31, 38, 135, 0.37),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
/* Smooth Hover Effects */
.hover-lift {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.hover-lift:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
/* Subtle Parallax */
.parallax-bg {
transform: translateZ(0);
will-change: transform;
}
/* Static Gradient Background */
.animated-gradient {
background: linear-gradient(-45deg, #22c55e, #16a34a, #15803d, #166534);
background-size: 400% 400%;
}
/* Pulse Glow */
.pulse-glow {
animation: pulseGlow 2s ease-in-out infinite alternate;
}
@keyframes pulseGlow {
from { box-shadow: 0 0 20px rgba(34, 197, 94, 0.3); }
to { box-shadow: 0 0 40px rgba(34, 197, 94, 0.6); }
}
/* Smooth Scroll Behavior */
html {
scroll-behavior: smooth;
}
/* Enhanced Button Hover */
.btn-enhanced {
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.btn-enhanced::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.btn-enhanced:hover::before {
left: 100%;
}
/* Text Glow Effect */
.text-glow {
text-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
}
/* Card Hover Effect */
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: scale(1.02);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
}
/* Spinning Numbers Animation */
.spinning-number {
position: relative;
font-size: 0.8em; /* Slightly larger than original but smaller than before */
width: 100%;
height: 100%;
min-height: 350px; /* Slightly larger than original */
display: flex;
align-items: center;
justify-content: center;
}
@media (min-width: 768px) {
.spinning-number {
font-size: 1em; /* Medium size on desktop */
min-height: 400px;
}
}
.spinning-number .wheel {
animation: spinning-number-spin var(--t) linear infinite var(--r1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@keyframes spinning-number-spin {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.spinning-number .number {
position: absolute;
transform: translate(-50%, -50%) rotate(var(--a)) translateY(calc(var(--l) * -1)) scale(var(--s));
}
.spinning-number .number::before {
content: '1';
--z: 2;
transform: translate(-50%, -50%);
animation: spinning-number-changing calc(var(--t) * var(--z))
calc(-1 * var(--z) * var(--t) * var(--i) / var(--m) - 60s) linear infinite var(--r);
}
@keyframes spinning-number-changing {
0% {
content: '1';
}
to {
content: '0';
}
}
/* LogoLoop Container - overflow visible beim Hover für Text */
.logo-loop-container {
overflow: visible;
}
.logo-loop-inner {
overflow: hidden;
}
.logo-loop-container:hover .logo-loop-inner {
overflow: visible;
}
/* LogoLoop Fade mit Hintergrundfarbe - Gradient vom Hero-Bereich */
/* Links: helleres Grün (secondary), Rechts: dunkleres Grün (primary) */
.logo-loop-fade.logoloop--fade::before {
background: linear-gradient(
to right,
rgba(129, 144, 103, 0.9) 0%,
rgba(129, 144, 103, 0.7) 30%,
transparent 100%
) !important;
}
.logo-loop-fade.logoloop--fade::after {
background: linear-gradient(
to left,
rgba(10, 64, 12, 0.9) 0%,
rgba(10, 64, 12, 0.7) 30%,
transparent 100%
) !important;
}

61
app/impressum/page.tsx Normal file
View File

@@ -0,0 +1,61 @@
"use client";
import WebklarLogoHeader from '@/components/WebklarLogoHeader';
import WebklarFooter from '@/components/WebklarFooter';
export default function ImpressumPage() {
return (
<div className="min-h-screen flex flex-col bg-[#FEFAE0]">
<WebklarLogoHeader />
<div className="flex-1 flex items-center justify-center p-4">
<div className="max-w-2xl w-full bg-white/80 rounded-2xl shadow-2xl p-8 mx-auto">
<h1 className="text-2xl font-bold mb-6 text-center">Impressum</h1>
<div className="space-y-4 text-gray-800">
<div className="mb-6">
<h2 className="text-lg font-semibold mb-2">Angaben gemäß § 5 TMG</h2>
</div>
<div>
<span className="font-semibold">Firmenname:</span> Webklar IT-Dienstleistungen GbR
</div>
<div>
<span className="font-semibold">Adresse:</span> Am Schwimmbad 10, 67722 Winnweiler
</div>
<div className="mt-4">
<span className="font-semibold">Vertreten durch:</span>
<div className="ml-4 mt-1">
<div>Justin Klein</div>
<div>Kenso Grimm</div>
</div>
</div>
<div className="mt-4">
<span className="font-semibold">Kontakt:</span>
<div className="ml-4 mt-1">
<div>Telefon: 0170 4969375</div>
<div>E-Mail: support@webklar.com</div>
</div>
</div>
<div className="mt-4">
<span className="font-semibold">Umsatzsteuer:</span>
<div className="ml-4 mt-1">
<div>Umsatzsteuer-Identifikationsnummer gemäß § 27a UStG: nicht vorhanden</div>
<div className="text-sm text-gray-600 mt-1">(Kleinunternehmerregelung)</div>
</div>
</div>
<div className="mt-4">
<span className="font-semibold">Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:</span>
<div className="ml-4 mt-1">
<div>Justin Klein & Kenso Grimm</div>
<div>Am Schwimmbad 10, 67722 Winnweiler</div>
</div>
</div>
</div>
</div>
</div>
<WebklarFooter />
</div>
);
}

35
app/kontakte/page.tsx Normal file
View File

@@ -0,0 +1,35 @@
"use client";
import WebklarLogoHeader from '@/components/WebklarLogoHeader';
import WebklarFooter from '@/components/WebklarFooter';
export default function KontaktePage() {
return (
<div className="min-h-screen flex flex-col bg-[#FEFAE0]">
<WebklarLogoHeader />
<div className="flex-1 flex items-center justify-center p-4">
<div className="max-w-3xl w-full mx-auto">
<h1 className="text-2xl font-bold mb-8 text-center">Kontakte</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Karte 1 */}
<div className="bg-white/80 rounded-2xl shadow-2xl p-6 flex flex-col items-center">
<div className="w-20 h-20 rounded-full bg-gray-200 mb-4 flex items-center justify-center text-3xl text-gray-400">👤</div>
<div className="font-bold text-lg mb-1">Kenso Grimm</div>
<div className="text-sm text-gray-600 mb-2">CEO & Webentwickler</div>
<div className="text-sm text-gray-800 mb-1">support@webklar.com</div>
<div className="text-sm text-gray-800">+49 176 23726355</div>
</div>
{/* Karte 2 */}
<div className="bg-white/80 rounded-2xl shadow-2xl p-6 flex flex-col items-center">
<div className="w-20 h-20 rounded-full bg-gray-200 mb-4 flex items-center justify-center text-3xl text-gray-400">👤</div>
<div className="font-bold text-lg mb-1">Justin Klein</div>
<div className="text-sm text-gray-600 mb-2">CEO & Kundenbetreuung</div>
<div className="text-sm text-gray-800 mb-1">support@webklar.com</div>
<div className="text-sm text-gray-800">+49 170 4969375</div>
</div>
</div>
</div>
</div>
<WebklarFooter />
</div>
);
}

385
app/kunden-liste/page.tsx Normal file
View File

@@ -0,0 +1,385 @@
'use client';
import React, { useState, useEffect } from 'react';
import { supabase } from '@/lib/supabaseClient';
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Search,
RefreshCw,
Building,
Calendar,
Users,
Globe,
Euro,
Clock,
ArrowLeft,
Plus,
Filter,
Download
} from "lucide-react";
import { colors } from "@/lib/colors";
interface KundenProjekt {
id: string;
erstellt_am: string;
firma: string;
beschreibung: string;
zielgruppe: string;
website_vorhanden: boolean;
stilvorbilder: string;
was_gefaellt_gefaellt_nicht: string;
ziel_der_website: string;
seiten_geplant: string;
texte_bilder_vorhanden: boolean;
fokus_inhalte: string;
logo_farben_vorhanden: boolean;
design_wunsch: string;
beispiellinks: string;
features_gewuenscht: string;
drittanbieter: string;
selbst_pflegen: boolean;
laufende_betreuung: boolean;
deadline: string;
projekt_verantwortlich: string;
budget: string;
kommunikationsweg: string;
feedback_geschwindigkeit: string;
}
export default function KundenListePage() {
const [kunden, setKunden] = useState<KundenProjekt[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
fetchKunden();
}, []);
async function fetchKunden() {
try {
setLoading(true);
const { data, error } = await supabase
.from('kunden_projekte')
.select('*')
.order('erstellt_am', { ascending: false });
if (error) {
setError('Fehler beim Laden der Daten: ' + error.message);
} else {
setKunden(data || []);
}
} catch (e: any) {
setError('Unbekannter Fehler: ' + e.message);
} finally {
setLoading(false);
}
}
const filteredKunden = kunden.filter(kunde =>
kunde.firma?.toLowerCase().includes(searchTerm.toLowerCase()) ||
kunde.beschreibung?.toLowerCase().includes(searchTerm.toLowerCase()) ||
kunde.zielgruppe?.toLowerCase().includes(searchTerm.toLowerCase())
);
function formatDate(dateString: string) {
return new Date(dateString).toLocaleDateString('de-DE');
}
function formatBoolean(value: boolean) {
return value ? 'Ja' : 'Nein';
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center !bg-[#FEFAE0]" style={{ backgroundColor: colors.background }}>
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 mx-auto mb-4" style={{ borderColor: colors.primary }}></div>
<p style={{ color: colors.primary }}>Lade Kundendaten...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center !bg-[#FEFAE0]" style={{ backgroundColor: colors.background }}>
<div className="text-center max-w-md mx-auto p-6 rounded-2xl shadow-lg" style={{ backgroundColor: colors.white }}>
<p className="mb-4" style={{ color: colors.primary }}>{error}</p>
<Button
onClick={fetchKunden}
className="rounded-full font-medium"
style={{
backgroundColor: colors.primary,
color: colors.background
}}
>
Erneut versuchen
</Button>
</div>
</div>
);
}
return (
<div className="min-h-screen !bg-[#FEFAE0]" style={{ backgroundColor: colors.background }}>
{/* Header Section */}
<div className="relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-10">
<div className="absolute inset-0" style={{
background: `linear-gradient(135deg, ${colors.primary}20, ${colors.secondary}20)`
}}></div>
</div>
<div className="relative z-10 px-4 sm:px-8 py-8 max-w-7xl mx-auto">
{/* Navigation */}
<div className="flex items-center justify-between mb-8">
<Button
variant="ghost"
className="rounded-full font-medium hover:scale-105 transition-all duration-300"
style={{ color: colors.primary }}
onClick={() => window.history.back()}
>
<ArrowLeft className="w-4 h-4 mr-2" />
Zurück
</Button>
<div className="text-center">
<h1 className="text-3xl sm:text-4xl font-bold mb-2" style={{ color: colors.primary }}>
Kundenprojekte
</h1>
<p className="text-sm" style={{ color: colors.secondary }}>
Übersicht aller Kundenprojekte und deren Status
</p>
</div>
<Button
className="rounded-full font-medium px-4 py-2 shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105"
style={{
backgroundColor: colors.primary,
color: colors.background
}}
>
<Plus className="w-4 h-4 mr-2" />
Neues Projekt
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div className="p-6 rounded-2xl shadow-lg backdrop-blur-sm" style={{ backgroundColor: `${colors.white}CC` }}>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium" style={{ color: colors.secondary }}>Gesamt Projekte</p>
<p className="text-2xl font-bold" style={{ color: colors.primary }}>{kunden.length}</p>
</div>
<div className="p-3 rounded-full" style={{ backgroundColor: colors.primary }}>
<Building className="w-6 h-6" style={{ color: colors.background }} />
</div>
</div>
</div>
<div className="p-6 rounded-2xl shadow-lg backdrop-blur-sm" style={{ backgroundColor: `${colors.white}CC` }}>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium" style={{ color: colors.secondary }}>Aktive Projekte</p>
<p className="text-2xl font-bold" style={{ color: colors.primary }}>
{kunden.filter(k => k.laufende_betreuung).length}
</p>
</div>
<div className="p-3 rounded-full" style={{ backgroundColor: colors.secondary }}>
<Clock className="w-6 h-6" style={{ color: colors.background }} />
</div>
</div>
</div>
<div className="p-6 rounded-2xl shadow-lg backdrop-blur-sm" style={{ backgroundColor: `${colors.white}CC` }}>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium" style={{ color: colors.secondary }}>Neue diese Woche</p>
<p className="text-2xl font-bold" style={{ color: colors.primary }}>
{kunden.filter(k => {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
return new Date(k.erstellt_am) > weekAgo;
}).length}
</p>
</div>
<div className="p-3 rounded-full" style={{ backgroundColor: colors.tertiary }}>
<Calendar className="w-6 h-6" style={{ color: colors.primary }} />
</div>
</div>
</div>
<div className="p-6 rounded-2xl shadow-lg backdrop-blur-sm" style={{ backgroundColor: `${colors.white}CC` }}>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium" style={{ color: colors.secondary }}>Durchschnitt Budget</p>
<p className="text-2xl font-bold" style={{ color: colors.primary }}>
{(() => {
const budgets = kunden
.filter(k => k.budget)
.map(k => parseInt(k.budget.replace(/[^\d]/g, '')))
.filter(b => !isNaN(b));
return budgets.length > 0
? `${Math.round(budgets.reduce((a, b) => a + b, 0) / budgets.length)}`
: '-';
})()}
</p>
</div>
<div className="p-3 rounded-full" style={{ backgroundColor: colors.primary }}>
<Euro className="w-6 h-6" style={{ color: colors.background }} />
</div>
</div>
</div>
</div>
{/* Search and Filters */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5" style={{ color: colors.secondary }} />
<input
type="text"
placeholder="Nach Firma, Beschreibung oder Zielgruppe suchen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 rounded-full border-2 focus:outline-none focus:ring-2 transition-all duration-300"
style={{
borderColor: colors.secondary,
backgroundColor: colors.white,
color: colors.primary
}}
/>
</div>
<div className="flex gap-2">
<Button
variant="outline"
className="rounded-full"
style={{
borderColor: colors.secondary,
color: colors.secondary,
backgroundColor: colors.white
}}
>
<Filter className="w-4 h-4 mr-2" />
Filter
</Button>
<Button
onClick={fetchKunden}
className="rounded-full font-medium"
style={{
backgroundColor: colors.secondary,
color: colors.background
}}
>
<RefreshCw className="w-4 h-4 mr-2" />
Aktualisieren
</Button>
<Button
variant="outline"
className="rounded-full"
style={{
borderColor: colors.secondary,
color: colors.secondary,
backgroundColor: colors.white
}}
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
</div>
</div>
{/* Results */}
<div className="rounded-2xl shadow-lg overflow-hidden" style={{ backgroundColor: colors.white }}>
{filteredKunden.length === 0 ? (
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center" style={{ backgroundColor: colors.secondary }}>
<Building className="w-8 h-8" style={{ color: colors.background }} />
</div>
<p className="text-lg font-medium mb-2" style={{ color: colors.primary }}>
{searchTerm ? 'Keine Kunden gefunden' : 'Noch keine Kundenprojekte vorhanden'}
</p>
<p className="text-sm" style={{ color: colors.secondary }}>
{searchTerm ? 'Versuchen Sie einen anderen Suchbegriff.' : 'Erstellen Sie Ihr erstes Kundenprojekt.'}
</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr style={{ backgroundColor: colors.background }}>
<th className="text-left p-4 font-semibold" style={{ color: colors.primary }}>Firma</th>
<th className="text-left p-4 font-semibold" style={{ color: colors.primary }}>Erstellt am</th>
<th className="text-left p-4 font-semibold" style={{ color: colors.primary }}>Zielgruppe</th>
<th className="text-left p-4 font-semibold" style={{ color: colors.primary }}>Website vorhanden</th>
<th className="text-left p-4 font-semibold" style={{ color: colors.primary }}>Deadline</th>
<th className="text-left p-4 font-semibold" style={{ color: colors.primary }}>Budget</th>
<th className="text-left p-4 font-semibold" style={{ color: colors.primary }}>Status</th>
</tr>
</thead>
<tbody>
{filteredKunden.map((kunde, index) => (
<tr
key={kunde.id}
className="border-b transition-all duration-200 hover:scale-[1.01] cursor-pointer"
style={{
borderColor: colors.background,
backgroundColor: index % 2 === 0 ? colors.white : colors.background
}}
>
<td className="p-4">
<div className="font-medium" style={{ color: colors.primary }}>{kunde.firma}</div>
<div className="text-sm truncate max-w-xs" style={{ color: colors.secondary }}>
{kunde.beschreibung}
</div>
</td>
<td className="p-4 text-sm" style={{ color: colors.secondary }}>
{formatDate(kunde.erstellt_am)}
</td>
<td className="p-4 text-sm" style={{ color: colors.secondary }}>
{kunde.zielgruppe}
</td>
<td className="p-4 text-sm" style={{ color: colors.secondary }}>
{formatBoolean(kunde.website_vorhanden)}
</td>
<td className="p-4 text-sm" style={{ color: colors.secondary }}>
{kunde.deadline ? formatDate(kunde.deadline) : '-'}
</td>
<td className="p-4 text-sm" style={{ color: colors.secondary }}>
{kunde.budget || '-'}
</td>
<td className="p-4">
<Badge
className="rounded-full font-medium"
style={{
backgroundColor: kunde.laufende_betreuung ? colors.secondary : colors.tertiary,
color: kunde.laufende_betreuung ? colors.background : colors.primary
}}
>
{kunde.laufende_betreuung ? 'Aktiv' : 'In Bearbeitung'}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Footer */}
<div className="mt-6 text-center">
<p className="text-sm" style={{ color: colors.secondary }}>
{filteredKunden.length} von {kunden.length} Kundenprojekten
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,715 @@
"use client";
import { useState, useEffect } from 'react';
import { supabase } from '@/lib/supabase';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox';
import { useRouter } from 'next/navigation';
import {
Building,
User,
Phone,
Mail,
Calendar,
Target,
Globe,
Palette,
Settings,
FileText,
CheckCircle,
AlertCircle,
Play,
ArrowLeft,
Save,
Eye,
Edit,
Users,
TrendingUp,
Zap,
Star
} from 'lucide-react';
interface Customer {
id: string;
berater?: string;
firma?: string;
ansprechpartn?: string;
telefon?: string;
email?: string;
beschreibung?: string;
zielgruppe?: string;
website_vorha?: boolean;
was_gefaellt_c?: string;
ziel_der_websi?: string;
seiten_geplant?: string;
texte_bilder_v?: boolean;
fokus_inhalte?: string;
logo_farben_v?: boolean;
stilvorbilder?: string;
design_wunsch?: string;
features_gewu?: string;
drittanbieter?: string;
selbst_pflegen?: boolean;
laufende_betre?: boolean;
deadline?: string;
projekt_verant?: string;
budget?: string;
kommunikation?: string;
feedback_gesc?: string;
beispiellinks?: string;
benoetigte_fur?: string;
webseiten_ziel?: string;
geplante_seite?: string;
termin_datum?: string;
erstellt_am: string;
appointment_status?: 'pending' | 'running' | 'completed';
started_by?: string;
started_at?: string;
}
interface CustomerDetailPageProps {
params: {
id: string;
};
}
export default function CustomerDetailPage({ params }: CustomerDetailPageProps) {
const [customer, setCustomer] = useState<Customer | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState<any>({});
const [activeSection, setActiveSection] = useState('overview');
const router = useRouter();
useEffect(() => {
fetchCustomer();
}, [params.id]);
const fetchCustomer = async () => {
try {
console.log('🔍 Fetching customer details:', params.id);
const { data, error } = await supabase
.from('kunden_projekte')
.select('*')
.eq('id', params.id)
.single();
if (error) {
console.error('❌ Error fetching customer:', error);
return;
}
console.log('✅ Customer details fetched:', data);
setCustomer(data);
setFormData(data);
} catch (error) {
console.error('❌ Error:', error);
} finally {
setLoading(false);
}
};
const updateAppointmentStatus = async (status: 'pending' | 'running' | 'completed') => {
try {
console.log(`🔄 Updating appointment status to ${status}`);
const updateData: any = { appointment_status: status };
if (status === 'running') {
updateData.started_by = 'admin@example.com';
updateData.started_at = new Date().toISOString();
} else if (status === 'completed') {
updateData.completed_at = new Date().toISOString();
}
const { error } = await supabase
.from('kunden_projekte')
.update(updateData)
.eq('id', params.id);
if (error) {
console.error('❌ Error updating status:', error);
return;
}
console.log('✅ Status updated successfully');
fetchCustomer();
} catch (error) {
console.error('❌ Error:', error);
}
};
const saveCustomerData = async () => {
setSaving(true);
try {
console.log('💾 Saving customer data:', formData);
const { error } = await supabase
.from('kunden_projekte')
.update(formData)
.eq('id', params.id);
if (error) {
console.error('❌ Error updating customer:', error);
return;
}
console.log('✅ Customer updated successfully');
fetchCustomer();
} catch (error) {
console.error('❌ Error:', error);
} finally {
setSaving(false);
}
};
const handleInputChange = (field: string, value: any) => {
setFormData((prev: any) => ({
...prev,
[field]: value
}));
};
const getStatusBadge = (status: string, startedBy?: string) => {
switch (status) {
case 'pending':
return (
<Badge variant="secondary" className="flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Termin wartet
</Badge>
);
case 'running':
return (
<div className="flex items-center gap-2">
<Badge variant="default" className="bg-green-500 flex items-center gap-1">
<Play className="w-3 h-3" />
Termin läuft
</Badge>
{startedBy && (
<span className="text-sm text-gray-600">({startedBy})</span>
)}
</div>
);
case 'completed':
return (
<Badge variant="outline" className="flex items-center gap-1">
<CheckCircle className="w-3 h-3" />
Termin abgeschlossen
</Badge>
);
default:
return (
<Badge variant="secondary" className="flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Termin wartet
</Badge>
);
}
};
const getCompletionStatus = () => {
if (!customer) return { percentage: 0, filledFields: 0, totalFields: 0 };
const fields = [
customer.firma, customer.ansprechpartn, customer.telefon, customer.email,
customer.beschreibung, customer.zielgruppe, customer.ziel_der_websi,
customer.seiten_geplant, customer.fokus_inhalte, customer.design_wunsch,
customer.features_gewu, customer.budget, customer.deadline
];
const filledFields = fields.filter(field => field && field.trim() !== '').length;
const totalFields = fields.length;
const percentage = Math.round((filledFields / totalFields) * 100);
return { filledFields, totalFields, percentage };
};
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-6">
<div className="max-w-7xl mx-auto">
<div className="text-center">
<h1 className="text-3xl font-bold mb-4 text-gray-800">Kunden-Projekt</h1>
<div className="animate-spin rounded-full h-12 w-12 border-b-4 border-blue-500 border-t-transparent mx-auto"></div>
<p className="mt-4 text-gray-600">Lade Projektdaten...</p>
</div>
</div>
</div>
);
}
if (!customer) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-6">
<div className="max-w-7xl mx-auto">
<div className="text-center">
<h1 className="text-3xl font-bold mb-4 text-gray-800">Projekt nicht gefunden</h1>
<p className="text-gray-600 mb-6">Das angeforderte Projekt konnte nicht gefunden werden.</p>
<Button
onClick={() => router.push('/kunden-projekte')}
className="bg-blue-500 hover:bg-blue-600"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Zurück zur Übersicht
</Button>
</div>
</div>
</div>
);
}
const completion = getCompletionStatus();
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="max-w-7xl mx-auto p-6">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<Button
onClick={() => router.push('/kunden-projekte')}
variant="outline"
className="flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Zurück
</Button>
<div className="flex items-center gap-3">
<div className="p-3 bg-blue-500 rounded-xl">
<Building className="w-8 h-8 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-800">
{customer.ansprechpartn || customer.firma || 'Unbekannter Kunde'}
</h1>
<p className="text-gray-600">
{customer.firma && customer.ansprechpartn ? `${customer.firma} - ${customer.ansprechpartn}` : customer.firma || customer.ansprechpartn}
</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
{getStatusBadge(customer.appointment_status || 'pending', customer.started_by)}
{customer.appointment_status === 'pending' && (
<Button
onClick={() => updateAppointmentStatus('running')}
className="bg-green-500 hover:bg-green-600 flex items-center gap-2"
>
<Play className="w-4 h-4" />
Termin starten
</Button>
)}
{customer.appointment_status === 'running' && (
<Button
onClick={() => updateAppointmentStatus('completed')}
variant="outline"
className="flex items-center gap-2"
>
<CheckCircle className="w-4 h-4" />
Termin beenden
</Button>
)}
</div>
</div>
{/* Progress Bar */}
<Card className="bg-white/80 backdrop-blur-sm mb-6">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-800">Befragung Fortschritt</h3>
<span className="text-sm font-medium text-gray-600">{completion.percentage}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all duration-300 ${
completion.percentage >= 80 ? 'bg-green-500' :
completion.percentage >= 50 ? 'bg-yellow-500' : 'bg-blue-500'
}`}
style={{ width: `${completion.percentage}%` }}
></div>
</div>
<p className="text-sm text-gray-500 mt-2">
{completion.filledFields} von {completion.totalFields} Feldern ausgefüllt
</p>
</CardContent>
</Card>
</div>
{/* Navigation Tabs */}
<div className="flex gap-2 mb-6 overflow-x-auto">
{[
{ id: 'overview', label: 'Übersicht', icon: Eye },
{ id: 'contact', label: 'Kontaktdaten', icon: User },
{ id: 'project', label: 'Projektinfo', icon: FileText },
{ id: 'design', label: 'Design & Features', icon: Palette },
{ id: 'technical', label: 'Technische Details', icon: Settings },
{ id: 'timeline', label: 'Zeitplan & Budget', icon: Calendar }
].map((tab) => (
<Button
key={tab.id}
variant={activeSection === tab.id ? 'default' : 'outline'}
onClick={() => setActiveSection(tab.id)}
className="flex items-center gap-2 whitespace-nowrap"
>
<tab.icon className="w-4 h-4" />
{tab.label}
</Button>
))}
</div>
{/* Content Sections */}
<div className="grid gap-6">
{/* Overview Section */}
{activeSection === 'overview' && (
<div className="grid gap-6">
<Card className="bg-white/80 backdrop-blur-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building className="w-5 h-5" />
Projektübersicht
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-600">Firma</Label>
<p className="text-gray-800">{customer.firma || 'Nicht angegeben'}</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-600">Ansprechpartner</Label>
<p className="text-gray-800">{customer.ansprechpartn || 'Nicht angegeben'}</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-600">E-Mail</Label>
<p className="text-gray-800">{customer.email || 'Nicht angegeben'}</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-600">Telefon</Label>
<p className="text-gray-800">{customer.telefon || 'Nicht angegeben'}</p>
</div>
</div>
{customer.beschreibung && (
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-600">Projektbeschreibung</Label>
<p className="text-gray-800">{customer.beschreibung}</p>
</div>
)}
{customer.termin_datum && (
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-600">Termin</Label>
<p className="text-gray-800">{new Date(customer.termin_datum).toLocaleString('de-DE')}</p>
</div>
)}
</CardContent>
</Card>
</div>
)}
{/* Contact Section */}
{activeSection === 'contact' && (
<Card className="bg-white/80 backdrop-blur-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="w-5 h-5" />
Kontaktdaten
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firma">Firma *</Label>
<Input
id="firma"
value={formData.firma || ''}
onChange={(e) => handleInputChange('firma', e.target.value)}
placeholder="Firmenname"
/>
</div>
<div className="space-y-2">
<Label htmlFor="ansprechpartn">Ansprechpartner *</Label>
<Input
id="ansprechpartn"
value={formData.ansprechpartn || ''}
onChange={(e) => handleInputChange('ansprechpartn', e.target.value)}
placeholder="Name des Ansprechpartners"
/>
</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)}
placeholder="email@beispiel.de"
/>
</div>
<div className="space-y-2">
<Label htmlFor="telefon">Telefon</Label>
<Input
id="telefon"
type="tel"
value={formData.telefon || ''}
onChange={(e) => handleInputChange('telefon', e.target.value)}
placeholder="+49 123 456789"
/>
</div>
</div>
</CardContent>
</Card>
)}
{/* Project Section */}
{activeSection === 'project' && (
<Card className="bg-white/80 backdrop-blur-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="w-5 h-5" />
Projektinformationen
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="beschreibung">Projektbeschreibung *</Label>
<Textarea
id="beschreibung"
value={formData.beschreibung || ''}
onChange={(e) => handleInputChange('beschreibung', e.target.value)}
placeholder="Beschreiben Sie Ihr Projekt, Ihre Ziele und Anforderungen..."
rows={4}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="zielgruppe">Zielgruppe</Label>
<Input
id="zielgruppe"
value={formData.zielgruppe || ''}
onChange={(e) => handleInputChange('zielgruppe', e.target.value)}
placeholder="Ihre Zielgruppe"
/>
</div>
<div className="space-y-2">
<Label htmlFor="ziel_der_websi">Ziel der Website</Label>
<Input
id="ziel_der_websi"
value={formData.ziel_der_websi || ''}
onChange={(e) => handleInputChange('ziel_der_websi', e.target.value)}
placeholder="Hauptziel der Website"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="seiten_geplant">Geplante Seiten</Label>
<Input
id="seiten_geplant"
value={formData.seiten_geplant || ''}
onChange={(e) => handleInputChange('seiten_geplant', e.target.value)}
placeholder="z.B. Home, Über uns, Kontakt, Blog..."
/>
</div>
</CardContent>
</Card>
)}
{/* Design Section */}
{activeSection === 'design' && (
<Card className="bg-white/80 backdrop-blur-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="w-5 h-5" />
Design & Features
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="design_wunsch">Design-Wünsche</Label>
<Textarea
id="design_wunsch"
value={formData.design_wunsch || ''}
onChange={(e) => handleInputChange('design_wunsch', e.target.value)}
placeholder="Beschreiben Sie Ihre Design-Vorstellungen..."
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="stilvorbilder">Stilvorbilder</Label>
<Input
id="stilvorbilder"
value={formData.stilvorbilder || ''}
onChange={(e) => handleInputChange('stilvorbilder', e.target.value)}
placeholder="Websites die Ihnen gefallen"
/>
</div>
<div className="space-y-2">
<Label htmlFor="features_gewu">Gewünschte Features</Label>
<Textarea
id="features_gewu"
value={formData.features_gewu || ''}
onChange={(e) => handleInputChange('features_gewu', e.target.value)}
placeholder="Welche Funktionen soll die Website haben?"
rows={3}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center space-x-2">
<Checkbox
id="logo_farben_v"
checked={formData.logo_farben_v || false}
onCheckedChange={(checked) => handleInputChange('logo_farben_v', checked)}
/>
<Label htmlFor="logo_farben_v">Logo & Farben vorhanden</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="texte_bilder_v"
checked={formData.texte_bilder_v || false}
onCheckedChange={(checked) => handleInputChange('texte_bilder_v', checked)}
/>
<Label htmlFor="texte_bilder_v">Texte & Bilder vorhanden</Label>
</div>
</div>
</CardContent>
</Card>
)}
{/* Technical Section */}
{activeSection === 'technical' && (
<Card className="bg-white/80 backdrop-blur-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="w-5 h-5" />
Technische Details
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="drittanbieter">Drittanbieter-Integrationen</Label>
<Input
id="drittanbieter"
value={formData.drittanbieter || ''}
onChange={(e) => handleInputChange('drittanbieter', e.target.value)}
placeholder="z.B. Google Analytics, Newsletter, Zahlungssysteme..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="benoetigte_fur">Benötigte Funktionen</Label>
<Textarea
id="benoetigte_fur"
value={formData.benoetigte_fur || ''}
onChange={(e) => handleInputChange('benoetigte_fur', e.target.value)}
placeholder="Spezielle technische Anforderungen..."
rows={3}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center space-x-2">
<Checkbox
id="selbst_pflegen"
checked={formData.selbst_pflegen || false}
onCheckedChange={(checked) => handleInputChange('selbst_pflegen', checked)}
/>
<Label htmlFor="selbst_pflegen">Selbst pflegen möchten</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="laufende_betre"
checked={formData.laufende_betre || false}
onCheckedChange={(checked) => handleInputChange('laufende_betre', checked)}
/>
<Label htmlFor="laufende_betre">Laufende Betreuung gewünscht</Label>
</div>
</div>
</CardContent>
</Card>
)}
{/* Timeline Section */}
{activeSection === 'timeline' && (
<Card className="bg-white/80 backdrop-blur-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="w-5 h-5" />
Zeitplan & Budget
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="deadline">Deadline</Label>
<Input
id="deadline"
type="date"
value={formData.deadline || ''}
onChange={(e) => handleInputChange('deadline', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="budget">Budget</Label>
<Input
id="budget"
value={formData.budget || ''}
onChange={(e) => handleInputChange('budget', e.target.value)}
placeholder="Budget-Rahmen"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="projekt_verant">Projektverantwortlicher</Label>
<Input
id="projekt_verant"
value={formData.projekt_verant || ''}
onChange={(e) => handleInputChange('projekt_verant', e.target.value)}
placeholder="Name des Projektverantwortlichen"
/>
</div>
<div className="space-y-2">
<Label htmlFor="kommunikation">Kommunikationsweg</Label>
<Input
id="kommunikation"
value={formData.kommunikation || ''}
onChange={(e) => handleInputChange('kommunikation', e.target.value)}
placeholder="Bevorzugter Kommunikationsweg"
/>
</div>
</CardContent>
</Card>
)}
</div>
{/* Save Button */}
<div className="mt-8 flex justify-end">
<Button
onClick={saveCustomerData}
disabled={saving}
className="bg-blue-500 hover:bg-blue-600 flex items-center gap-2"
>
<Save className="w-4 h-4" />
{saving ? 'Speichern...' : 'Daten speichern'}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,477 @@
"use client";
import { useState, useEffect } from 'react';
import { supabase } from '@/lib/supabase';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useRouter } from 'next/navigation';
import {
Calendar,
User,
Building,
Phone,
Mail,
FileText,
Clock,
Play,
CheckCircle,
AlertCircle,
Users,
Target,
Globe,
Palette,
Settings,
TrendingUp
} from 'lucide-react';
interface Customer {
id: string;
berater?: string;
firma?: string;
ansprechpartn?: string;
telefon?: string;
email?: string;
beschreibung?: string;
zielgruppe?: string;
website_vorha?: boolean;
was_gefaellt_c?: string;
ziel_der_websi?: string;
seiten_geplant?: string;
texte_bilder_v?: boolean;
fokus_inhalte?: string;
logo_farben_v?: boolean;
stilvorbilder?: string;
design_wunsch?: string;
features_gewu?: string;
drittanbieter?: string;
selbst_pflegen?: boolean;
laufende_betre?: boolean;
deadline?: string;
projekt_verant?: string;
budget?: string;
kommunikation?: string;
feedback_gesc?: string;
beispiellinks?: string;
benoetigte_fur?: string;
webseiten_ziel?: string;
geplante_seite?: string;
termin_datum?: string;
erstellt_am: string;
appointment_status?: 'pending' | 'running' | 'completed';
started_by?: string;
started_at?: string;
}
export default function KundenProjektePage() {
const [customers, setCustomers] = useState<Customer[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const router = useRouter();
useEffect(() => {
fetchCustomers();
}, []);
const fetchCustomers = async () => {
try {
console.log('🔍 Fetching customers with appointments...');
const { data, error } = await supabase
.from('kunden_projekte')
.select('*')
.not('termin_datum', 'is', null)
.order('id', { ascending: false });
if (error) {
console.error('❌ Error fetching customers:', error);
return;
}
console.log('✅ Customers fetched:', data?.length || 0);
setCustomers(data || []);
} catch (error) {
console.error('❌ Error:', error);
} finally {
setLoading(false);
}
};
const updateAppointmentStatus = async (customerId: string, status: 'pending' | 'running' | 'completed') => {
try {
console.log(`🔄 Updating appointment status for ${customerId} to ${status}`);
const updateData: any = { appointment_status: status };
if (status === 'running') {
updateData.started_by = 'admin@example.com'; // TODO: Get from auth
updateData.started_at = new Date().toISOString();
} else if (status === 'completed') {
updateData.completed_at = new Date().toISOString();
}
const { error } = await supabase
.from('kunden_projekte')
.update(updateData)
.eq('id', customerId);
if (error) {
console.error('❌ Error updating status:', error);
return;
}
console.log('✅ Status updated successfully');
fetchCustomers(); // Refresh data
} catch (error) {
console.error('❌ Error:', error);
}
};
const getStatusBadge = (status: string, startedBy?: string) => {
switch (status) {
case 'pending':
return (
<Badge variant="secondary" className="flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Termin wartet
</Badge>
);
case 'running':
return (
<div className="flex items-center gap-2">
<Badge variant="default" className="bg-green-500 flex items-center gap-1">
<Play className="w-3 h-3" />
Termin läuft
</Badge>
{startedBy && (
<span className="text-sm text-gray-600">({startedBy})</span>
)}
</div>
);
case 'completed':
return (
<Badge variant="outline" className="flex items-center gap-1">
<CheckCircle className="w-3 h-3" />
Termin abgeschlossen
</Badge>
);
default:
return (
<Badge variant="secondary" className="flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
Termin wartet
</Badge>
);
}
};
const getStatusActions = (customer: Customer) => {
switch (customer.appointment_status) {
case 'pending':
return (
<Button
onClick={() => updateAppointmentStatus(customer.id, 'running')}
className="bg-green-500 hover:bg-green-600 flex items-center gap-2"
>
<Play className="w-4 h-4" />
Termin starten
</Button>
);
case 'running':
return (
<div className="flex gap-2">
<Button
onClick={() => router.push(`/kunden-projekte/${customer.id}`)}
className="bg-blue-500 hover:bg-blue-600 flex items-center gap-2"
>
<FileText className="w-4 h-4" />
Befragung
</Button>
<Button
onClick={() => updateAppointmentStatus(customer.id, 'completed')}
variant="outline"
className="flex items-center gap-2"
>
<CheckCircle className="w-4 h-4" />
Beenden
</Button>
</div>
);
case 'completed':
return (
<div className="flex gap-2">
<Button
onClick={() => router.push(`/kunden-projekte/${customer.id}`)}
variant="outline"
className="flex items-center gap-2"
>
<FileText className="w-4 h-4" />
Projekt anzeigen
</Button>
<Button
onClick={() => updateAppointmentStatus(customer.id, 'pending')}
variant="outline"
size="sm"
>
Zurücksetzen
</Button>
</div>
);
default:
return (
<Button
onClick={() => updateAppointmentStatus(customer.id, 'running')}
className="bg-green-500 hover:bg-green-600 flex items-center gap-2"
>
<Play className="w-4 h-4" />
Termin starten
</Button>
);
}
};
const getCompletionStatus = (customer: Customer) => {
const fields = [
customer.firma, customer.ansprechpartn, customer.telefon, customer.email,
customer.beschreibung, customer.zielgruppe, customer.ziel_der_websi,
customer.seiten_geplant, customer.fokus_inhalte, customer.design_wunsch,
customer.features_gewu, customer.budget, customer.deadline
];
const filledFields = fields.filter(field => field && field.trim() !== '').length;
const totalFields = fields.length;
const percentage = Math.round((filledFields / totalFields) * 100);
return { filledFields, totalFields, percentage };
};
const filteredCustomers = customers.filter(customer => {
const matchesSearch = (customer.ansprechpartn || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
(customer.firma || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
(customer.email || '').toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || customer.appointment_status === statusFilter;
return matchesSearch && matchesStatus;
});
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-6">
<div className="max-w-7xl mx-auto">
<div className="text-center">
<h1 className="text-3xl font-bold mb-4 text-gray-800">Kunden-Projekte</h1>
<div className="animate-spin rounded-full h-12 w-12 border-b-4 border-blue-500 border-t-transparent mx-auto"></div>
<p className="mt-4 text-gray-600">Lade Projekte...</p>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="max-w-7xl mx-auto p-6">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-blue-500 rounded-xl">
<Users className="w-8 h-8 text-white" />
</div>
<div>
<h1 className="text-4xl font-bold text-gray-800">Kunden-Projekte</h1>
<p className="text-lg text-gray-600">
Professionelle Projektverwaltung und Kundenbefragung
</p>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<Card className="bg-white/80 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Calendar className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-sm text-gray-600">Gesamt Projekte</p>
<p className="text-2xl font-bold text-gray-800">{customers.length}</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white/80 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-yellow-100 rounded-lg">
<AlertCircle className="w-5 h-5 text-yellow-600" />
</div>
<div>
<p className="text-sm text-gray-600">Wartend</p>
<p className="text-2xl font-bold text-gray-800">
{customers.filter(c => c.appointment_status === 'pending').length}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white/80 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-100 rounded-lg">
<Play className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-sm text-gray-600">Aktiv</p>
<p className="text-2xl font-bold text-gray-800">
{customers.filter(c => c.appointment_status === 'running').length}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white/80 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-gray-100 rounded-lg">
<CheckCircle className="w-5 h-5 text-gray-600" />
</div>
<div>
<p className="text-sm text-gray-600">Abgeschlossen</p>
<p className="text-2xl font-bold text-gray-800">
{customers.filter(c => c.appointment_status === 'completed').length}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="flex-1">
<div className="relative">
<Input
placeholder="Nach Kunde, Firma oder E-Mail suchen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 bg-white/80 backdrop-blur-sm border-0 shadow-sm"
/>
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
</div>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full sm:w-48 bg-white/80 backdrop-blur-sm border-0 shadow-sm">
<SelectValue placeholder="Status filtern" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Alle Status</SelectItem>
<SelectItem value="pending">Wartend</SelectItem>
<SelectItem value="running">Aktiv</SelectItem>
<SelectItem value="completed">Abgeschlossen</SelectItem>
</SelectContent>
</Select>
</div>
{/* Projects Grid */}
{filteredCustomers.length === 0 ? (
<Card className="bg-white/80 backdrop-blur-sm">
<CardContent className="p-12 text-center">
<div className="p-4 bg-blue-100 rounded-full w-16 h-16 mx-auto mb-4 flex items-center justify-center">
<Users className="w-8 h-8 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-2">
{customers.length === 0
? "Keine Projekte gefunden"
: "Keine Projekte entsprechen den Filterkriterien"}
</h3>
<p className="text-gray-600">
{customers.length === 0
? "Erstellen Sie Ihr erstes Kundenprojekt über die Terminbuchung."
: "Versuchen Sie andere Suchkriterien oder Filter."}
</p>
</CardContent>
</Card>
) : (
<div className="grid gap-6">
{filteredCustomers.map((customer) => {
const completion = getCompletionStatus(customer);
return (
<Card key={customer.id} className="bg-white/80 backdrop-blur-sm hover:shadow-lg transition-all duration-300 border-0 shadow-sm">
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Building className="w-5 h-5 text-blue-600" />
</div>
<div>
<CardTitle className="text-xl text-gray-800">
{customer.ansprechpartn || customer.firma || 'Unbekannter Kunde'}
</CardTitle>
<p className="text-sm text-gray-600">
{customer.firma && customer.ansprechpartn ? `${customer.firma} - ${customer.ansprechpartn}` : customer.firma || customer.ansprechpartn}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Mail className="w-4 h-4" />
<span>{customer.email || 'E-Mail nicht angegeben'}</span>
</div>
{customer.telefon && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<Phone className="w-4 h-4" />
<span>{customer.telefon}</span>
</div>
)}
{customer.termin_datum && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>{new Date(customer.termin_datum).toLocaleString('de-DE')}</span>
</div>
)}
</div>
{/* Completion Progress */}
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">Befragung Fortschritt</span>
<span className="text-sm text-gray-600">{completion.percentage}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
completion.percentage >= 80 ? 'bg-green-500' :
completion.percentage >= 50 ? 'bg-yellow-500' : 'bg-blue-500'
}`}
style={{ width: `${completion.percentage}%` }}
></div>
</div>
<p className="text-xs text-gray-500 mt-1">
{completion.filledFields} von {completion.totalFields} Feldern ausgefüllt
</p>
</div>
</div>
<div className="flex flex-col items-end gap-3">
{getStatusBadge(customer.appointment_status || 'pending', customer.started_by)}
{getStatusActions(customer)}
</div>
</div>
</CardHeader>
</Card>
);
})}
</div>
)}
</div>
</div>
);
}

28
app/layout.tsx Normal file
View File

@@ -0,0 +1,28 @@
import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Webklar Klarheit im Webdesign',
description: 'Wir gestalten moderne, schnelle Websites für Ihr Business.',
openGraph: {
title: 'Webklar Klarheit im Webdesign',
description: 'Wir gestalten moderne, schnelle Websites für Ihr Business.',
url: 'https://webklar.com',
type: 'website',
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="de">
<body className={inter.className}>{children}</body>
</html>
);
}

874
app/page.tsx Normal file
View File

@@ -0,0 +1,874 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Calendar, Cookie } from "lucide-react";
import { colors } from "@/lib/colors";
import ProtectedAppointmentBooking from "@/components/ProtectedAppointmentBooking";
import AppointmentStatus from "@/components/AppointmentStatus";
import PriceCalculator from "@/components/PriceCalculator";
import SpinningNumbers from "@/components/SpinningNumbers";
import GlassSurface from "@/components/GlassSurface";
import LogoLoop from "@/components/LogoLoop";
import PillNav from "@/components/PillNav";
import TimelineDemo from "@/components/ui/timeline-demo";
import { HoverEffect } from "@/components/ui/card-hover-effect";
import HeroScrollDemo from "@/components/ui/container-scroll-animation-demo";
import Link from 'next/link';
// Scroll animation hook
const useScrollAnimation = () => {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return scrollY;
};
// Intersection Observer hook for fade-in animations
const useInView = (threshold = 0.1) => {
const [isInView, setIsInView] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
}
},
{ threshold }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [threshold]);
return [ref, isInView] as const;
};
// Cookie Button Component
const CookieButton = () => {
const [showBanner, setShowBanner] = useState(false);
const handleAccept = () => {
setShowBanner(false);
// Add cookie acceptance logic here
};
return (
<>
{/* Cookie Button */}
<button
onClick={() => setShowBanner(true)}
className="fixed bottom-6 left-6 z-50 w-14 h-14 rounded-full flex items-center justify-center shadow-lg transition-all duration-300 hover:scale-110"
style={{ backgroundColor: colors.secondary }}
>
<Cookie className="w-6 h-6 text-white" />
</button>
{/* Cookie Banner */}
{showBanner && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div
className="max-w-md w-full p-6 rounded-3xl shadow-2xl"
style={{ backgroundColor: colors.background }}
>
<div className="flex items-center space-x-3 mb-4">
<Cookie className="w-6 h-6" style={{ color: colors.primary }} />
<h3 className="text-lg font-semibold" style={{ color: colors.primary }}>
Cookie-Einstellungen
</h3>
</div>
<p className="mb-6 text-sm" style={{ color: colors.secondary }}>
Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Durch die Nutzung unserer Website stimmen Sie unserer Datenschutzrichtlinie zu.
</p>
<div className="flex space-x-3">
<Button
onClick={handleAccept}
className="flex-1 rounded-full font-medium"
style={{
backgroundColor: colors.primary,
color: colors.background
}}
>
Akzeptieren
</Button>
<Button
onClick={() => setShowBanner(false)}
variant="outline"
className="flex-1 rounded-full"
style={{
borderColor: colors.secondary,
color: colors.secondary
}}
>
Ablehnen
</Button>
</div>
</div>
</div>
)}
</>
);
};
export default function AboutServicePage() {
const scrollY = useScrollAnimation();
const [heroRef, heroInView] = useInView();
const [servicesRef, servicesInView] = useInView();
const [processRef, processInView] = useInView();
const [pricingRef, pricingInView] = useInView();
const [aboutRef, aboutInView] = useInView();
const [contactRef, contactInView] = useInView();
const navWrapperRef = useRef<HTMLDivElement>(null);
const navInnerRef = useRef<HTMLDivElement>(null);
const [navOffset, setNavOffset] = useState(0);
const navOffsetRef = useRef(0);
const navExpandedRef = useRef(false);
const animationFrameRef = useRef<number | null>(null);
useEffect(() => {
navOffsetRef.current = navOffset;
}, [navOffset]);
useEffect(() => {
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, []);
const animateOffset = useCallback((target: number) => {
if (!Number.isFinite(target)) return;
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
const startValue = navOffsetRef.current;
const delta = target - startValue;
if (Math.abs(delta) < 0.5) {
navOffsetRef.current = target;
setNavOffset(target);
return;
}
const duration = 520;
const startTime = performance.now();
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
const step = (now: number) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = easeOutCubic(progress);
const value = startValue + delta * eased;
navOffsetRef.current = value;
setNavOffset(value);
if (progress < 1) {
animationFrameRef.current = requestAnimationFrame(step);
} else {
animationFrameRef.current = null;
}
};
animationFrameRef.current = requestAnimationFrame(step);
}, []);
const calculateNavOffset = useCallback(
(mode: "animate" | "immediate" = "animate", expandedOverride?: boolean) => {
if (!navWrapperRef.current || !navInnerRef.current) return;
const expanded = expandedOverride ?? navExpandedRef.current;
const wrapperWidth = navWrapperRef.current.clientWidth;
const navWidth = navInnerRef.current.offsetWidth;
const offset = expanded ? Math.max(0, (wrapperWidth - navWidth) / 2) : 0;
if (!Number.isFinite(offset)) return;
if (mode === "immediate") {
navOffsetRef.current = offset;
setNavOffset(offset);
} else {
animateOffset(offset);
}
},
[animateOffset]
);
useEffect(() => {
calculateNavOffset("immediate", navExpandedRef.current);
const handleResize = () => calculateNavOffset("immediate", navExpandedRef.current);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [calculateNavOffset]);
useEffect(() => {
if (!navInnerRef.current || typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(() => {
calculateNavOffset("immediate", navExpandedRef.current);
});
observer.observe(navInnerRef.current);
return () => observer.disconnect();
}, [calculateNavOffset]);
useEffect(() => {
const wrapper = navWrapperRef.current;
if (!wrapper) return;
const handleTransitionEnd = (event: TransitionEvent) => {
if (event.propertyName === "max-width" || event.propertyName === "width" || event.propertyName === "transform") {
calculateNavOffset(navExpandedRef.current ? "animate" : "immediate", navExpandedRef.current);
}
};
wrapper.addEventListener("transitionend", handleTransitionEnd);
return () => wrapper.removeEventListener("transitionend", handleTransitionEnd);
}, [calculateNavOffset]);
// Partner Logos für LogoLoop
const partnerLogos = [
{
node: (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<svg viewBox="0 0 48 48" fill="none" width={96} height={96} xmlns="http://www.w3.org/2000/svg">
{/* Traefik - Reverse Proxy mit drei Punkten */}
<circle cx="16" cy="16" r="4" fill={colors.background}/>
<circle cx="24" cy="24" r="4" fill={colors.background}/>
<circle cx="32" cy="32" r="4" fill={colors.background}/>
<path d="M16 16L24 24M24 24L32 32" stroke={colors.background} strokeWidth="2" strokeLinecap="round"/>
<path d="M12 24L24 12L36 24" stroke={colors.background} strokeWidth="2" opacity="0.5"/>
</svg>
<span style={{ color: colors.background, fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>Traefik</span>
</div>
),
title: "Traefik",
href: "https://traefik.io"
},
{
node: (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<svg viewBox="0 0 48 48" fill="none" width={96} height={96} xmlns="http://www.w3.org/2000/svg">
{/* Porkbun - Domain/Globe */}
<circle cx="24" cy="24" r="16" stroke={colors.background} strokeWidth="3"/>
<path d="M8 24C8 18 12 14 24 14C36 14 40 18 40 24C40 30 36 34 24 34C12 34 8 30 8 24Z" stroke={colors.background} strokeWidth="2"/>
<path d="M24 8C24 8 18 14 18 20C18 26 24 32 24 32" stroke={colors.background} strokeWidth="2"/>
</svg>
<span style={{ color: colors.background, fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>Porkbun</span>
</div>
),
title: "Porkbun",
href: "https://porkbun.com"
},
{
node: (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<svg viewBox="0 0 48 48" fill="none" width={96} height={96} xmlns="http://www.w3.org/2000/svg">
{/* n8n - Workflow Nodes */}
<circle cx="16" cy="16" r="5" fill={colors.background}/>
<circle cx="32" cy="16" r="5" fill={colors.background}/>
<circle cx="16" cy="32" r="5" fill={colors.background}/>
<circle cx="32" cy="32" r="5" fill={colors.background}/>
<path d="M21 16L27 16M16 21L16 27M21 32L27 32" stroke={colors.background} strokeWidth="2" strokeLinecap="round"/>
</svg>
<span style={{ color: colors.background, fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>n8n</span>
</div>
),
title: "n8n",
href: "https://n8n.io"
},
{
node: (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<svg viewBox="0 0 48 48" fill="none" width={96} height={96} xmlns="http://www.w3.org/2000/svg">
{/* Mistral AI - Wind/Wave */}
<path d="M8 24Q16 16 24 24T40 24" stroke={colors.background} strokeWidth="3" strokeLinecap="round" fill="none"/>
<path d="M10 28Q18 20 26 28T42 28" stroke={colors.background} strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.7"/>
<circle cx="24" cy="24" r="2" fill={colors.background} opacity="0.5"/>
</svg>
<span style={{ color: colors.background, fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>Mistral AI</span>
</div>
),
title: "Mistral AI",
href: "https://mistral.ai"
},
{
node: (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<svg viewBox="0 0 48 48" fill="none" width={96} height={96} xmlns="http://www.w3.org/2000/svg">
{/* Hetzner - Server/Cloud */}
<rect x="12" y="14" width="24" height="20" rx="2" stroke={colors.background} strokeWidth="3"/>
<path d="M16 20H32M16 24H32M16 28H28" stroke={colors.background} strokeWidth="2" strokeLinecap="round"/>
<circle cx="36" cy="18" r="3" fill={colors.background}/>
</svg>
<span style={{ color: colors.background, fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>Hetzner</span>
</div>
),
title: "Hetzner",
href: "https://www.hetzner.com"
},
{
node: (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<svg viewBox="0 0 48 48" fill="none" width={96} height={96} xmlns="http://www.w3.org/2000/svg">
{/* Cursor - Code Editor */}
<rect x="12" y="12" width="24" height="24" rx="4" stroke={colors.background} strokeWidth="3"/>
<path d="M18 20L24 24L18 28" stroke={colors.background} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M28 20L30 24L28 28" stroke={colors.background} strokeWidth="2" strokeLinecap="round"/>
</svg>
<span style={{ color: colors.background, fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>Cursor</span>
</div>
),
title: "Cursor AI",
href: "https://cursor.sh"
},
{
node: (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<svg viewBox="0 0 48 48" fill="none" width={96} height={96} xmlns="http://www.w3.org/2000/svg">
{/* Appwrite - Database/Backend */}
<rect x="14" y="10" width="20" height="28" rx="2" stroke={colors.background} strokeWidth="3"/>
<path d="M14 18H34M14 24H34M14 30H28" stroke={colors.background} strokeWidth="2" strokeLinecap="round"/>
<circle cx="32" cy="14" r="2" fill={colors.background}/>
</svg>
<span style={{ color: colors.background, fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>Appwrite</span>
</div>
),
title: "Appwrite",
href: "https://appwrite.io"
}
];
const valueProps = [
{
title: "Zeitersparnis",
description: "Wir übernehmen den digitalen Teil, Sie konzentrieren sich aufs Geschäft",
link: "#zeitersparnis"
},
{
title: "Kompetenz & Erfahrung",
description: "Technisch stark, klar in der Umsetzung",
link: "#kompetenz-erfahrung"
},
{
title: "Maßgeschneiderte Lösungen",
description: "Keine Templates, sondern individuelle Umsetzung",
link: "#massgeschneiderte-loesungen"
},
{
title: "Stressfreies Webmanagement",
description: "Ein Ansprechpartner für alles",
link: "#stressfreies-webmanagement"
}
];
const navIsExpanded = scrollY > 40;
const previousNavState = useRef(navIsExpanded);
useEffect(() => {
const previous = previousNavState.current;
previousNavState.current = navIsExpanded;
navExpandedRef.current = navIsExpanded;
let raf1: number | null = null;
let raf2: number | null = null;
let timeoutId: number | null = null;
if (navIsExpanded) {
if (previous === false) {
raf1 = requestAnimationFrame(() => {
calculateNavOffset("animate", true);
});
raf2 = requestAnimationFrame(() => {
calculateNavOffset("animate", true);
});
timeoutId = window.setTimeout(() => {
calculateNavOffset("animate", true);
}, 320);
} else {
calculateNavOffset("immediate", true);
}
} else {
animateOffset(0);
}
return () => {
if (raf1 !== null) cancelAnimationFrame(raf1);
if (raf2 !== null) cancelAnimationFrame(raf2);
if (timeoutId !== null) window.clearTimeout(timeoutId);
};
}, [navIsExpanded, calculateNavOffset, animateOffset]);
return (
<>
<div className="min-h-screen overflow-hidden" style={{ backgroundColor: colors.background }}>
{/* Fixed Navigation mit PillNav */}
<div
className="fixed left-0 right-0 z-40 px-4 top-4 transition-all duration-500"
style={{
transitionTimingFunction: "cubic-bezier(0.22, 1, 0.36, 1)"
}}
>
<div
className="w-full mx-auto transition-all duration-500 ease-out"
style={{
maxWidth: navIsExpanded ? "72rem" : "38rem",
width: navIsExpanded ? "100%" : "96%"
}}
>
<GlassSurface
width="100%"
height="auto"
borderRadius={9999}
displace={2.0}
backgroundOpacity={0.3}
className="w-full transition-all duration-500 ease-out"
style={{
minHeight: '70px',
backgroundColor: `${colors.secondary}66`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0.5rem 1.5rem',
transition: 'backdrop-filter 0.45s ease'
}}
>
<div
ref={navWrapperRef}
className="relative w-full flex items-center justify-center transition-all duration-500"
style={{
transitionTimingFunction: "cubic-bezier(0.22, 1, 0.36, 1)"
}}
>
{/* PillNav mit den Nav-Links */}
<div
ref={navInnerRef}
className="inline-flex transition-all duration-500"
style={{
transform: navIsExpanded ? `translateX(-${navOffset}px)` : "translateX(0)",
transitionTimingFunction: "cubic-bezier(0.22, 1, 0.36, 1)"
}}
>
<PillNav
logo="/WebKlarLogo.png"
logoAlt="Webklar Logo"
items={[
{ label: 'Über uns', href: '#about' },
{ label: 'Leistungen', href: '#services' },
{ label: 'Unsere Abläufe', href: '#process' }
]}
activeHref="#"
baseColor={colors.secondary}
pillColor={colors.background}
hoveredPillTextColor={colors.background}
pillTextColor={colors.primary}
className="pill-nav-custom"
/>
</div>
{/* Kontakt-Button rechts */}
<div
className="hidden md:block absolute right-0 transition-all duration-500 overflow-hidden"
style={{
opacity: navIsExpanded ? 1 : 0,
maxWidth: navIsExpanded ? "200px" : "0px",
pointerEvents: navIsExpanded ? "auto" : "none",
transform: navIsExpanded ? "translateX(0)" : "translateX(16px)",
transitionTimingFunction: "cubic-bezier(0.22, 1, 0.36, 1)"
}}
>
<Link href="/kontakte">
<Button className="rounded-full text-sm font-semibold px-3 sm:px-5 py-1.5 shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 btn-enhanced pulse-glow" style={{ backgroundColor: colors.primary, color: colors.background }}>
Kontakt
</Button>
</Link>
</div>
</div>
</GlassSurface>
</div>
</div>
{/* Background Video Hero Section */}
<section id="hero" className="relative h-screen flex items-center justify-center overflow-hidden">
{/* Video Background */}
<div className="absolute inset-0 z-0">
<video
autoPlay
muted
loop
playsInline
className="w-full h-full object-cover"
style={{ filter: 'blur(8px)' }}
>
<source src="/path/to/your/background-video.mp4" type="video/mp4" />
</video>
<div
className="absolute inset-0 backdrop-blur-sm"
style={{
background: `linear-gradient(135deg, ${colors.primary}CC, ${colors.secondary}CC)`
}}
></div>
</div>
<div ref={heroRef} className="relative z-20 px-4 sm:px-8 pt-24 sm:pt-28 pb-20 sm:pb-24 max-w-7xl mx-auto text-center">
<h1 className={`text-4xl sm:text-6xl md:text-8xl font-bold mb-6 sm:mb-8 transition-all duration-1000 ${
heroInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`}>
<span style={{ color: colors.background }}>
webklar das Web maßgeschneidert auf Ihr Unternehmen
</span>
</h1>
<div className={`flex flex-wrap justify-center gap-2 sm:gap-4 mb-8 sm:mb-12 text-xs sm:text-sm mt-6 transition-all duration-1000 delay-300 ${
heroInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`}>
{['Strategieberatung', 'UX/UI Design', 'Entwicklung', 'SEO & Support'].map((item) => (
<span
key={item}
className="px-3 sm:px-4 py-2 rounded-full backdrop-blur-sm border"
style={{
backgroundColor: `${colors.background}80`,
borderColor: colors.tertiary,
color: colors.primary
}}
>
{item}
</span>
))}
</div>
<div className={`flex flex-col sm:flex-row items-center justify-center gap-4 sm:gap-6 transition-all duration-1000 delay-500 ${
heroInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`}>
<Button
onClick={() => document.getElementById('contact')?.scrollIntoView({ behavior: 'smooth' })}
className="w-full sm:w-auto px-6 sm:px-8 py-3 sm:py-4 rounded-full flex items-center justify-center space-x-3 text-base sm:text-lg font-semibold shadow-2xl hover:shadow-3xl transition-all duration-300 hover:scale-105 btn-enhanced hover-lift"
style={{
backgroundColor: colors.background,
color: colors.primary
}}
>
<Calendar className="w-5 sm:w-6 h-5 sm:h-6" />
<span>Kostenlosen Termin buchen</span>
</Button>
</div>
{/* Partner Tools */}
<div className="mt-8 sm:mt-12 px-4 sm:px-8">
<div style={{ position: 'relative', overflow: 'visible', maxWidth: '100%' }} className="logo-loop-container">
<div style={{ height: '180px', position: 'relative', width: '100%', paddingBottom: '30px', overflow: 'hidden' }} className="logo-loop-inner">
<LogoLoop
logos={partnerLogos}
speed={60}
direction="left"
logoHeight={140}
gap={100}
pauseOnHover
scaleOnHover
ariaLabel="Technology partners"
style={{ width: '100%' }}
/>
</div>
</div>
</div>
</div>
</section>
{/* About Section */}
<section
id="about"
ref={aboutRef}
className="relative px-4 sm:px-8 py-12 sm:py-20 rounded-t-[2rem] sm:rounded-t-[3rem] rounded-b-[2rem] sm:rounded-b-[3rem] mx-2 sm:mx-4 backdrop-blur-sm"
style={{ backgroundColor: `${colors.background}F0`, position: 'relative', zIndex: 10 }}
>
<div className="max-w-7xl mx-auto">
<div className="grid lg:grid-cols-2 gap-8 sm:gap-16 items-center">
{/* Links: Spinning Numbers */}
<div className="relative order-2 lg:order-1">
<SpinningNumbers />
</div>
{/* Rechts: Text im Zeitungsstil (mehrspaltig) */}
<div className={`order-1 lg:order-2 transition-all duration-1000 w-full flex flex-col justify-center ${
aboutInView ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-10'
}`}>
<h2 className="text-3xl sm:text-5xl md:text-6xl font-bold mb-6 sm:mb-8 leading-tight whitespace-nowrap" style={{ color: colors.primary }}>
<span className="inline-block">Worauf wir</span>{" "}
<span className="inline-block">Wert</span>{" "}
<span className="inline-block">legen</span>
</h2>
<div
className="text-xl sm:text-2xl md:text-3xl leading-relaxed"
style={{
color: colors.secondary,
width: '100%',
maxWidth: '100%',
columnCount: 1,
columnGap: '3rem',
columnFill: 'balance',
textAlign: 'justify'
}}
>
Sicherheit ist für uns keine Nebensache, sondern die Grundlage jeder Website. Wir setzen auf moderne Technologien, zertifizierte Partner und höchste Datenschutzstandards. Unsere Systeme sind darauf ausgelegt, Ausfälle zu vermeiden und langfristig stabile Ergebnisse zu liefern damit Ihre Online-Präsenz so zuverlässig ist wie Ihr Unternehmen selbst.
</div>
</div>
</div>
</div>
</section>
{/* Services Grid */}
<section
id="services"
ref={servicesRef}
className="relative px-4 sm:px-8 py-12 sm:py-20 rounded-t-[2rem] sm:rounded-t-[3rem] rounded-b-[2rem] sm:rounded-b-[3rem] mx-2 sm:mx-4 backdrop-blur-sm"
style={{ backgroundColor: colors.primary }}
>
<div className="max-w-7xl mx-auto">
<div className="text-center mb-12 sm:mb-16">
<h2 className={`text-3xl sm:text-5xl md:text-6xl font-bold mb-4 sm:mb-6 transition-all duration-1000 ${
servicesInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`} style={{ color: colors.tertiary }}>
Unsere Leistungen
</h2>
<p className={`text-lg sm:text-xl transition-all duration-1000 delay-200 ${
servicesInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`} style={{ color: colors.background }}>
Alles aus einer Hand für Ihren digitalen Erfolg
</p>
</div>
<div
className={`transition-all duration-1000 ${
servicesInView ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
}`}
>
<HeroScrollDemo />
</div>
</div>
</section>
{/* Process Section */}
<section
id="process"
ref={processRef}
className="relative px-4 sm:px-8 py-12 sm:py-20 rounded-t-[2rem] sm:rounded-t-[3rem] rounded-b-[2rem] sm:rounded-b-[3rem] mx-2 sm:mx-4 backdrop-blur-sm"
style={{ backgroundColor: `${colors.background}F0` }}
>
<div className="max-w-6xl mx-auto">
<div className="text-center mb-12 sm:mb-16">
<h2 className={`text-3xl sm:text-5xl md:text-6xl font-bold mb-4 sm:mb-6 transition-all duration-1000 ${
processInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`} style={{ color: colors.primary }}>
Unser Ablauf
</h2>
<p className={`text-lg sm:text-xl transition-all duration-1000 delay-200 ${
processInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`} style={{ color: colors.secondary }}>
So läuft die Zusammenarbeit ab
</p>
</div>
<div
className="rounded-[32px] px-2 py-10 sm:px-6 sm:py-16 transition-all duration-700"
style={{
background: `linear-gradient(135deg, ${colors.background}F2, ${colors.background}E8)`
}}
>
<TimelineDemo />
</div>
</div>
</section>
{/* Pricing Section */}
<section
id="references"
ref={pricingRef}
className="relative px-4 sm:px-8 py-12 sm:py-20 rounded-t-[2rem] sm:rounded-t-[3rem] rounded-b-[2rem] sm:rounded-b-[3rem] mx-2 sm:mx-4 backdrop-blur-sm"
style={{ backgroundColor: `${colors.primary}F0` }}
>
<div className="max-w-4xl mx-auto text-center">
<div className="mb-12 sm:mb-16">
<h2 className={`text-3xl sm:text-5xl md:text-6xl font-bold mb-4 sm:mb-6 transition-all duration-1000 ${
pricingInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`} style={{ color: colors.tertiary }}>
Faire Preise
</h2>
<p className={`text-lg sm:text-xl transition-all duration-1000 delay-200 ${
pricingInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`} style={{ color: colors.background }}>
Transparent und flexibel
</p>
</div>
<div className={`p-8 sm:p-12 rounded-3xl shadow-2xl backdrop-blur-sm transition-all duration-1000 delay-300 ${
pricingInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`} style={{ backgroundColor: `${colors.background}F0` }}>
<h3 className="text-2xl sm:text-3xl font-bold mb-6" style={{ color: colors.primary }}>
Individuelle Lösungen
</h3>
<p className="text-lg sm:text-xl mb-8 leading-relaxed" style={{ color: colors.secondary }}>
Unsere Preise richten sich nach dem Projektumfang und Ihren Anforderungen.
Gemeinsam finden wir eine Lösung, die zu Ihrem Budget passt transparent, fair und flexibel.
</p>
<PriceCalculator />
</div>
</div>
</section>
{/* Target Groups & Value Props */}
<section
className="relative px-4 sm:px-8 py-12 sm:py-20 rounded-t-[2rem] sm:rounded-t-[3rem] rounded-b-[2rem] sm:rounded-b-[3rem] mx-2 sm:mx-4 backdrop-blur-sm"
style={{ backgroundColor: `${colors.background}F0` }}
>
<div className="max-w-7xl mx-auto">
<div className="grid lg:grid-cols-2 gap-12 sm:gap-16">
{/* Target Groups */}
<div>
<h2 className="text-2xl sm:text-4xl font-bold mb-6 sm:mb-8" style={{ color: colors.primary }}>
Für wen wir arbeiten
</h2>
<p className="text-lg sm:text-xl leading-relaxed" style={{ color: colors.secondary }}>
Wir arbeiten mit Unternehmen, die ihre veraltete Website modernisieren oder ihre Zeit nicht mehr mit Technik und Support verschwenden wollen.
</p>
</div>
{/* Value Props */}
<div>
<h2 className="text-2xl sm:text-4xl font-bold mb-6 sm:mb-8" style={{ color: colors.primary }}>
Warum wir das tun
</h2>
<div
className="rounded-3xl border shadow-sm"
style={{
background: `linear-gradient(135deg, ${colors.background}F5, ${colors.tertiary}1A)`,
borderColor: `${colors.secondary}55`
}}
>
<div className="p-4 sm:p-8">
<HoverEffect
items={valueProps}
className="py-2"
/>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Contact Section */}
<section
ref={contactRef}
id="contact"
className="relative px-4 sm:px-8 py-12 sm:py-20 rounded-t-[2rem] sm:rounded-t-[3rem] rounded-b-[2rem] sm:rounded-b-[3rem] mx-2 sm:mx-4 backdrop-blur-sm"
style={{ backgroundColor: colors.secondary }}
>
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12 sm:mb-16">
<h2
className="text-3xl sm:text-5xl md:text-6xl font-bold mb-4 sm:mb-6"
style={{ color: colors.background }}
>
Lassen Sie uns sprechen
</h2>
<p
className="text-lg sm:text-xl opacity-90"
style={{ color: colors.background }}
>
Erzählen Sie uns von Ihrem Projekt
</p>
</div>
<div className={`transition-all duration-1000 ${
contactInView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`}>
<ProtectedAppointmentBooking />
</div>
</div>
</section>
{/* Footer */}
<footer
className="relative py-8 sm:py-12 border-t rounded-t-[2rem] sm:rounded-t-[3rem] mx-2 sm:mx-4 backdrop-blur-sm"
style={{
backgroundColor: `${colors.primary}F0`,
borderColor: colors.secondary
}}
>
<div className="max-w-7xl mx-auto px-4 sm:px-8">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6 sm:gap-8">
<div className="col-span-1 sm:col-span-2">
<div
className="text-2xl sm:text-3xl font-bold mb-4 relative"
style={{ color: colors.tertiary }}
>
<span className="relative z-10">Webklar</span>
<div
className="absolute -inset-2 rounded-xl blur-sm opacity-20"
style={{ backgroundColor: colors.secondary }}
></div>
</div>
<p
className="mb-6 leading-relaxed text-sm sm:text-base"
style={{ color: colors.background }}
>
Ihr Partner für Web & Support. Moderne Websites. Klare Kommunikation. Persönlicher Support.
</p>
</div>
<div>
<h4 className="text-base sm:text-lg font-semibold mb-4" style={{ color: colors.tertiary }}>
Services
</h4>
<ul className="space-y-2 text-sm sm:text-base" style={{ color: colors.background }}>
{['Webdesign', 'E-Commerce', 'SEO', 'Hosting'].map((item) => (
<li key={item}>
<a href="#" className="hover:opacity-80 transition-opacity">{item}</a>
</li>
))}
</ul>
</div>
<div>
<h4 className="text-base sm:text-lg font-semibold mb-4" style={{ color: colors.tertiary }}>
Kontakt
</h4>
<ul className="space-y-2 text-sm sm:text-base" style={{ color: colors.background }}>
<li><Link href="/impressum" className="hover:opacity-80 transition-opacity">Impressum</Link></li>
<li><Link href="/datenschutz" className="hover:opacity-80 transition-opacity">Datenschutz</Link></li>
<li><Link href="/agb" className="hover:opacity-80 transition-opacity">AGB</Link></li>
<li><Link href="/kontakte" className="hover:opacity-80 transition-opacity">Kontakte</Link></li>
</ul>
</div>
</div>
<div
className="border-t mt-6 sm:mt-8 pt-6 sm:pt-8 text-center text-sm"
style={{
borderColor: colors.secondary,
color: colors.background
}}
>
<p>&copy; 2025 Webklar. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
{/* Film Grain Effect */}
{/* Cookie Button */}
<CookieButton />
{/* Appointment Status */}
<AppointmentStatus />
</div>
</>
);
}

9
app/protected/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
import ProtectedAppointmentBooking from '@/components/ProtectedAppointmentBooking'
export default function ProtectedPage() {
return (
<div className="min-h-screen p-4">
<ProtectedAppointmentBooking />
</div>
)
}

103
app/success/page.tsx Normal file
View File

@@ -0,0 +1,103 @@
"use client";
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from "@/components/ui/button";
import { CheckCircle, ArrowRight, Mail } from "lucide-react";
import { colors } from '@/lib/colors';
export default function SuccessPage() {
const router = useRouter();
const [countdown, setCountdown] = useState(5);
useEffect(() => {
// Auto-redirect after 5 seconds
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer);
router.push('/');
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [router]);
const handleContinue = () => {
router.push('/');
};
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="w-full max-w-md">
<div
className="p-8 rounded-3xl shadow-lg backdrop-blur-sm text-center"
style={{ backgroundColor: `${colors.background}F0` }}
>
{/* Success Icon */}
<div className="w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6"
style={{ backgroundColor: `${colors.primary}20` }}>
<CheckCircle className="w-12 h-12" style={{ color: colors.primary }} />
</div>
{/* Success Message */}
<h1 className="text-2xl font-bold mb-4" style={{ color: colors.primary }}>
E-Mail erfolgreich bestätigt!
</h1>
<p className="text-lg mb-6" style={{ color: colors.secondary }}>
Ihre E-Mail-Adresse wurde erfolgreich verifiziert.
Ihr Termin ist jetzt bestätigt.
</p>
{/* Additional Info */}
<div className="p-4 rounded-xl border-2 mb-6"
style={{
backgroundColor: `${colors.primary}10`,
borderColor: colors.primary
}}>
<div className="flex items-center space-x-2 mb-2">
<Mail className="w-5 h-5" style={{ color: colors.primary }} />
<span className="text-sm font-semibold" style={{ color: colors.primary }}>
Automatische Bestätigung
</span>
</div>
<p className="text-xs" style={{ color: colors.secondary }}>
Sie wurden automatisch angemeldet und Ihr Termin wurde bestätigt.
Keine weiteren Schritte erforderlich.
</p>
</div>
{/* Action Buttons */}
<div className="space-y-3">
<Button
onClick={handleContinue}
className="w-full py-3 rounded-xl text-lg font-semibold flex items-center justify-center space-x-2"
style={{
backgroundColor: colors.primary,
color: colors.background
}}
>
<ArrowRight className="w-5 h-5" />
<span>Weiter zur Startseite</span>
</Button>
<p className="text-xs" style={{ color: colors.secondary }}>
Automatische Weiterleitung in {countdown} Sekunden...
</p>
</div>
{/* Additional Info */}
<div className="mt-6 text-xs" style={{ color: colors.secondary }}>
<p> Sie erhalten eine Bestätigungs-E-Mail</p>
<p> Unser Team wird sich bald bei Ihnen melden</p>
<p> Vielen Dank für Ihr Vertrauen!</p>
</div>
</div>
</div>
</div>
);
}

162
app/verify-email/page.tsx Normal file
View File

@@ -0,0 +1,162 @@
"use client";
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button } from "@/components/ui/button";
import { CheckCircle, AlertCircle, ArrowRight } from "lucide-react";
import { supabase } from '@/lib/supabaseClient';
import { colors } from '@/lib/colors';
export default function VerifyEmailPage() {
const router = useRouter();
const searchParams = useSearchParams();
const [verificationStatus, setVerificationStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [errorMessage, setErrorMessage] = useState<string>('');
useEffect(() => {
const handleEmailVerification = async () => {
try {
// Get the session from the URL parameters
const { data, error } = await supabase.auth.getSession();
if (error) {
console.error('Error getting session:', error);
setVerificationStatus('error');
setErrorMessage('Fehler bei der E-Mail-Verifizierung. Bitte versuchen Sie es erneut.');
return;
}
if (data.session) {
// User is authenticated, verification was successful
setVerificationStatus('success');
// Redirect back to the main page after a short delay
setTimeout(() => {
router.push('/#contact');
}, 3000);
} else {
// No session found, verification might have failed
setVerificationStatus('error');
setErrorMessage('E-Mail-Verifizierung fehlgeschlagen. Bitte überprüfen Sie den Link und versuchen Sie es erneut.');
}
} catch (err) {
console.error('Error during email verification:', err);
setVerificationStatus('error');
setErrorMessage('Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
};
handleEmailVerification();
}, [router]);
return (
<div className="min-h-screen flex items-center justify-center px-4">
<div className="w-full max-w-md">
<div
className="p-8 rounded-3xl shadow-lg backdrop-blur-sm text-center"
style={{ backgroundColor: `${colors.background}F0` }}
>
{verificationStatus === 'loading' && (
<>
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-6"
style={{ backgroundColor: `${colors.primary}20` }}>
<div className="animate-spin rounded-full h-8 w-8 border-b-2"
style={{ borderColor: colors.primary }}></div>
</div>
<h2 className="text-2xl font-bold mb-4" style={{ color: colors.primary }}>
E-Mail wird verifiziert...
</h2>
<p className="text-sm" style={{ color: colors.secondary }}>
Bitte warten Sie einen Moment, während wir Ihre E-Mail-Adresse bestätigen.
</p>
</>
)}
{verificationStatus === 'success' && (
<>
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-6"
style={{ backgroundColor: `${colors.primary}20` }}>
<CheckCircle className="w-8 h-8" style={{ color: colors.primary }} />
</div>
<h2 className="text-2xl font-bold mb-4" style={{ color: colors.primary }}>
E-Mail erfolgreich verifiziert!
</h2>
<p className="text-sm mb-6" style={{ color: colors.secondary }}>
Vielen Dank! Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
Ihr Termin wird nun in unserem System gespeichert.
</p>
<div className="p-4 rounded-xl border-2 mb-6"
style={{
backgroundColor: `${colors.primary}10`,
borderColor: colors.primary
}}>
<p className="text-xs" style={{ color: colors.secondary }}>
Sie werden automatisch zur Hauptseite weitergeleitet...
</p>
</div>
<Button
onClick={() => router.push('/#contact')}
className="w-full rounded-xl flex items-center justify-center space-x-2"
style={{
backgroundColor: colors.primary,
color: colors.background
}}
>
<span>Zurück zur Hauptseite</span>
<ArrowRight className="w-4 h-4" />
</Button>
</>
)}
{verificationStatus === 'error' && (
<>
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-6"
style={{ backgroundColor: '#fef2f2' }}>
<AlertCircle className="w-8 h-8 text-red-500" />
</div>
<h2 className="text-2xl font-bold mb-4" style={{ color: colors.primary }}>
Verifizierung fehlgeschlagen
</h2>
<p className="text-sm mb-6" style={{ color: colors.secondary }}>
{errorMessage}
</p>
<div className="space-y-3">
<Button
onClick={() => router.push('/#contact')}
className="w-full rounded-xl"
style={{
backgroundColor: colors.primary,
color: colors.background
}}
>
Zurück zur Terminbuchung
</Button>
<Button
variant="outline"
className="w-full rounded-xl"
style={{
borderColor: colors.tertiary,
color: colors.primary
}}
onClick={() => window.location.reload()}
>
Erneut versuchen
</Button>
</div>
</>
)}
</div>
</div>
</div>
);
}