feat: React-Landingpage (Vite + TypeScript + Tailwind) hinzugefügt

Originalgetreue Migration der HTML-Landingpage in eine React-SPA.
Registrierungsformular mit Webhook-Integration und n8n-Response-Anzeige.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wm
2026-03-17 08:03:23 +01:00
parent 075aa4bbae
commit 3118943b2e
25 changed files with 4481 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
BotKonzept Frontend — a static SaaS landing page and customer dashboard for an AI chatbot service. Users can upload PDFs, create chatbots trained on their documents, and embed them on their websites.
**Stack:** Vanilla HTML5 / CSS3 / JavaScript (ES6+). No framework, no bundler, no package manager.
## Local Development
```bash
python3 -m http.server 8000
# → http://localhost:8000
```
No build step required. Edit files and refresh the browser.
## Architecture
```
index.html # Public landing page: hero, features, pricing, registration form, FAQ
dashboard.html # Authenticated customer dashboard
css/
style.css # Landing page styles (~30KB) — CSS variables, components, animations
dashboard.css # Dashboard styles (~21KB) — sidebar layout, chat UI, file upload
js/
main.js # Landing page logic — registration form, FAQ accordion, scroll animations
dashboard.js # Dashboard logic — file upload, chatbot chat, embed code, settings
logo/ # SVG logo asset
```
## Key Implementation Details
### API / Webhook Integration
Endpoints are defined as `CONFIG` objects at the top of each JS file:
- **Registration webhook:** `https://n8n.userman.de/webhook/botkonzept-registration` (`main.js`)
- **API base:** `https://api.botkonzept.de` (`dashboard.js`)
The dashboard has a full **demo mode** that works without a backend — it uses `localStorage` for session state and simulates uploads/responses with dummy data.
### dashboard.js Feature Areas
- **Auth:** Session check via `localStorage`; falls back to demo mode
- **File Upload:** Drag-and-drop + click, PDF-only, max 10 MB; simulates upload progress
- **Chat:** User/bot message UI, typing indicator, webhook call with demo fallback
- **Embed Code:** Clipboard API copy with `document.execCommand` fallback
- **Trial Banner:** Dynamic discount logic — 30% on day 3, 15% on day 5, 48-hour countdown
- **Settings:** Bot name, welcome message, and color saved to `localStorage`
### main.js Feature Areas
- Registration form: validates email + URL, POSTs to n8n webhook with German error messages
- FAQ accordion (one item open at a time)
- Scroll animations via `IntersectionObserver`
- Typing animation in chat preview section
### CSS Design Tokens
Defined as CSS variables in `style.css`:
- Primary: `#6366f1` (indigo), Secondary: `#0ea5e9` (sky blue), Accent: amber, Success: emerald, Error: red
- Font: Inter (Google Fonts CDN)
- Responsive breakpoints: `>1024px` desktop, `7681024px` tablet, `<768px` mobile, `<480px` small mobile
### Security
- Chat messages are HTML-escaped before insertion to prevent XSS
- Form inputs validated client-side before submission
## React-Migration (landing-react/)
Vollständige React-SPA unter `landing-react/` — originalgetreue Portierung der `index.html`-Landingpage.
**Stack:** Vite · React 18 · TypeScript · Tailwind CSS · Lucide React
```bash
cd landing-react
npm install
npm run dev # Entwicklungsserver → http://localhost:5173
npm run build # Produktions-Build → dist/
npm run preview # Build lokal vorschauen
```
**Komponenten:** `Navbar`, `Hero`, `TrustedBy`, `Features`, `Steps`, `Pricing`, `RegistrationSection`, `FAQ`, `CTA`, `Footer`
**Registrierungsformular:** POST an `https://n8n.zq0.de/webhook-test/test`. Status-States: `idle → loading → success | error`. Erfolgsantwort wird als JSON-Karte oder Bild dargestellt.
**Globale Stile:** Gradient-Utilities (`.gradient-text`, `.bg-gradient-primary`, `.btn-gradient`), Typing-Dots-Animation und FAQ-Accordion-Transition in `src/index.css`. Tailwind-Farberweiterungen (`primary`, `primary-dark`, `primary-light`, `secondary`, `accent`, `success`, `error`) in `tailwind.config.js`.
## Localization
All user-facing strings are in **German (de-DE)**. Number/date formatting uses `de-DE` locale.
+2
View File
@@ -0,0 +1,2 @@
node_modules/
dist/
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="BotKonzept - Ihr KI-Chatbot für die Website. Einfach einrichten, PDF hochladen, fertig!" />
<meta name="keywords" content="KI Chatbot, Website Chatbot, RAG, PDF Chatbot, Kundenservice Automation" />
<title>BotKonzept - KI-Chatbot für Ihre Website</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+17
View File
@@ -0,0 +1,17 @@
# Diesen Block in eure bestehende Nginx-Server-Konfiguration einfügen.
# Er proxyt /api/register an n8n weiter kein CORS-Problem, da der
# Request serverseitig von eurer eigenen Domain abgeht.
location /api/register {
proxy_pass https://n8n.zq0.de/webhook-test/test;
proxy_http_version 1.1;
proxy_set_header Host n8n.zq0.de;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeout auf 5 Minuten setzen n8n kann lange brauchen
proxy_read_timeout 300s;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
}
+2776
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "botkonzept-landing",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.460.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "^5.6.3",
"vite": "^6.0.5"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+75
View File
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="359px" height="60px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 2150 359"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil1 {fill:#FEFEFE;fill-rule:nonzero}
.fil4 {fill:#88CED7;fill-rule:nonzero}
.fil3 {fill:#4FC0EF;fill-rule:nonzero}
.fil2 {fill:#3A7ABD;fill-rule:nonzero}
.fil0 {fill:#234182;fill-rule:nonzero}
]]>
</style>
</defs>
<g id="Bot_x0020_TV">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_1548485603296">
<g>
<path class="fil0" d="M204 168l0 22c0,8 5,14 12,14 3,0 6,-2 9,-4 2,-3 4,-6 4,-10l0 -22c0,-8 -5,-14 -12,-14 -3,0 -6,2 -9,4 -2,3 -4,6 -4,10z"/>
</g>
<g>
<path class="fil0" d="M142 168l0 22c0,8 5,14 12,14 3,0 6,-2 9,-4 2,-3 4,-6 4,-10l0 -22c0,-8 -5,-14 -12,-14 -3,0 -6,2 -9,4 -2,3 -4,6 -4,10z"/>
</g>
</g>
<g id="_1548485603200">
<g>
<path class="fil1" d="M209 192l2 0c1,0 1,0 1,-1l0 -24c0,-1 0,-1 -1,-1l-2 0c-1,0 -1,0 -1,1l0 24c0,1 0,1 1,1z"/>
</g>
<g>
<path class="fil1" d="M147 192l2 0c1,0 1,0 1,-1l0 -24c0,-1 0,-1 -1,-1l-2 0c-1,0 -1,0 -1,1l0 24c0,1 0,1 1,1z"/>
</g>
</g>
<path class="fil0" d="M119 24l0 32 -38 0 0 -32c0,-10 8,-19 19,-19 10,0 19,8 19,19zm221 259l-221 0 0 56c0,10 -8,19 -18,20 -11,1 -20,-8 -20,-19l0 -219 0 -8 47 0c-5,0 -8,3 -9,8 0,1 0,1 0,2l0 113c0,5 4,9 9,9l211 0c10,0 19,8 20,18 1,11 -8,20 -19,20z"/>
<g id="_1548485605696">
<path class="fil2" d="M251 302l38 0 0 38c0,10 -8,19 -19,19 -10,0 -19,-8 -19,-19l0 -38z"/>
</g>
<g id="_1548485607712">
<path class="fil3" d="M359 94c0,10 -8,19 -19,19l-32 0 0 -38 32 0c10,0 19,8 19,19z"/>
</g>
<g id="_1548485606320">
<path class="fil4" d="M19 245l43 0 0 38 -43 0c-10,0 -19,-8 -19,-19 0,-10 8,-19 19,-19z"/>
</g>
<path class="fil2" d="M251 237l0 -2 0 -112c0,-5 -4,-9 -9,-9l-222 0c-10,0 -19,-8 -20,-18 -1,-11 8,-20 19,-20l232 0 0 -55c0,-11 9,-20 20,-19 10,1 18,9 18,20l0 217 0 8 -47 0c4,0 8,-3 9,-8 0,0 0,-1 0,-1z"/>
<path class="fil0" d="M391 213l0 -212 90 0c40,0 60,16 60,49 0,24 -13,41 -39,52 28,5 42,21 42,48 0,42 -22,63 -66,63l-87 0zm86 -22c25,0 38,-13 38,-38 0,-25 -17,-37 -51,-37l-12 0 0 -18c40,-5 61,-20 61,-44 0,-20 -11,-30 -34,-30l-60 0 0 167 58 0z"/>
<path id="_1" class="fil0" d="M609 107c0,57 26,86 77,86 51,0 76,-29 76,-86 0,-56 -25,-85 -76,-85 -52,0 -77,28 -77,85zm-29 1c0,-72 35,-108 106,-108 70,0 105,36 105,108 0,71 -35,107 -105,107 -71,0 -106,-36 -106,-107z"/>
<polygon id="_2" class="fil0" points="977,1 977,24 910,24 910,213 883,213 883,24 816,24 816,1 "/>
<path id="_3" class="fil0" d="M1037 1l0 212 -27 0 0 -212 27 0zm149 0l-95 100 97 112 -38 0 -86 -102 0 -17 88 -93 35 0z"/>
<path id="_4" class="fil0" d="M1216 107c0,57 26,86 77,86 51,0 76,-29 76,-86 0,-56 -25,-85 -76,-85 -52,0 -77,28 -77,85zm-29 1c0,-72 35,-108 106,-108 70,0 105,36 105,108 0,71 -35,107 -105,107 -71,0 -106,-36 -106,-107z"/>
<polygon id="_5" class="fil0" points="1594,1 1594,24 1468,191 1592,191 1592,213 1435,213 1435,191 1565,24 1438,24 1438,1 "/>
<polygon id="_6" class="fil0" points="1777,1 1777,24 1666,24 1666,95 1771,95 1771,117 1666,117 1666,191 1779,191 1779,213 1638,213 1638,1 "/>
<path id="_7" class="fil0" d="M1820 213l0 -212 86 0c43,0 65,19 65,57 0,41 -30,66 -89,75l-6 -23c43,-6 65,-23 65,-51 0,-24 -13,-35 -38,-35l-56 0 0 189 -27 0z"/>
<polygon id="_8" class="fil0" points="2150,1 2150,24 2083,24 2083,213 2056,213 2056,24 1989,24 1989,1 "/>
<polygon class="fil2" points="391,359 391,254 411,254 446,333 481,254 500,254 500,359 482,359 482,286 454,359 437,359 409,286 409,359 "/>
<polygon id="_1_8" class="fil2" points="593,254 593,271 541,271 541,297 591,297 591,315 541,315 541,342 594,342 594,359 522,359 522,254 "/>
<path id="_2_9" class="fil2" d="M653 342c22,0 32,-13 32,-38 0,-22 -11,-32 -32,-32l-21 0 0 71 21 0zm-41 17l0 -104 41 0c34,0 52,16 52,49 0,37 -17,55 -52,55l-41 0z"/>
<polygon id="_3_10" class="fil2" points="740,254 740,359 721,359 721,254 "/>
<polygon id="_4_11" class="fil2" points="773,359 753,359 797,254 818,254 863,359 842,359 830,330 796,330 802,313 823,313 807,274 "/>
<polygon id="_5_12" class="fil2" points="876,359 876,254 896,254 931,333 966,254 985,254 985,359 967,359 967,286 939,359 922,359 893,286 893,359 "/>
<polygon id="_6_13" class="fil2" points="1078,254 1078,271 1026,271 1026,297 1076,297 1076,315 1026,315 1026,342 1079,342 1079,359 1007,359 1007,254 "/>
<polygon id="_7_14" class="fil2" points="1165,254 1165,271 1135,271 1135,359 1116,359 1116,271 1086,271 1086,254 "/>
<polygon id="_8_15" class="fil2" points="1251,254 1251,271 1196,342 1251,342 1251,359 1174,359 1174,342 1232,271 1176,271 1176,254 "/>
<polygon id="_9" class="fil2" points="1332,359 1332,254 1352,254 1407,330 1407,254 1425,254 1425,359 1406,359 1351,281 1351,359 "/>
<polygon id="_10" class="fil2" points="1518,254 1518,271 1466,271 1466,297 1516,297 1516,315 1466,315 1466,342 1519,342 1519,359 1447,359 1447,254 "/>
<polygon id="_11" class="fil2" points="1605,254 1605,271 1576,271 1576,359 1556,359 1556,271 1526,271 1526,254 "/>
<polygon id="_12" class="fil2" points="1608,254 1628,254 1639,329 1666,254 1684,254 1710,326 1721,254 1740,254 1721,359 1703,359 1674,277 1643,359 1625,359 "/>
<path id="_13" class="fil2" d="M1770 306c0,24 11,36 32,36 21,0 31,-12 31,-36 0,-24 -10,-36 -31,-36 -22,0 -32,12 -32,36zm-20 0c0,-35 17,-53 52,-53 34,0 51,18 51,53 0,35 -17,53 -51,53 -33,0 -51,-18 -52,-53z"/>
<path id="_14" class="fil2" d="M1872 358l0 -104 47 0c21,0 32,9 32,28 0,13 -8,23 -25,31l33 45 -24 0 -32 -45 0 -9c18,-3 28,-10 28,-21 0,-8 -4,-12 -13,-12l-26 0 0 87 -20 0z"/>
<path id="_15" class="fil2" d="M1988 254l0 104 -19 0 0 -104 19 0zm73 0l-44 49 49 56 -27 0 -40 -49 0 -13 39 -43 24 0z"/>
<path id="_16" class="fil2" d="M2071 354l0 -17c10,4 22,6 35,6 16,0 24,-5 24,-16 0,-8 -5,-12 -15,-12l-16 0c-21,0 -32,-10 -32,-29 0,-21 15,-32 46,-32 12,0 23,2 33,5l0 17c-10,-4 -21,-6 -33,-6 -17,0 -26,5 -26,15 0,8 4,12 13,12l16 0c23,0 34,10 34,29 0,22 -14,33 -43,33 -13,0 -25,-2 -35,-5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

+29
View File
@@ -0,0 +1,29 @@
import Navbar from './components/Navbar'
import Hero from './components/Hero'
import TrustedBy from './components/TrustedBy'
import Features from './components/Features'
import Steps from './components/Steps'
import Pricing from './components/Pricing'
import RegistrationSection from './components/RegistrationSection'
import FAQ from './components/FAQ'
import CTA from './components/CTA'
import Footer from './components/Footer'
export default function App() {
return (
<>
<Navbar />
<main>
<Hero />
<TrustedBy />
<Features />
<Steps />
<Pricing />
<RegistrationSection />
<FAQ />
<CTA />
</main>
<Footer />
</>
)
}
+34
View File
@@ -0,0 +1,34 @@
import { Rocket } from 'lucide-react'
export default function CTA() {
const scrollToRegister = () => {
const el = document.getElementById('register')
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - 80
window.scrollTo({ top, behavior: 'smooth' })
}
}
return (
<section
className="py-[80px]"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)' }}
>
<div className="max-w-container mx-auto px-6 text-center text-white">
<h2 className="text-[clamp(2rem,4vw,3rem)] font-bold text-white mb-4">
Bereit, Ihren Kundenservice zu revolutionieren?
</h2>
<p className="text-xl opacity-90 mb-8">
Starten Sie noch heute Ihre kostenlose 7-Tage-Trial
</p>
<button
onClick={scrollToRegister}
className="inline-flex items-center gap-2 px-8 py-4 bg-white text-primary font-semibold text-lg rounded-lg hover:bg-gray-100 hover:text-primary-dark transition-all duration-300"
>
<Rocket size={20} />
Jetzt kostenlos starten
</button>
</div>
</section>
)
}
+92
View File
@@ -0,0 +1,92 @@
import { useState } from 'react'
import { ChevronDown } from 'lucide-react'
interface FAQItem {
question: string
answer: string
}
const faqs: FAQItem[] = [
{
question: 'Wie funktioniert der KI-Chatbot?',
answer:
'Unser Chatbot nutzt modernste KI-Technologie (RAG - Retrieval Augmented Generation). Sie laden Ihre Dokumente hoch, und der Bot durchsucht diese, um präzise Antworten auf Kundenfragen zu geben. Die KI versteht den Kontext und formuliert natürliche Antworten.',
},
{
question: 'Welche Dateiformate werden unterstützt?',
answer:
'Aktuell unterstützen wir PDF-Dateien. Weitere Formate wie Word, Excel und Textdateien sind in Planung. Sie können beliebig viele PDFs hochladen FAQs, Produktkataloge, Anleitungen, etc.',
},
{
question: 'Ist der Service DSGVO-konform?',
answer:
'Ja, 100%! Alle Daten werden ausschließlich auf Servern in Deutschland gehostet. Wir verarbeiten keine Daten außerhalb der EU. Sie erhalten einen Auftragsverarbeitungsvertrag (AVV) auf Anfrage.',
},
{
question: 'Kann ich den Chatbot an mein Design anpassen?',
answer:
'Ja! Im Starter- und Business-Plan können Sie Farben, Logo und Begrüßungstext anpassen. Der Chatbot fügt sich nahtlos in Ihr Website-Design ein.',
},
{
question: 'Was passiert nach der Trial-Phase?',
answer:
'Nach 7 Tagen endet Ihre Trial automatisch. Sie können jederzeit auf einen bezahlten Plan upgraden. Wenn Sie innerhalb der ersten 3 Tage upgraden, erhalten Sie 30% Rabatt!',
},
{
question: 'Wie integriere ich den Chatbot in meine Website?',
answer:
'Ganz einfach! Sie erhalten einen Code-Snippet, den Sie vor dem </body>-Tag Ihrer Website einfügen. Das funktioniert mit WordPress, Shopify, Wix, und jeder anderen Website.',
},
]
export default function FAQ() {
const [openIndex, setOpenIndex] = useState<number | null>(null)
const toggle = (idx: number) => {
setOpenIndex((prev) => (prev === idx ? null : idx))
}
return (
<section id="faq" className="py-[100px] bg-white">
<div className="max-w-container mx-auto px-6">
{/* Section Header */}
<div className="text-center max-w-[700px] mx-auto mb-[60px]">
<span
className="inline-block px-4 py-1.5 text-white text-xs font-semibold uppercase tracking-wider rounded-full mb-4"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)' }}
>
FAQ
</span>
<h2 className="text-[clamp(2rem,4vw,3rem)] font-bold text-gray-900 mb-4">
Häufig gestellte Fragen
</h2>
<p className="text-lg text-gray-600">Alles was Sie wissen müssen</p>
</div>
{/* Accordion */}
<div className="max-w-[800px] mx-auto">
{faqs.map((item, idx) => (
<div key={idx} className="border-b border-gray-200">
<button
onClick={() => toggle(idx)}
className="w-full flex items-center justify-between py-6 bg-transparent border-none cursor-pointer text-left text-lg font-semibold text-gray-900 hover:text-primary transition-colors duration-150"
>
<span>{item.question}</span>
<ChevronDown
size={16}
className={`text-gray-400 flex-shrink-0 transition-transform duration-300 ${
openIndex === idx ? 'rotate-180' : ''
}`}
/>
</button>
<div className={`faq-answer ${openIndex === idx ? 'open' : ''}`}>
<p className="pb-6 text-gray-600 leading-[1.7]">{item.answer}</p>
</div>
</div>
))}
</div>
</div>
</section>
)
}
+90
View File
@@ -0,0 +1,90 @@
import { FileText, Brain, Code2, Shield, Palette, BarChart2 } from 'lucide-react'
import type { ReactNode } from 'react'
interface Feature {
icon: ReactNode
title: string
description: string
}
const features: Feature[] = [
{
icon: <FileText size={28} />,
title: 'PDF-Upload',
description:
'Laden Sie einfach Ihre Dokumente hoch. Der Chatbot lernt automatisch aus Ihren Inhalten.',
},
{
icon: <Brain size={28} />,
title: 'KI-gestützte Antworten',
description:
'Modernste KI-Technologie für natürliche und präzise Antworten auf Kundenfragen.',
},
{
icon: <Code2 size={28} />,
title: 'Einfache Integration',
description:
'Ein Zeile Code mehr brauchen Sie nicht. Funktioniert mit jeder Website.',
},
{
icon: <Shield size={28} />,
title: 'DSGVO-konform',
description:
'Alle Daten werden in Deutschland gehostet. 100% DSGVO-konform.',
},
{
icon: <Palette size={28} />,
title: 'Anpassbares Design',
description:
'Passen Sie Farben und Stil an Ihre Marke an. Ihr Chatbot, Ihr Look.',
},
{
icon: <BarChart2 size={28} />,
title: 'Analytics Dashboard',
description:
'Verstehen Sie, was Ihre Kunden fragen. Detaillierte Statistiken und Insights.',
},
]
export default function Features() {
return (
<section id="features" className="py-[100px] bg-white">
<div className="max-w-container mx-auto px-6">
{/* Section Header */}
<div className="text-center max-w-[700px] mx-auto mb-[60px]">
<span
className="inline-block px-4 py-1.5 text-white text-xs font-semibold uppercase tracking-wider rounded-full mb-4"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)' }}
>
Features
</span>
<h2 className="text-[clamp(2rem,4vw,3rem)] font-bold text-gray-900 mb-4">
Alles was Sie für einen erfolgreichen Chatbot brauchen
</h2>
<p className="text-lg text-gray-600">
Leistungsstarke Funktionen, die Ihren Kundenservice revolutionieren
</p>
</div>
{/* Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((f) => (
<div
key={f.title}
className="p-8 bg-white border border-gray-200 rounded-2xl transition-all duration-300 hover:-translate-y-1 hover:shadow-lg hover:border-primary-light"
>
<div
className="w-14 h-14 rounded-xl flex items-center justify-center text-white mb-5"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)' }}
>
{f.icon}
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">{f.title}</h3>
<p className="text-[0.9375rem] text-gray-600">{f.description}</p>
</div>
))}
</div>
</div>
</section>
)
}
+114
View File
@@ -0,0 +1,114 @@
import { Heart, Linkedin, Twitter, Github } from 'lucide-react'
export default function Footer() {
return (
<footer className="pt-[80px] pb-[40px] bg-gray-900 text-gray-400">
<div className="max-w-container mx-auto px-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-[2fr_1fr_1fr_1fr] gap-12 mb-12">
{/* Brand */}
<div>
<a
href="#"
className="flex items-center gap-3 text-2xl font-bold text-white mb-4 no-underline"
onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }) }}
>
<img
src="/logo.svg"
alt="BotKonzept Logo"
className="h-10 w-auto"
style={{ filter: 'brightness(0) invert(1)' }}
/>
<span>BotKonzept</span>
</a>
<p className="mb-6 leading-relaxed">
Ihr intelligenter KI-Chatbot für besseren Kundenservice.
</p>
<div className="flex gap-4">
{[
{ icon: <Linkedin size={18} />, label: 'LinkedIn' },
{ icon: <Twitter size={18} />, label: 'Twitter' },
{ icon: <Github size={18} />, label: 'GitHub' },
].map((s) => (
<a
key={s.label}
href="#"
aria-label={s.label}
className="w-10 h-10 bg-gray-800 rounded-lg flex items-center justify-center text-gray-400 hover:bg-primary hover:text-white transition-all duration-300"
>
{s.icon}
</a>
))}
</div>
</div>
{/* Produkt */}
<FooterLinks
title="Produkt"
links={[
{ label: 'Features', href: '#features' },
{ label: 'Preise', href: '#pricing' },
{ label: 'FAQ', href: '#faq' },
{ label: 'Dokumentation', href: '#' },
]}
/>
{/* Unternehmen */}
<FooterLinks
title="Unternehmen"
links={[
{ label: 'Über uns', href: '#' },
{ label: 'Blog', href: '#' },
{ label: 'Karriere', href: '#' },
{ label: 'Kontakt', href: '#' },
]}
/>
{/* Rechtliches */}
<FooterLinks
title="Rechtliches"
links={[
{ label: 'Impressum', href: '#' },
{ label: 'Datenschutz', href: '#' },
{ label: 'AGB', href: '#' },
{ label: 'Cookie-Einstellungen', href: '#' },
]}
/>
</div>
{/* Bottom Bar */}
<div className="flex flex-col sm:flex-row justify-between items-center gap-3 pt-8 border-t border-gray-800 text-sm">
<p>© 2025 BotKonzept. Alle Rechte vorbehalten.</p>
<p className="flex items-center gap-1">
Made with <Heart size={14} className="text-red-500" /> in Germany
</p>
</div>
</div>
</footer>
)
}
function FooterLinks({
title,
links,
}: {
title: string
links: { label: string; href: string }[]
}) {
return (
<div>
<h4 className="text-white font-semibold text-base mb-5">{title}</h4>
<ul className="space-y-3">
{links.map((l) => (
<li key={l.label}>
<a
href={l.href}
className="text-gray-400 hover:text-white transition-colors duration-150"
>
{l.label}
</a>
</li>
))}
</ul>
</div>
)
}
+158
View File
@@ -0,0 +1,158 @@
import { Rocket, Play, Info, Bot, Circle, Send } from 'lucide-react'
export default function Hero() {
const scrollTo = (id: string) => {
const el = document.getElementById(id)
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - 80
window.scrollTo({ top, behavior: 'smooth' })
}
}
return (
<section
className="relative pt-[160px] pb-[100px] overflow-hidden"
style={{ background: 'linear-gradient(180deg, #f9fafb 0%, #ffffff 100%)' }}
>
{/* Background Orbs */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div
className="orb"
style={{ width: 600, height: 600, background: '#818cf8', top: -200, right: -100 }}
/>
<div
className="orb"
style={{ width: 400, height: 400, background: '#0ea5e9', bottom: -100, left: -100 }}
/>
<div
className="orb"
style={{
width: 300,
height: 300,
background: '#f59e0b',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
/>
</div>
<div className="max-w-container mx-auto px-6 relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-[60px] items-center">
{/* Content */}
<div>
{/* Badge */}
<div className="inline-flex items-center gap-2 px-4 py-2 bg-primary-light text-white rounded-full text-sm font-semibold mb-6">
<Rocket size={12} />
<span>7 Tage kostenlos testen</span>
</div>
<h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold text-gray-900 leading-tight mb-6">
Ihr intelligenter{' '}
<span className="gradient-text">KI-Chatbot</span>{' '}
für die Website
</h1>
<p className="text-xl text-gray-600 mb-8 max-w-[540px]">
Laden Sie einfach Ihre PDFs hoch und Ihr Chatbot beantwortet Kundenfragen
automatisch. Keine Programmierung erforderlich in 5 Minuten einsatzbereit.
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 mb-12">
<button
onClick={() => scrollTo('register')}
className="btn-gradient inline-flex items-center justify-center gap-2 px-8 py-4 text-lg rounded-lg"
>
<Play size={18} />
Jetzt kostenlos starten
</button>
<button
onClick={() => scrollTo('how-it-works')}
className="inline-flex items-center justify-center gap-2 px-8 py-4 text-lg font-semibold rounded-lg border-2 border-primary text-primary hover:bg-primary hover:text-white transition-all duration-300"
>
<Info size={18} />
Mehr erfahren
</button>
</div>
{/* Stats */}
<div className="flex gap-12 flex-wrap">
{[
{ number: '5 Min', label: 'Setup-Zeit' },
{ number: '100%', label: 'DSGVO-konform' },
{ number: '24/7', label: 'Verfügbar' },
].map((stat) => (
<div key={stat.label} className="text-center">
<span className="block text-[2rem] font-extrabold text-gray-900 leading-none">
{stat.number}
</span>
<span className="text-sm text-gray-500">{stat.label}</span>
</div>
))}
</div>
</div>
{/* Chat Preview Widget */}
<div className="flex justify-center lg:justify-end">
<div className="bg-white rounded-3xl shadow-xl overflow-hidden w-full max-w-[400px]">
{/* Chat Header */}
<div
className="flex items-center gap-3 px-5 py-4 text-white"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)' }}
>
<div className="w-11 h-11 bg-white/20 rounded-full flex items-center justify-center text-xl">
<Bot size={22} />
</div>
<div className="flex flex-col">
<span className="font-semibold text-sm">BotKonzept Assistent</span>
<span className="text-xs opacity-90 flex items-center gap-1">
<Circle size={7} className="fill-emerald-400 text-emerald-400" />
Online
</span>
</div>
</div>
{/* Messages */}
<div className="px-5 py-5 flex flex-col gap-3 min-h-[280px]">
<div className="max-w-[85%] bg-gray-100 px-4 py-3 rounded-2xl rounded-bl-sm text-[0.9375rem] self-start">
Hallo! 👋 Wie kann ich Ihnen heute helfen?
</div>
<div className="max-w-[85%] bg-primary text-white px-4 py-3 rounded-2xl rounded-br-sm text-[0.9375rem] self-end">
Was sind Ihre Öffnungszeiten?
</div>
<div className="max-w-[85%] bg-gray-100 px-4 py-3 rounded-2xl rounded-bl-sm text-[0.9375rem] self-start">
Unsere Öffnungszeiten sind Montag bis Freitag von 9:00 bis 18:00 Uhr. Am
Wochenende sind wir geschlossen.
</div>
<div className="max-w-[85%] bg-primary text-white px-4 py-3 rounded-2xl rounded-br-sm text-[0.9375rem] self-end">
Wie kann ich eine Bestellung aufgeben?
</div>
{/* Typing Indicator */}
<div className="max-w-[85%] bg-gray-100 px-5 py-4 rounded-2xl rounded-bl-sm self-start flex gap-1 items-center">
<span className="typing-dot" />
<span className="typing-dot" />
<span className="typing-dot" />
</div>
</div>
{/* Chat Input */}
<div className="flex items-center gap-3 px-5 py-4 border-t border-gray-200">
<input
type="text"
placeholder="Nachricht eingeben..."
className="flex-1 px-4 py-3 border border-gray-300 rounded-full text-[0.9375rem] outline-none focus:border-primary transition-colors"
readOnly
/>
<button
className="w-11 h-11 rounded-full text-white flex items-center justify-center flex-shrink-0 hover:scale-105 transition-transform"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)' }}
>
<Send size={16} />
</button>
</div>
</div>
</div>
</div>
</section>
)
}
+102
View File
@@ -0,0 +1,102 @@
import { useState, useEffect } from 'react'
import { Menu, X } from 'lucide-react'
export default function Navbar() {
const [mobileOpen, setMobileOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 20)
window.addEventListener('scroll', onScroll)
return () => window.removeEventListener('scroll', onScroll)
}, [])
const scrollTo = (id: string) => {
setMobileOpen(false)
const el = document.getElementById(id)
if (el) {
const offset = 80
const top = el.getBoundingClientRect().top + window.scrollY - offset
window.scrollTo({ top, behavior: 'smooth' })
}
}
return (
<nav
className={`fixed top-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-md border-b border-gray-200 transition-shadow duration-300 ${
scrolled ? 'shadow-md' : ''
}`}
>
<div className="max-w-container mx-auto px-6 flex items-center justify-between h-[72px]">
{/* Logo */}
<a
href="#"
className="flex items-center gap-3 text-2xl font-bold text-gray-900 no-underline"
onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }) }}
>
<img src="/logo.svg" alt="BotKonzept Logo" className="h-10 w-auto" />
<span>BotKonzept</span>
</a>
{/* Desktop Links */}
<div className="hidden md:flex items-center gap-8">
{[
{ label: 'Features', id: 'features' },
{ label: "So funktioniert's", id: 'how-it-works' },
{ label: 'Preise', id: 'pricing' },
{ label: 'FAQ', id: 'faq' },
].map((link) => (
<button
key={link.id}
onClick={() => scrollTo(link.id)}
className="text-gray-600 font-medium hover:text-primary transition-colors duration-150 bg-transparent border-none cursor-pointer text-base"
>
{link.label}
</button>
))}
<button
onClick={() => scrollTo('register')}
className="btn-gradient px-4 py-2 text-sm rounded-lg"
>
Kostenlos testen
</button>
</div>
{/* Mobile Toggle */}
<button
className="md:hidden text-gray-700 bg-transparent border-none cursor-pointer"
onClick={() => setMobileOpen((v) => !v)}
aria-label="Menü öffnen"
>
{mobileOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
{/* Mobile Menu */}
{mobileOpen && (
<div className="md:hidden bg-white border-b border-gray-200 shadow-lg px-6 py-5 flex flex-col gap-4">
{[
{ label: 'Features', id: 'features' },
{ label: "So funktioniert's", id: 'how-it-works' },
{ label: 'Preise', id: 'pricing' },
{ label: 'FAQ', id: 'faq' },
].map((link) => (
<button
key={link.id}
onClick={() => scrollTo(link.id)}
className="text-gray-600 font-medium hover:text-primary transition-colors text-left bg-transparent border-none cursor-pointer text-base"
>
{link.label}
</button>
))}
<button
onClick={() => scrollTo('register')}
className="btn-gradient px-4 py-2 text-sm rounded-lg text-center"
>
Kostenlos testen
</button>
</div>
)}
</nav>
)
}
+202
View File
@@ -0,0 +1,202 @@
import { Check, X, Clock, Gift } from 'lucide-react'
interface PricingPlan {
name: string
price: string
period: string
description: string
features: { text: string; included: boolean }[]
cta: string
featured?: boolean
note?: string
}
const plans: PricingPlan[] = [
{
name: 'Trial',
price: '0',
period: '/7 Tage',
description: 'Perfekt zum Testen',
features: [
{ text: '100 Dokumente', included: true },
{ text: '1.000 Nachrichten', included: true },
{ text: '1 Chatbot', included: true },
{ text: 'Standard Support', included: true },
{ text: 'Custom Branding', included: false },
{ text: 'Analytics', included: false },
],
cta: 'Kostenlos starten',
},
{
name: 'Starter',
price: '49',
period: '/Monat',
description: 'Für kleine Unternehmen',
features: [
{ text: 'Unbegrenzte Dokumente', included: true },
{ text: '10.000 Nachrichten/Monat', included: true },
{ text: '1 Chatbot', included: true },
{ text: 'Prioritäts-Support', included: true },
{ text: 'Custom Branding', included: true },
{ text: 'Analytics Dashboard', included: true },
],
cta: 'Jetzt starten',
featured: true,
note: '30% Rabatt bei Upgrade innerhalb von 3 Tagen',
},
{
name: 'Business',
price: '149',
period: '/Monat',
description: 'Für wachsende Teams',
features: [
{ text: 'Unbegrenzte Dokumente', included: true },
{ text: '50.000 Nachrichten/Monat', included: true },
{ text: '5 Chatbots', included: true },
{ text: 'Dedizierter Support', included: true },
{ text: 'API-Zugriff', included: true },
{ text: 'SLA-Garantie', included: true },
],
cta: 'Kontakt aufnehmen',
},
]
const timeline = [
{ days: 'Tag 1-3', discount: '30% Rabatt', price: '€34,30/Monat', active: true },
{ days: 'Tag 4-5', discount: '15% Rabatt', price: '€41,65/Monat', active: false },
{ days: 'Tag 6-7', discount: 'Normalpreis', price: '€49/Monat', active: false },
]
export default function Pricing() {
const scrollToRegister = () => {
const el = document.getElementById('register')
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - 80
window.scrollTo({ top, behavior: 'smooth' })
}
}
return (
<section id="pricing" className="py-[100px] bg-white">
<div className="max-w-container mx-auto px-6">
{/* Section Header */}
<div className="text-center max-w-[700px] mx-auto mb-[60px]">
<span
className="inline-block px-4 py-1.5 text-white text-xs font-semibold uppercase tracking-wider rounded-full mb-4"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)' }}
>
Preise
</span>
<h2 className="text-[clamp(2rem,4vw,3rem)] font-bold text-gray-900 mb-4">
Transparente Preise, keine versteckten Kosten
</h2>
<p className="text-lg text-gray-600">
Starten Sie kostenlos und upgraden Sie, wenn Sie bereit sind
</p>
</div>
{/* Pricing Cards */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-[60px] max-w-[400px] lg:max-w-none mx-auto">
{plans.map((plan) => (
<div
key={plan.name}
className={`relative p-10 bg-white rounded-3xl border-2 transition-all duration-300 ${
plan.featured
? 'border-primary shadow-xl lg:scale-105'
: 'border-gray-200 hover:border-primary-light'
}`}
>
{plan.featured && (
<div
className="absolute -top-3 left-1/2 -translate-x-1/2 px-5 py-1.5 text-white text-xs font-semibold uppercase rounded-full"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)' }}
>
Beliebt
</div>
)}
<div className="text-center mb-8">
<h3 className="text-2xl font-bold text-gray-900 mb-4">{plan.name}</h3>
<div className="flex items-baseline justify-center gap-1 mb-2">
<span className="text-2xl font-semibold text-gray-600"></span>
<span className="text-[3.5rem] font-extrabold text-gray-900 leading-none">
{plan.price}
</span>
<span className="text-base text-gray-500">{plan.period}</span>
</div>
<p className="text-[0.9375rem] text-gray-600">{plan.description}</p>
</div>
<ul className="mb-8 space-y-0">
{plan.features.map((f) => (
<li
key={f.text}
className={`flex items-center gap-3 py-3 border-b border-gray-100 last:border-0 text-[0.9375rem] ${
f.included ? 'text-gray-700' : 'text-gray-400'
}`}
>
{f.included ? (
<Check size={14} className="text-emerald-500 flex-shrink-0" />
) : (
<X size={14} className="text-gray-400 flex-shrink-0" />
)}
{f.text}
</li>
))}
</ul>
<button
onClick={scrollToRegister}
className={`w-full py-3 px-6 rounded-lg font-semibold transition-all duration-300 ${
plan.featured
? 'btn-gradient text-white'
: 'border-2 border-primary text-primary hover:bg-primary hover:text-white'
}`}
>
{plan.cta}
</button>
{plan.note && (
<p className="text-center mt-4 text-sm text-gray-600 flex items-center justify-center gap-1 flex-wrap">
<Gift size={14} className="text-amber-500 flex-shrink-0" />
<span className="bg-amber-500 text-white px-2 py-0.5 rounded text-xs font-semibold">
30% Rabatt
</span>
bei Upgrade innerhalb von 3 Tagen
</p>
)}
</div>
))}
</div>
{/* Discount Timeline */}
<div className="max-w-[800px] mx-auto bg-gray-50 rounded-3xl p-10">
<h3 className="text-center text-xl font-bold text-gray-900 mb-8 flex items-center justify-center gap-3">
<Clock size={22} className="text-primary" />
Frühbucher-Rabatte
</h3>
<div className="relative flex flex-col sm:flex-row justify-between gap-6 sm:gap-0">
{/* Connector line (desktop only) */}
<div className="hidden sm:block absolute top-3 left-[50px] right-[50px] h-1 bg-gray-300" />
{timeline.map((item) => (
<div
key={item.days}
className="flex flex-col items-center text-center relative z-10"
>
<div
className={`w-6 h-6 rounded-full border-4 border-white mb-4 ${
item.active ? 'bg-emerald-500' : 'bg-gray-300'
}`}
/>
<span className="font-semibold text-gray-700 text-sm">{item.days}</span>
<span className="text-primary font-semibold text-sm">{item.discount}</span>
<span className="text-xl font-bold text-gray-900">{item.price}</span>
</div>
))}
</div>
</div>
</div>
</section>
)
}
@@ -0,0 +1,323 @@
import { useState } from 'react'
import { CheckCircle, AlertCircle, Check } from 'lucide-react'
type FormStatus = 'idle' | 'loading' | 'success' | 'error'
interface FormData {
firstName: string
lastName: string
email: string
company: string
website: string
privacy: boolean
newsletter: boolean
}
const WEBHOOK_URL = 'https://n8n.zq0.de/webhook-test/test'
export default function RegistrationSection() {
const [status, setStatus] = useState<FormStatus>('idle')
const [errorMessage, setErrorMessage] = useState('')
const [responseData, setResponseData] = useState<unknown>(null)
const [form, setForm] = useState<FormData>({
firstName: '',
lastName: '',
email: '',
company: '',
website: '',
privacy: false,
newsletter: false,
})
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target
setForm((prev) => ({ ...prev, [name]: type === 'checkbox' ? checked : value }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setStatus('loading')
setErrorMessage('')
try {
const res = await fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
firstName: form.firstName,
lastName: form.lastName,
email: form.email,
company: form.company || undefined,
website: form.website || undefined,
newsletter: form.newsletter,
timestamp: new Date().toISOString(),
source: 'landing-page',
userAgent: navigator.userAgent,
language: navigator.language,
}),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const contentType = res.headers.get('content-type') ?? ''
let data: unknown = null
if (contentType.includes('application/json')) {
data = await res.json()
} else {
const text = await res.text()
data = text || null
}
setResponseData(data)
setStatus('success')
} catch (err) {
console.error('[Webhook-Fehler]', err)
setErrorMessage(err instanceof Error ? err.message : 'Unbekannter Fehler.')
setStatus('error')
}
}
const reset = () => {
setStatus('idle')
setResponseData(null)
setErrorMessage('')
setForm({
firstName: '',
lastName: '',
email: '',
company: '',
website: '',
privacy: false,
newsletter: false,
})
}
return (
<section id="register" className="py-[100px] bg-gray-50">
<div className="max-w-container mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-[60px] items-center">
{/* Info */}
<div className="lg:text-left">
<h2 className="text-[clamp(2rem,4vw,3rem)] font-bold text-gray-900 mb-4">
Starten Sie Ihre{' '}
<span className="gradient-text">7-Tage Trial</span>
</h2>
<p className="text-lg text-gray-600 mb-8">
Keine Kreditkarte erforderlich. Voller Funktionsumfang. Jederzeit kündbar.
</p>
<ul className="space-y-0">
{[
'Sofortiger Zugang',
'Keine Zahlungsdaten nötig',
'Persönlicher Support',
'Alle Features inklusive',
].map((benefit) => (
<li key={benefit} className="flex items-center gap-3 py-3 text-gray-700">
<Check size={20} className="text-emerald-500 flex-shrink-0" />
{benefit}
</li>
))}
</ul>
</div>
{/* Form Card */}
<div className="bg-white rounded-3xl shadow-lg p-10">
{status === 'idle' || status === 'loading' ? (
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
{/* Name Row */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
<label htmlFor="firstName" className="text-sm font-medium text-gray-700">
Vorname *
</label>
<input
id="firstName"
name="firstName"
type="text"
required
placeholder="Max"
value={form.firstName}
onChange={handleChange}
className="px-4 py-3 border border-gray-300 rounded-lg text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10 transition-colors"
/>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="lastName" className="text-sm font-medium text-gray-700">
Nachname *
</label>
<input
id="lastName"
name="lastName"
type="text"
required
placeholder="Mustermann"
value={form.lastName}
onChange={handleChange}
className="px-4 py-3 border border-gray-300 rounded-lg text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10 transition-colors"
/>
</div>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="email" className="text-sm font-medium text-gray-700">
E-Mail-Adresse *
</label>
<input
id="email"
name="email"
type="email"
required
placeholder="max@beispiel.de"
value={form.email}
onChange={handleChange}
className="px-4 py-3 border border-gray-300 rounded-lg text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10 transition-colors"
/>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="company" className="text-sm font-medium text-gray-700">
Unternehmen (optional)
</label>
<input
id="company"
name="company"
type="text"
placeholder="Muster GmbH"
value={form.company}
onChange={handleChange}
className="px-4 py-3 border border-gray-300 rounded-lg text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10 transition-colors"
/>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="website" className="text-sm font-medium text-gray-700">
Website (optional)
</label>
<input
id="website"
name="website"
type="url"
placeholder="https://www.beispiel.de"
value={form.website}
onChange={handleChange}
className="px-4 py-3 border border-gray-300 rounded-lg text-base focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10 transition-colors"
/>
</div>
{/* Privacy Checkbox */}
<div className="flex items-start gap-3">
<input
id="privacy"
name="privacy"
type="checkbox"
required
checked={form.privacy}
onChange={handleChange}
className="w-4.5 h-4.5 mt-0.5 accent-primary flex-shrink-0"
style={{ width: 18, height: 18, accentColor: '#6366f1' }}
/>
<label htmlFor="privacy" className="text-sm text-gray-600 leading-snug">
Ich akzeptiere die{' '}
<a href="#" className="text-primary hover:text-primary-dark">
Datenschutzerklärung
</a>{' '}
und{' '}
<a href="#" className="text-primary hover:text-primary-dark">
AGB
</a>{' '}
*
</label>
</div>
{/* Newsletter Checkbox */}
<div className="flex items-start gap-3">
<input
id="newsletter"
name="newsletter"
type="checkbox"
checked={form.newsletter}
onChange={handleChange}
style={{ width: 18, height: 18, accentColor: '#6366f1', flexShrink: 0 }}
/>
<label htmlFor="newsletter" className="text-sm text-gray-600 leading-snug">
Ich möchte Updates und Tipps per E-Mail erhalten (optional)
</label>
</div>
<button
type="submit"
disabled={status === 'loading'}
className="btn-gradient w-full py-4 text-lg rounded-lg flex items-center justify-center gap-3 disabled:opacity-75 disabled:cursor-not-allowed mt-2"
>
{status === 'loading' ? (
<>
<span className="spinner" />
<span>Wird erstellt</span>
</>
) : (
'Kostenlos registrieren'
)}
</button>
{status === 'loading' && (
<p className="text-sm text-gray-500 text-center -mt-2">
Ihre Instanz wird erstellt dies kann bis zu 2 Minuten dauern.
</p>
)}
</form>
) : status === 'success' ? (
<SuccessCard data={responseData} />
) : (
<ErrorCard message={errorMessage} onReset={reset} />
)}
</div>
</div>
</div>
</section>
)
}
/* ---- Success Card ---- */
function SuccessCard({ data }: { data: unknown }) {
const message =
typeof data === 'string'
? data
: typeof data === 'object' && data !== null && 'message' in data
? String((data as { message: unknown }).message)
: null
return (
<div className="text-center py-4">
<CheckCircle size={64} className="text-emerald-500 mx-auto mb-5" />
<h3 className="text-2xl font-bold text-gray-900 mb-3">Willkommen bei BotKonzept!</h3>
{message ? (
<p className="text-gray-700 text-base leading-relaxed">{message}</p>
) : (
<p className="text-gray-600">
Ihre Registrierung wurde erfolgreich abgeschickt. Sie erhalten in Kürze eine E-Mail.
</p>
)}
</div>
)
}
/* ---- Error Card ---- */
function ErrorCard({ message, onReset }: { message: string; onReset: () => void }) {
return (
<div className="text-center py-4">
<AlertCircle size={64} className="text-red-500 mx-auto mb-5" />
<h3 className="text-2xl font-bold text-gray-900 mb-3">Ups, etwas ist schiefgelaufen</h3>
<div className="bg-red-50 border border-red-200 rounded-xl px-5 py-4 mb-8 text-left">
<p className="text-red-800 text-sm font-mono break-words">
{message || 'Bitte versuchen Sie es später erneut.'}
</p>
</div>
<button
onClick={onReset}
className="border-2 border-primary text-primary px-6 py-2 rounded-lg font-semibold hover:bg-primary hover:text-white transition-all duration-300"
>
Erneut versuchen
</button>
</div>
)
}
+100
View File
@@ -0,0 +1,100 @@
import { UserPlus, Upload, Code2 } from 'lucide-react'
import type { ReactNode } from 'react'
interface Step {
number: number
title: string
description: string
icon: ReactNode
}
const steps: Step[] = [
{
number: 1,
title: 'Registrieren',
description:
'Erstellen Sie Ihr kostenloses Konto in weniger als einer Minute. Keine Kreditkarte erforderlich.',
icon: <UserPlus size={40} />,
},
{
number: 2,
title: 'PDFs hochladen',
description:
'Laden Sie Ihre Dokumente hoch FAQs, Produktinfos, Anleitungen. Der Bot lernt automatisch.',
icon: <Upload size={40} />,
},
{
number: 3,
title: 'Code einbinden',
description:
'Kopieren Sie den Code-Snippet und fügen Sie ihn in Ihre Website ein. Fertig!',
icon: <Code2 size={40} />,
},
]
export default function Steps() {
return (
<section id="how-it-works" className="py-[100px] bg-gray-50">
<div className="max-w-container mx-auto px-6">
{/* Section Header */}
<div className="text-center max-w-[700px] mx-auto mb-[60px]">
<span
className="inline-block px-4 py-1.5 text-white text-xs font-semibold uppercase tracking-wider rounded-full mb-4"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)' }}
>
So funktioniert's
</span>
<h2 className="text-[clamp(2rem,4vw,3rem)] font-bold text-gray-900 mb-4">
In 3 einfachen Schritten zum eigenen KI-Chatbot
</h2>
<p className="text-lg text-gray-600">Keine technischen Kenntnisse erforderlich</p>
</div>
{/* Steps */}
<div className="flex flex-col md:flex-row items-start justify-center mb-[60px]">
{steps.map((step, idx) => (
<>
<div
key={step.number}
className="flex flex-col items-center text-center max-w-[280px] px-5"
>
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white text-xl font-bold mb-5 flex-shrink-0"
style={{ background: 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)' }}
>
{step.number}
</div>
<h3 className="text-[1.375rem] font-bold text-gray-900 mb-2">{step.title}</h3>
<p className="text-[0.9375rem] text-gray-600">{step.description}</p>
<div className="mt-5 text-primary-light">{step.icon}</div>
</div>
{/* Connector */}
{idx < steps.length - 1 && (
<div
key={`connector-${idx}`}
className="hidden md:block w-[100px] h-0.5 bg-gray-300 mt-6 flex-shrink-0"
/>
)}
</>
))}
</div>
{/* Code Preview */}
<div className="max-w-[600px] mx-auto bg-gray-900 rounded-2xl overflow-hidden shadow-xl">
<div className="flex items-center gap-2 px-4 py-3 bg-gray-800">
<span className="w-3 h-3 rounded-full bg-[#ff5f56]" />
<span className="w-3 h-3 rounded-full bg-[#ffbd2e]" />
<span className="w-3 h-3 rounded-full bg-[#27ca40]" />
<span className="ml-auto text-gray-400 text-xs">Ihr Embed-Code</span>
</div>
<pre className="px-5 py-5 overflow-x-auto m-0">
<code className="text-emerald-400 font-mono text-sm">
{`<script src="https://botkonzept.de/embed/IHRE-ID.js"></script>`}
</code>
</pre>
</div>
</div>
</section>
)
}
@@ -0,0 +1,23 @@
import { Building2 } from 'lucide-react'
const companies = ['Unternehmen A', 'Unternehmen B', 'Unternehmen C', 'Unternehmen D']
export default function TrustedBy() {
return (
<section className="py-[60px] bg-white border-b border-gray-200">
<div className="max-w-container mx-auto px-6">
<p className="text-center text-gray-500 text-sm uppercase tracking-widest mb-6">
Vertraut von innovativen Unternehmen
</p>
<div className="flex justify-center items-center gap-12 flex-wrap">
{companies.map((name) => (
<div key={name} className="text-gray-400 text-sm flex items-center gap-2">
<Building2 size={16} />
{name}
</div>
))}
</div>
</div>
</section>
)
}
+98
View File
@@ -0,0 +1,98 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@apply text-gray-700 bg-white;
}
}
@layer utilities {
.gradient-text {
background: linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.bg-gradient-primary {
background: linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%);
}
.btn-gradient {
background: linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%);
@apply text-white font-semibold rounded-lg transition-all duration-300;
}
.btn-gradient:hover {
@apply -translate-y-0.5 shadow-lg;
}
}
/* Typing-Dots-Animation */
@keyframes typing-bounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-8px); }
}
.typing-dot {
width: 8px;
height: 8px;
background: #9ca3af;
border-radius: 50%;
animation: typing-bounce 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
/* Gradient Orbs */
.orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
pointer-events: none;
}
/* FAQ-Accordion-Transition */
.faq-answer {
max-height: 0;
overflow: hidden;
transition: max-height 0.35s ease;
}
.faq-answer.open {
max-height: 600px;
}
/* Spinner */
@keyframes spin-anim {
to { transform: rotate(360deg); }
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin-anim 0.8s linear infinite;
display: inline-block;
}
/* Scroll-Reveal */
.scroll-animate {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.scroll-animate.visible {
opacity: 1;
transform: translateY(0);
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+53
View File
@@ -0,0 +1,53 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#6366f1',
dark: '#4f46e5',
light: '#818cf8',
},
secondary: '#0ea5e9',
accent: '#f59e0b',
success: '#10b981',
error: '#ef4444',
},
fontFamily: {
sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
},
backgroundImage: {
'gradient-primary': 'linear-gradient(135deg, #6366f1 0%, #0ea5e9 100%)',
'gradient-hero': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
},
maxWidth: {
container: '1200px',
},
keyframes: {
typing: {
'0%, 60%, 100%': { transform: 'translateY(0)' },
'30%': { transform: 'translateY(-8px)' },
},
fadeInUp: {
from: { opacity: '0', transform: 'translateY(30px)' },
to: { opacity: '1', transform: 'translateY(0)' },
},
spin: {
to: { transform: 'rotate(360deg)' },
},
},
animation: {
typing: 'typing 1.4s infinite ease-in-out',
'typing-2': 'typing 1.4s 0.2s infinite ease-in-out',
'typing-3': 'typing 1.4s 0.4s infinite ease-in-out',
'fade-in-up': 'fadeInUp 0.6s ease forwards',
'spin-slow': 'spin 0.8s linear infinite',
},
boxShadow: {
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
},
},
},
plugins: [],
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})