feat: Add CHORUS teaser website with mobile-responsive design
- Created complete Next.js 15 teaser website with CHORUS brand styling - Implemented mobile-responsive 3D logo (128px mobile, 512px desktop) - Added proper Exo font loading via Next.js Google Fonts for iOS/Chrome compatibility - Built comprehensive early access form with GDPR compliance and rate limiting - Integrated PostgreSQL database with complete schema for lead capture - Added scroll indicators that auto-hide when scrolling begins - Optimized mobile modal forms with proper scrolling and submit button access - Deployed via Docker Swarm with Traefik SSL termination at chorus.services - Includes database migrations, consent tracking, and email notifications 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
276
modules/teaser/components/EarlyAccessForm.tsx
Normal file
276
modules/teaser/components/EarlyAccessForm.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
'use client'
|
||||
|
||||
import { useState, FormEvent } from 'react'
|
||||
import { EarlyAccessLead, useEarlyAccessCapture } from '../hooks/useEarlyAccessCapture'
|
||||
|
||||
import { LeadSourceType } from '../hooks/useEarlyAccessCapture'
|
||||
|
||||
interface EarlyAccessFormProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
leadSource: LeadSourceType
|
||||
}
|
||||
|
||||
export default function EarlyAccessForm({ isOpen, onClose, leadSource }: EarlyAccessFormProps) {
|
||||
const { submitEarlyAccess, isSubmitting, submitStatus, errorMessage } = useEarlyAccessCapture()
|
||||
|
||||
const [formData, setFormData] = useState<EarlyAccessLead>({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
companyName: '',
|
||||
companyRole: '',
|
||||
interestLevel: 'general_interest',
|
||||
leadSource: leadSource,
|
||||
gdprConsent: false,
|
||||
marketingConsent: false,
|
||||
})
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!formData.gdprConsent) {
|
||||
alert('Please accept the privacy policy to continue.')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await submitEarlyAccess(formData)
|
||||
|
||||
if (result.success) {
|
||||
// Reset form on success
|
||||
setFormData({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
companyName: '',
|
||||
companyRole: '',
|
||||
interestLevel: 'general_interest',
|
||||
leadSource: leadSource,
|
||||
gdprConsent: false,
|
||||
marketingConsent: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputChange = (field: keyof EarlyAccessLead, value: string | boolean) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}))
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm overflow-y-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="relative w-full max-w-md bg-gradient-to-b from-mulberry-900 to-carbon-900 text-white p-4 sm:p-chorus-xl rounded-lg border border-mulberry-700/50 shadow-2xl my-8 max-h-[90vh] overflow-y-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 text-mulberry-300 hover:text-white text-2xl font-light transition-colors duration-200"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
{/* Form Header */}
|
||||
<h3 className="text-h3 font-semibold text-white mb-chorus-sm">
|
||||
{leadSource === 'request_early_access'
|
||||
? 'Request Early Access to CHORUS'
|
||||
: 'Join the CHORUS Waitlist'}
|
||||
</h3>
|
||||
<p className="text-sm text-mulberry-200 font-light mb-4 sm:mb-chorus-xl">
|
||||
{leadSource === 'request_early_access'
|
||||
? 'Get priority access to contextual AI orchestration'
|
||||
: 'Be notified when CHORUS becomes available'}
|
||||
</p>
|
||||
|
||||
{/* Success State */}
|
||||
{submitStatus === 'success' ? (
|
||||
<div className="text-center py-chorus-xl">
|
||||
<div className="text-5xl text-eucalyptus-400 mb-chorus-lg animate-bounce">
|
||||
✓
|
||||
</div>
|
||||
<h4 className="text-lg font-semibold text-eucalyptus-300 mb-chorus-sm">
|
||||
{leadSource === 'request_early_access'
|
||||
? 'Request submitted successfully!'
|
||||
: 'Welcome to the waitlist!'}
|
||||
</h4>
|
||||
<p className="text-sm text-mulberry-200 font-light">
|
||||
{leadSource === 'request_early_access'
|
||||
? 'We\'ll prioritize your request and contact you soon.'
|
||||
: 'We\'ll notify you when CHORUS becomes available.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/* Form */
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* Name Fields */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-chorus-sm sm:gap-chorus-md mb-chorus-md">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-mulberry-200 mb-chorus-xs">
|
||||
First Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="John"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-mulberry-200 mb-chorus-xs">
|
||||
Last Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div className="mb-chorus-md">
|
||||
<label className="block text-sm font-medium text-mulberry-200 mb-chorus-xs">
|
||||
Email Address *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="john@company.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Company Information */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-chorus-sm sm:gap-chorus-md mb-chorus-md">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-mulberry-200 mb-chorus-xs">
|
||||
Company
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.companyName || ''}
|
||||
onChange={(e) => handleInputChange('companyName', e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="Company Name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-mulberry-200 mb-chorus-xs">
|
||||
Role
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.companyRole || ''}
|
||||
onChange={(e) => handleInputChange('companyRole', e.target.value)}
|
||||
className="form-input"
|
||||
placeholder="CTO, Director, etc."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interest Level */}
|
||||
<div className="mb-4 sm:mb-chorus-lg">
|
||||
<label className="block text-sm font-medium text-mulberry-200 mb-chorus-xs">
|
||||
Primary Interest
|
||||
</label>
|
||||
<select
|
||||
value={formData.interestLevel}
|
||||
onChange={(e) => handleInputChange('interestLevel', e.target.value as any)}
|
||||
className="form-input"
|
||||
>
|
||||
<option value="general_interest">General Interest</option>
|
||||
<option value="technical_evaluation">Technical Evaluation</option>
|
||||
<option value="strategic_demo">Strategic Demo</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* GDPR Consent */}
|
||||
<div style={{ marginBottom: '1.5rem', fontSize: '0.85rem' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '0.5rem', marginBottom: '0.75rem' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.gdprConsent}
|
||||
onChange={(e) => handleInputChange('gdprConsent', e.target.checked)}
|
||||
required
|
||||
/>
|
||||
<span>
|
||||
I agree to the privacy policy and consent to processing my personal data for early access communications. *
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '0.5rem' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.marketingConsent}
|
||||
onChange={(e) => handleInputChange('marketingConsent', e.target.checked)}
|
||||
/>
|
||||
<span>
|
||||
I would like to receive updates about CHORUS Services and related products.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{submitStatus === 'error' && errorMessage && (
|
||||
<div style={{
|
||||
padding: '0.75rem',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
||||
border: '1px solid #dc2626',
|
||||
color: '#fca5a5',
|
||||
fontSize: '0.85rem',
|
||||
marginBottom: '1rem'
|
||||
}}>
|
||||
⚠ {errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !formData.gdprConsent}
|
||||
className="btn-primary"
|
||||
style={{
|
||||
width: '100%',
|
||||
opacity: (isSubmitting || !formData.gdprConsent) ? 0.5 : 1,
|
||||
cursor: (isSubmitting || !formData.gdprConsent) ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{isSubmitting
|
||||
? (leadSource === 'request_early_access' ? 'Submitting Request...' : 'Joining Waitlist...')
|
||||
: (leadSource === 'request_early_access' ? 'Submit Request' : 'Join Waitlist')
|
||||
}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<p style={{
|
||||
fontSize: '0.75rem',
|
||||
opacity: 0.6,
|
||||
textAlign: 'center',
|
||||
marginTop: '1.5rem',
|
||||
paddingTop: '1.5rem',
|
||||
borderTop: '1px solid #444'
|
||||
}}>
|
||||
By joining our waitlist, you'll receive exclusive early access and product updates.
|
||||
We respect your privacy and won't spam you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user