This commit is contained in:
Timo Knuth
2025-10-17 13:45:33 +02:00
parent cd3ee5fc8f
commit 254e6490b8
36 changed files with 1712 additions and 917 deletions

View File

@@ -0,0 +1,36 @@
import React from 'react';
import Link from 'next/link';
export interface BreadcrumbItem {
name: string;
url: string;
}
interface BreadcrumbsProps {
items: BreadcrumbItem[];
}
export default function Breadcrumbs({ items }: BreadcrumbsProps) {
return (
<nav aria-label="Breadcrumb" className="mb-6">
<ol className="flex items-center space-x-2 text-sm text-gray-600">
{items.map((item, index) => (
<li key={item.url} className="flex items-center">
{index > 0 && <span className="mx-2">/</span>}
{index === items.length - 1 ? (
<span className="font-semibold text-gray-900" aria-current="page">
{item.name}
</span>
) : (
<Link href={item.url} className="hover:text-blue-600 transition-colors">
{item.name}
</Link>
)}
</li>
))}
</ol>
</nav>
);
}
export { type BreadcrumbItem as BreadcrumbItemType };

View File

@@ -0,0 +1,23 @@
import React from 'react';
interface SeoJsonLdProps {
data: object | object[];
}
export default function SeoJsonLd({ data }: SeoJsonLdProps) {
const jsonLdArray = Array.isArray(data) ? data : [data];
return (
<>
{jsonLdArray.map((item, index) => (
<script
key={index}
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(item, null, 0),
}}
/>
))}
</>
);
}

View File

@@ -22,21 +22,21 @@ export const FAQ: React.FC<FAQProps> = ({ t }) => {
];
return (
<section className="py-16 bg-gray-50">
<section id="faq" className="py-16 bg-gray-50">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t('faq.title')}
{t.faq.title}
</h2>
</div>
<div className="max-w-3xl mx-auto space-y-4">
{questions.map((key, index) => (
<Card key={key} className="cursor-pointer" onClick={() => setOpenIndex(openIndex === index ? null : index)}>
<div className="p-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">
{t(`faq.questions.${key}.question`)}
{t.faq.questions[key].question}
</h3>
<svg
className={`w-5 h-5 text-gray-500 transition-transform ${openIndex === index ? 'rotate-180' : ''}`}
@@ -47,10 +47,10 @@ export const FAQ: React.FC<FAQProps> = ({ t }) => {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
{openIndex === index && (
<div className="mt-4 text-gray-600">
{t(`faq.questions.${key}.answer`)}
{t.faq.questions[key].answer}
</div>
)}
</div>

View File

@@ -27,42 +27,6 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
),
color: 'text-purple-600 bg-purple-100',
},
{
key: 'bulk',
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
),
color: 'text-green-600 bg-green-100',
},
{
key: 'integrations',
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z" />
</svg>
),
color: 'text-orange-600 bg-orange-100',
},
{
key: 'api',
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
),
color: 'text-indigo-600 bg-indigo-100',
},
{
key: 'support',
icon: (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
),
color: 'text-red-600 bg-red-100',
},
];
return (
@@ -70,10 +34,10 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t('features.title')}
{t.features.title}
</h2>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
{features.map((feature) => (
<Card key={feature.key} hover>
@@ -81,11 +45,11 @@ export const Features: React.FC<FeaturesProps> = ({ t }) => {
<div className={`w-12 h-12 rounded-lg ${feature.color} flex items-center justify-center mb-4`}>
{feature.icon}
</div>
<CardTitle>{t(`features.${feature.key}.title`)}</CardTitle>
<CardTitle>{t.features[feature.key].title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600">
{t(`features.${feature.key}.description`)}
{t.features[feature.key].description}
</p>
</CardContent>
</Card>

View File

@@ -25,20 +25,20 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
{/* Left Content */}
<div className="space-y-8">
<Badge variant="info" className="inline-flex items-center space-x-2">
<span>{t('hero.badge')}</span>
<span>{t.hero.badge}</span>
</Badge>
<div className="space-y-6">
<h1 className="text-5xl lg:text-6xl font-bold text-gray-900 leading-tight">
{t('hero.title')}
{t.hero.title}
</h1>
<p className="text-xl text-gray-600 leading-relaxed">
{t('hero.subtitle')}
{t.hero.subtitle}
</p>
<div className="space-y-3">
{t('hero.features', { returnObjects: true }).map((feature: string, index: number) => (
{t.hero.features.map((feature: string, index: number) => (
<div key={index} className="flex items-center space-x-3">
<div className="flex-shrink-0 w-5 h-5 bg-success-500 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
@@ -49,22 +49,22 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
</div>
))}
</div>
<div className="flex flex-col sm:flex-row gap-4">
<Link href="/signup">
<Button size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
{t('hero.cta_primary')}
{t.hero.cta_primary}
</Button>
</Link>
<Link href="/create">
<Button variant="outline" size="lg" className="text-lg px-8 py-4 w-full sm:w-auto">
{t('hero.cta_secondary')}
{t.hero.cta_secondary}
</Button>
</Link>
</div>
</div>
</div>
{/* Right Preview Widget */}
<div className="relative">
<div className="grid grid-cols-2 gap-4">
@@ -75,10 +75,10 @@ export const Hero: React.FC<HeroProps> = ({ t }) => {
</Card>
))}
</div>
{/* Floating Badge */}
<div className="absolute -top-4 -right-4 bg-success-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg">
{t('hero.engagement_badge')}
{t.hero.engagement_badge}
</div>
</div>
</div>

View File

@@ -0,0 +1,58 @@
'use client';
import React from 'react';
import { Hero } from '@/components/marketing/Hero';
import { StatsStrip } from '@/components/marketing/StatsStrip';
import { TemplateCards } from '@/components/marketing/TemplateCards';
import { InstantGenerator } from '@/components/marketing/InstantGenerator';
import { StaticVsDynamic } from '@/components/marketing/StaticVsDynamic';
import { Features } from '@/components/marketing/Features';
import { Pricing } from '@/components/marketing/Pricing';
import { FAQ } from '@/components/marketing/FAQ';
import { Button } from '@/components/ui/Button';
import en from '@/i18n/en.json';
export default function HomePageClient() {
// Always use English for marketing pages
const t = en;
const industries = [
'Restaurant Chain',
'Tech Startup',
'Real Estate',
'Event Agency',
'Retail Store',
'Healthcare',
];
return (
<>
<Hero t={t} />
<StatsStrip t={t} />
{/* Industry Buttons */}
<section className="py-8">
<div className="container mx-auto px-4">
<div className="flex flex-wrap justify-center gap-3">
{industries.map((industry) => (
<Button key={industry} variant="outline" size="sm">
{industry}
</Button>
))}
</div>
</div>
</section>
<TemplateCards t={t} />
<InstantGenerator t={t} />
<StaticVsDynamic t={t} />
<Features t={t} />
{/* Pricing Section */}
<Pricing t={t} />
{/* FAQ Section */}
<FAQ t={t} />
</>
);
}

View File

@@ -77,10 +77,10 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t('generator.title')}
{t.generator.title}
</h2>
</div>
<div className="grid lg:grid-cols-2 gap-12 max-w-6xl mx-auto">
{/* Left Form */}
<Card className="space-y-6">
@@ -88,13 +88,13 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
label="URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder={t('generator.url_placeholder')}
placeholder={t.generator.url_placeholder}
/>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('generator.foreground')}
{t.generator.foreground}
</label>
<div className="flex items-center space-x-2">
<input
@@ -110,10 +110,10 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('generator.background')}
{t.generator.background}
</label>
<div className="flex items-center space-x-2">
<input
@@ -134,7 +134,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('generator.corners')}
{t.generator.corners}
</label>
<select
value={cornerStyle}
@@ -145,10 +145,10 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
<option value="rounded">Rounded</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
{t('generator.size')}
{t.generator.size}
</label>
<input
type="range"
@@ -164,31 +164,31 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
<div className="flex items-center justify-between">
<Badge variant={hasGoodContrast ? 'success' : 'warning'}>
{hasGoodContrast ? t('generator.contrast_good') : 'Low contrast'}
{hasGoodContrast ? t.generator.contrast_good : 'Low contrast'}
</Badge>
<div className="text-sm text-gray-500">
Contrast: {contrast.toFixed(1)}:1
</div>
</div>
<div className="flex space-x-3">
<Button variant="outline" className="flex-1" onClick={() => downloadQR('svg')}>
{t('generator.download_svg')}
{t.generator.download_svg}
</Button>
<Button variant="outline" className="flex-1" onClick={() => downloadQR('png')}>
{t('generator.download_png')}
{t.generator.download_png}
</Button>
</div>
<Button className="w-full">
{t('generator.save_track')}
{t.generator.save_track}
</Button>
</Card>
{/* Right Preview */}
<div className="flex flex-col items-center justify-center">
<Card className="text-center p-8">
<h3 className="text-lg font-semibold mb-4">{t('generator.live_preview')}</h3>
<h3 className="text-lg font-semibold mb-4">{t.generator.live_preview}</h3>
<div id="instant-qr-preview" className="flex justify-center mb-4">
{url ? (
<div className={`${cornerStyle === 'rounded' ? 'rounded-lg overflow-hidden' : ''}`}>
@@ -210,7 +210,7 @@ export const InstantGenerator: React.FC<InstantGeneratorProps> = ({ t }) => {
)}
</div>
<div className="text-sm text-gray-600 mb-2">URL</div>
<div className="text-xs text-gray-500">{t('generator.demo_note')}</div>
<div className="text-xs text-gray-500">{t.generator.demo_note}</div>
</Card>
</div>
</div>

View File

@@ -26,48 +26,48 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
];
return (
<section className="py-16">
<section id="pricing" className="py-16">
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t('pricing.title')}
{t.pricing.title}
</h2>
<p className="text-xl text-gray-600">
{t('pricing.subtitle')}
{t.pricing.subtitle}
</p>
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
{plans.map((plan) => (
<Card
key={plan.key}
<Card
key={plan.key}
className={plan.popular ? 'border-primary-500 shadow-xl relative' : ''}
>
{plan.popular && (
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
<Badge variant="info" className="px-3 py-1">
{t(`pricing.${plan.key}.badge`)}
{t.pricing[plan.key].badge}
</Badge>
</div>
)}
<CardHeader className="text-center pb-8">
<CardTitle className="text-2xl mb-4">
{t(`pricing.${plan.key}.title`)}
{t.pricing[plan.key].title}
</CardTitle>
<div className="flex items-baseline justify-center">
<span className="text-4xl font-bold">
{t(`pricing.${plan.key}.price`)}
{t.pricing[plan.key].price}
</span>
<span className="text-gray-600 ml-2">
{t(`pricing.${plan.key}.period`)}
{t.pricing[plan.key].period}
</span>
</div>
</CardHeader>
<CardContent className="space-y-4">
<ul className="space-y-3">
{t(`pricing.${plan.key}.features`, { returnObjects: true }).map((feature: string, index: number) => (
{t.pricing[plan.key].features.map((feature: string, index: number) => (
<li key={index} className="flex items-start space-x-3">
<svg className="w-5 h-5 text-success-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
@@ -76,9 +76,9 @@ export const Pricing: React.FC<PricingProps> = ({ t }) => {
</li>
))}
</ul>
<Button
variant={plan.popular ? 'primary' : 'outline'}
<Button
variant={plan.popular ? 'primary' : 'outline'}
className="w-full"
size="lg"
>

View File

@@ -17,14 +17,14 @@ export const StaticVsDynamic: React.FC<StaticVsDynamicProps> = ({ t }) => {
<Card className="relative">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-2xl">{t('static_vs_dynamic.static.title')}</CardTitle>
<Badge variant="success">{t('static_vs_dynamic.static.subtitle')}</Badge>
<CardTitle className="text-2xl">{t.static_vs_dynamic.static.title}</CardTitle>
<Badge variant="success">{t.static_vs_dynamic.static.subtitle}</Badge>
</div>
<p className="text-gray-600">{t('static_vs_dynamic.static.description')}</p>
<p className="text-gray-600">{t.static_vs_dynamic.static.description}</p>
</CardHeader>
<CardContent>
<ul className="space-y-3">
{t('static_vs_dynamic.static.features', { returnObjects: true }).map((feature: string, index: number) => (
{t.static_vs_dynamic.static.features.map((feature: string, index: number) => (
<li key={index} className="flex items-center space-x-3">
<div className="flex-shrink-0 w-5 h-5 bg-gray-400 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
@@ -37,19 +37,19 @@ export const StaticVsDynamic: React.FC<StaticVsDynamicProps> = ({ t }) => {
</ul>
</CardContent>
</Card>
{/* Dynamic QR Codes */}
<Card className="relative border-primary-200 bg-primary-50">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-2xl">{t('static_vs_dynamic.dynamic.title')}</CardTitle>
<Badge variant="info">{t('static_vs_dynamic.dynamic.subtitle')}</Badge>
<CardTitle className="text-2xl">{t.static_vs_dynamic.dynamic.title}</CardTitle>
<Badge variant="info">{t.static_vs_dynamic.dynamic.subtitle}</Badge>
</div>
<p className="text-gray-600">{t('static_vs_dynamic.dynamic.description')}</p>
<p className="text-gray-600">{t.static_vs_dynamic.dynamic.description}</p>
</CardHeader>
<CardContent>
<ul className="space-y-3">
{t('static_vs_dynamic.dynamic.features', { returnObjects: true }).map((feature: string, index: number) => (
{t.static_vs_dynamic.dynamic.features.map((feature: string, index: number) => (
<li key={index} className="flex items-center space-x-3">
<div className="flex-shrink-0 w-5 h-5 bg-primary-500 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">

View File

@@ -8,10 +8,10 @@ interface StatsStripProps {
export const StatsStrip: React.FC<StatsStripProps> = ({ t }) => {
const stats = [
{ key: 'users', value: '10,000+', label: t('trust.users') },
{ key: 'codes', value: '500,000+', label: t('trust.codes') },
{ key: 'scans', value: '50M+', label: t('trust.scans') },
{ key: 'countries', value: '120+', label: t('trust.countries') },
{ key: 'users', value: '10,000+', label: t.trust.users },
{ key: 'codes', value: '500,000+', label: t.trust.codes },
{ key: 'scans', value: '50M+', label: t.trust.scans },
{ key: 'countries', value: '120+', label: t.trust.countries },
];
return (

View File

@@ -12,28 +12,28 @@ export const TemplateCards: React.FC<TemplateCardsProps> = ({ t }) => {
const templates = [
{
key: 'restaurant',
title: t('templates.restaurant'),
title: t.templates.restaurant,
icon: '🍽️',
color: 'bg-red-50 border-red-200',
iconBg: 'bg-red-100',
},
{
key: 'business',
title: t('templates.business'),
title: t.templates.business,
icon: '💼',
color: 'bg-blue-50 border-blue-200',
iconBg: 'bg-blue-100',
},
{
key: 'wifi',
title: t('templates.wifi'),
title: t.templates.wifi,
icon: '📶',
color: 'bg-purple-50 border-purple-200',
iconBg: 'bg-purple-100',
},
{
key: 'event',
title: t('templates.event'),
title: t.templates.event,
icon: '🎫',
color: 'bg-green-50 border-green-200',
iconBg: 'bg-green-100',
@@ -45,10 +45,10 @@ export const TemplateCards: React.FC<TemplateCardsProps> = ({ t }) => {
<div className="container mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-3xl lg:text-4xl font-bold text-gray-900 mb-4">
{t('templates.title')}
{t.templates.title}
</h2>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{templates.map((template) => (
<Card key={template.key} className={`${template.color} text-center hover:scale-105 transition-transform cursor-pointer`}>
@@ -59,7 +59,7 @@ export const TemplateCards: React.FC<TemplateCardsProps> = ({ t }) => {
{template.title}
</h3>
<Button variant="outline" size="sm" className="w-full">
{t('templates.use_template')}
{t.templates.use_template}
</Button>
</Card>
))}