xpeditis2.0/apps/frontend/app/[locale]/contact/page.tsx
David ec0173483a
All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m23s
Dev CI / Backend — Unit Tests (push) Successful in 10m17s
Dev CI / Frontend — Lint & Type-check (push) Successful in 11m3s
Dev CI / Frontend — Unit Tests (push) Successful in 10m33s
Dev CI / Notify Failure (push) Has been skipped
fix language
2026-04-21 18:04:02 +02:00

629 lines
27 KiB
TypeScript

'use client';
import { useState, useRef } from 'react';
import { useTranslations } from 'next-intl';
import { motion, useInView } from 'framer-motion';
import {
Mail,
Phone,
MapPin,
Clock,
Send,
MessageSquare,
Headphones,
Building2,
CheckCircle2,
Loader2,
Shield,
Zap,
BookOpen,
ArrowRight,
type LucideIcon,
} from 'lucide-react';
import { Link } from '@/i18n/navigation';
import { LandingHeader, LandingFooter } from '@/components/layout';
import { sendContactForm } from '@/lib/api/auth';
type MethodKey = 'email' | 'phone' | 'chat' | 'support';
type SubjectKey = 'demo' | 'pricing' | 'partnership' | 'support' | 'press' | 'careers' | 'other';
const METHODS: { key: MethodKey; icon: LucideIcon; color: string }[] = [
{ key: 'email', icon: Mail, color: 'from-blue-500 to-cyan-500' },
{ key: 'phone', icon: Phone, color: 'from-green-500 to-emerald-500' },
{ key: 'chat', icon: MessageSquare, color: 'from-purple-500 to-pink-500' },
{ key: 'support', icon: Headphones, color: 'from-orange-500 to-red-500' },
];
const SUBJECTS: SubjectKey[] = ['demo', 'pricing', 'partnership', 'support', 'press', 'careers', 'other'];
export default function ContactPage() {
const t = useTranslations('marketing.contact');
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
company: '',
phone: '',
subject: '',
message: '',
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const [error, setError] = useState('');
const heroRef = useRef(null);
const formRef = useRef(null);
const contactRef = useRef(null);
const afterSubmitRef = useRef(null);
const quickAccessRef = useRef(null);
const isHeroInView = useInView(heroRef, { once: true });
const isFormInView = useInView(formRef, { once: true });
const isContactInView = useInView(contactRef, { once: true });
const isAfterSubmitInView = useInView(afterSubmitRef, { once: true });
const isQuickAccessInView = useInView(quickAccessRef, { once: true });
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsSubmitting(true);
try {
await sendContactForm({
firstName: formData.firstName,
lastName: formData.lastName,
email: formData.email,
company: formData.company || undefined,
phone: formData.phone || undefined,
subject: formData.subject,
message: formData.message,
});
setIsSubmitted(true);
} catch (err: any) {
setError(err.message || t('form.genericError'));
} finally {
setIsSubmitting(false);
}
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const containerVariants = {
hidden: { opacity: 0, y: 50 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5 },
},
};
return (
<div className="min-h-screen bg-white">
<LandingHeader activePage="contact" />
{/* Hero Section */}
<section ref={heroRef} className="relative pt-32 pb-20 bg-gradient-to-br from-brand-navy to-brand-navy/95 overflow-hidden">
<div className="absolute inset-0 opacity-10">
<div className="absolute top-20 left-20 w-96 h-96 bg-brand-turquoise rounded-full blur-3xl" />
<div className="absolute bottom-20 right-20 w-96 h-96 bg-brand-green rounded-full blur-3xl" />
</div>
<div className="relative z-10 max-w-7xl mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isHeroInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
className="text-center"
>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={isHeroInView ? { scale: 1, opacity: 1 } : {}}
transition={{ duration: 0.6, delay: 0.2 }}
className="inline-flex items-center space-x-2 bg-white/10 backdrop-blur-sm px-4 py-2 rounded-full mb-8 border border-white/20"
>
<Mail className="w-5 h-5 text-brand-turquoise" />
<span className="text-white/90 text-sm font-medium">{t('badge')}</span>
</motion.div>
<h1 className="text-4xl lg:text-6xl font-bold text-white mb-6 leading-tight">
{t('title1')}
<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-brand-turquoise to-brand-green">
{t('title2')}
</span>
</h1>
<p className="text-xl text-white/80 mb-10 max-w-3xl mx-auto leading-relaxed">
{t('intro')}
</p>
</motion.div>
</div>
{/* Wave */}
<div className="absolute bottom-0 left-0 right-0">
<svg className="w-full h-16" viewBox="0 0 1440 60" preserveAspectRatio="none">
<path
d="M0,30 C240,50 480,10 720,30 C960,50 1200,10 1440,30 L1440,60 L0,60 Z"
fill="white"
/>
</svg>
</div>
</section>
{/* Contact Methods */}
<section ref={contactRef} className="py-16 bg-gray-50">
<motion.div
variants={containerVariants}
initial="hidden"
animate={isContactInView ? 'visible' : 'hidden'}
className="max-w-7xl mx-auto px-6 lg:px-8"
>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{METHODS.map((method) => {
const IconComponent = method.icon;
return (
<motion.div
key={method.key}
variants={itemVariants}
className="bg-white p-6 rounded-2xl shadow-lg border border-gray-100"
>
<div
className={`w-12 h-12 rounded-xl bg-gradient-to-br ${method.color} flex items-center justify-center mb-4`}
>
<IconComponent className="w-6 h-6 text-white" />
</div>
<h3 className="text-lg font-bold text-brand-navy mb-1">{t(`methods.${method.key}.title`)}</h3>
<p className="text-gray-500 text-sm mb-2">{t(`methods.${method.key}.description`)}</p>
<p className="text-brand-turquoise font-medium">{t(`methods.${method.key}.value`)}</p>
</motion.div>
);
})}
</div>
</motion.div>
</section>
{/* Contact Form & Info */}
<section ref={formRef} className="py-20">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Form */}
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={isFormInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8 }}
>
<h2 className="text-3xl font-bold text-brand-navy mb-6">{t('form.title')}</h2>
<p className="text-gray-600 mb-8">
{t('form.description')}
</p>
{isSubmitted ? (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="bg-green-50 border border-green-200 rounded-2xl p-8 text-center"
>
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle2 className="w-8 h-8 text-green-600" />
</div>
<h3 className="text-2xl font-bold text-green-800 mb-2">{t('form.successTitle')}</h3>
<p className="text-green-700 mb-6">
{t('form.successBody')}
</p>
<button
onClick={() => {
setIsSubmitted(false);
setFormData({
firstName: '',
lastName: '',
email: '',
company: '',
phone: '',
subject: '',
message: '',
});
}}
className="text-brand-turquoise font-medium hover:underline"
>
{t('form.sendAnother')}
</button>
</motion.div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-gray-700 mb-2">
{t('form.firstName')} *
</label>
<input
type="text"
id="firstName"
name="firstName"
required
value={formData.firstName}
onChange={handleChange}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-brand-turquoise focus:border-transparent transition-all"
placeholder={t('form.firstNamePlaceholder')}
/>
</div>
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-gray-700 mb-2">
{t('form.lastName')} *
</label>
<input
type="text"
id="lastName"
name="lastName"
required
value={formData.lastName}
onChange={handleChange}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-brand-turquoise focus:border-transparent transition-all"
placeholder={t('form.lastNamePlaceholder')}
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
{t('form.email')} *
</label>
<input
type="email"
id="email"
name="email"
required
value={formData.email}
onChange={handleChange}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-brand-turquoise focus:border-transparent transition-all"
placeholder={t('form.emailPlaceholder')}
/>
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-2">
{t('form.phone')}
</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-brand-turquoise focus:border-transparent transition-all"
placeholder={t('form.phonePlaceholder')}
/>
</div>
</div>
<div>
<label htmlFor="company" className="block text-sm font-medium text-gray-700 mb-2">
{t('form.company')}
</label>
<input
type="text"
id="company"
name="company"
value={formData.company}
onChange={handleChange}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-brand-turquoise focus:border-transparent transition-all"
placeholder={t('form.companyPlaceholder')}
/>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-2">
{t('form.subject')} *
</label>
<select
id="subject"
name="subject"
required
value={formData.subject}
onChange={handleChange}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-brand-turquoise focus:border-transparent transition-all"
>
<option value="">{t('subjects.placeholder')}</option>
{SUBJECTS.map((key) => (
<option key={key} value={key}>
{t(`subjects.${key}`)}
</option>
))}
</select>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-2">
{t('form.message')} *
</label>
<textarea
id="message"
name="message"
required
rows={5}
value={formData.message}
onChange={handleChange}
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-brand-turquoise focus:border-transparent transition-all resize-none"
placeholder={t('form.messagePlaceholder')}
/>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
{error}
</div>
)}
<button
type="submit"
disabled={isSubmitting}
className="w-full px-8 py-4 bg-brand-turquoise text-white rounded-lg hover:bg-brand-turquoise/90 transition-all font-semibold text-lg flex items-center justify-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>{t('form.submitting')}</span>
</>
) : (
<>
<Send className="w-5 h-5" />
<span>{t('form.submit')}</span>
</>
)}
</button>
</form>
)}
</motion.div>
{/* Offices */}
<motion.div
initial={{ opacity: 0, x: 50 }}
animate={isFormInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }}
>
<h2 className="text-3xl font-bold text-brand-navy mb-6">{t('office.title')}</h2>
<p className="text-gray-600 mb-8">
{t('office.subtitle')}
</p>
<div className="space-y-6">
<div className="bg-white p-6 rounded-2xl border-2 border-brand-turquoise shadow-lg">
<div className="flex items-start space-x-4">
<div className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 bg-brand-turquoise">
<Building2 className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<div className="flex items-center space-x-2 mb-2">
<h3 className="text-xl font-bold text-brand-navy">{t('office.city')}</h3>
<span className="px-2 py-1 bg-brand-turquoise/10 text-brand-turquoise text-xs font-medium rounded-full">
{t('office.hqBadge')}
</span>
</div>
<div className="space-y-2 text-gray-600">
<div className="flex items-center space-x-2">
<MapPin className="w-4 h-4 text-gray-400" />
<span>{t('office.address')}</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-gray-400 ml-6">{t('office.postalCode')}</span>
</div>
<div className="flex items-center space-x-2">
<Phone className="w-4 h-4 text-gray-400" />
<a href={`tel:${t('office.phone').replace(/\s/g, '')}`} className="hover:text-brand-turquoise">
{t('office.phone')}
</a>
</div>
<div className="flex items-center space-x-2">
<Mail className="w-4 h-4 text-gray-400" />
<a href={`mailto:${t('office.email')}`} className="hover:text-brand-turquoise">
{t('office.email')}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Hours */}
<div className="mt-8 bg-gray-50 p-6 rounded-2xl">
<div className="flex items-center space-x-3 mb-4">
<Clock className="w-6 h-6 text-brand-turquoise" />
<h3 className="text-lg font-bold text-brand-navy">{t('hours.title')}</h3>
</div>
<div className="space-y-2 text-gray-600">
<div className="flex justify-between">
<span>{t('hours.weekdays')}</span>
<span className="font-medium">{t('hours.weekdaysHours')}</span>
</div>
<div className="flex justify-between">
<span>{t('hours.saturday')}</span>
<span className="font-medium">{t('hours.saturdayHours')}</span>
</div>
<div className="flex justify-between">
<span>{t('hours.sunday')}</span>
<span className="font-medium text-gray-400">{t('hours.closed')}</span>
</div>
</div>
<p className="mt-4 text-sm text-gray-500">
{t('hours.supportNote')}
</p>
</div>
</motion.div>
</div>
</div>
</section>
{/* Section 1 : After submit */}
<section ref={afterSubmitRef} className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={isAfterSubmitInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
className="relative bg-gradient-to-br from-brand-navy to-brand-navy/90 rounded-3xl overflow-hidden p-8 lg:p-12"
>
<div className="absolute inset-0 opacity-10 pointer-events-none">
<div className="absolute -top-10 -left-10 w-64 h-64 bg-brand-turquoise rounded-full blur-3xl" />
<div className="absolute -bottom-10 -right-10 w-64 h-64 bg-brand-green rounded-full blur-3xl" />
</div>
<div className="relative z-10">
<div className="flex items-center space-x-3 mb-2">
<div className="p-2 bg-brand-turquoise/20 rounded-lg">
<Mail className="w-5 h-5 text-brand-turquoise" />
</div>
<span className="text-brand-turquoise font-semibold uppercase tracking-widest text-xs">
{t('afterSubmit.badge')}
</span>
</div>
<h2 className="text-2xl lg:text-3xl font-bold text-white mb-8">
{t('afterSubmit.title')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Commitment */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isAfterSubmitInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2 }}
className="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20"
>
<div className="flex items-center space-x-3 mb-4">
<div className="w-10 h-10 bg-brand-turquoise/30 rounded-xl flex items-center justify-center flex-shrink-0">
<CheckCircle2 className="w-5 h-5 text-brand-turquoise" />
</div>
<h3 className="text-lg font-bold text-white">{t('afterSubmit.commitmentTitle')}</h3>
</div>
<p className="text-white/80 leading-relaxed">
{t('afterSubmit.commitmentBody1')}
<span className="text-brand-turquoise font-semibold">
{t('afterSubmit.commitmentHighlight')}
</span>
</p>
</motion.div>
{/* Security */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isAfterSubmitInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.35 }}
className="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20"
>
<div className="flex items-center space-x-3 mb-4">
<div className="w-10 h-10 bg-brand-green/30 rounded-xl flex items-center justify-center flex-shrink-0">
<Shield className="w-5 h-5 text-brand-green" />
</div>
<h3 className="text-lg font-bold text-white">{t('afterSubmit.securityTitle')}</h3>
</div>
<p className="text-white/80 leading-relaxed">
{t('afterSubmit.securityBody1')}
<Link href="/privacy" className="text-brand-turquoise font-semibold hover:underline">
{t('afterSubmit.privacyLink')}
</Link>
{t('afterSubmit.securityBody2')}
</p>
</motion.div>
</div>
</div>
</motion.div>
</div>
</section>
{/* Section 2: Quick access */}
<section ref={quickAccessRef} className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isQuickAccessInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7 }}
>
<div className="text-center mb-10">
<span className="text-brand-turquoise font-semibold uppercase tracking-widest text-xs">
{t('quickAccess.badge')}
</span>
<h2 className="text-2xl lg:text-3xl font-bold text-brand-navy mt-2">
{t('quickAccess.title')}
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
{/* Instant pricing */}
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={isQuickAccessInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, delay: 0.15 }}
whileHover={{ y: -4 }}
className="bg-white rounded-2xl shadow-lg border border-gray-100 p-8 flex flex-col"
>
<div className="w-14 h-14 bg-gradient-to-br from-brand-turquoise to-cyan-400 rounded-2xl flex items-center justify-center mb-6 flex-shrink-0">
<Zap className="w-7 h-7 text-white" />
</div>
<h3 className="text-xl font-bold text-brand-navy mb-3">{t('quickAccess.pricingTitle')}</h3>
<p className="text-gray-600 leading-relaxed flex-1 mb-6">
{t('quickAccess.pricingBody1')}
<span className="font-semibold text-brand-navy">{t('quickAccess.pricingHighlight')}</span>
{t('quickAccess.pricingBody2')}
</p>
<Link
href="/dashboard"
className="inline-flex items-center justify-center space-x-2 px-6 py-3 bg-brand-turquoise text-white rounded-xl font-semibold hover:bg-brand-turquoise/90 transition-all group"
>
<span>{t('quickAccess.pricingCta')}</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</Link>
</motion.div>
{/* Wiki */}
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={isQuickAccessInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.6, delay: 0.25 }}
whileHover={{ y: -4 }}
className="bg-white rounded-2xl shadow-lg border border-gray-100 p-8 flex flex-col"
>
<div className="w-14 h-14 bg-gradient-to-br from-brand-navy to-brand-navy/80 rounded-2xl flex items-center justify-center mb-6 flex-shrink-0">
<BookOpen className="w-7 h-7 text-brand-turquoise" />
</div>
<h3 className="text-xl font-bold text-brand-navy mb-3">{t('quickAccess.wikiTitle')}</h3>
<p className="text-gray-600 leading-relaxed flex-1 mb-6">
{t('quickAccess.wikiBody1')}
<span className="font-semibold text-brand-navy">{t('quickAccess.wikiHighlight')}</span>
{t('quickAccess.wikiBody2')}
</p>
<Link
href="/dashboard/wiki"
className="inline-flex items-center justify-center space-x-2 px-6 py-3 border-2 border-brand-navy text-brand-navy rounded-xl font-semibold hover:bg-brand-navy hover:text-white transition-all group"
>
<span>{t('quickAccess.wikiCta')}</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</Link>
</motion.div>
</div>
</motion.div>
</div>
</section>
<LandingFooter />
</div>
);
}