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:
tony
2025-08-26 13:57:30 +10:00
parent 630d1c26ad
commit c8fb816775
236 changed files with 17525 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
'use client'
import { useState, useCallback } from 'react'
export type LeadSourceType = 'early_access_waitlist' | 'request_early_access'
export interface EarlyAccessLead {
firstName: string
lastName: string
email: string
companyName?: string
companyRole?: string
interestLevel: 'technical_evaluation' | 'strategic_demo' | 'general_interest'
leadSource: LeadSourceType
gdprConsent: boolean
marketingConsent: boolean
}
export interface EarlyAccessResponse {
success: boolean
message: string
leadId?: string
error?: string
}
export function useEarlyAccessCapture() {
const [isModalOpen, setIsModalOpen] = useState(false)
const [currentLeadSource, setCurrentLeadSource] = useState<LeadSourceType>('early_access_waitlist')
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle')
const [errorMessage, setErrorMessage] = useState('')
const openModal = useCallback((leadSource: LeadSourceType) => {
setCurrentLeadSource(leadSource)
setIsModalOpen(true)
setSubmitStatus('idle')
setErrorMessage('')
}, [])
const closeModal = useCallback(() => {
setIsModalOpen(false)
setSubmitStatus('idle')
setErrorMessage('')
}, [])
const submitEarlyAccess = useCallback(async (leadData: EarlyAccessLead): Promise<EarlyAccessResponse> => {
setIsSubmitting(true)
setSubmitStatus('idle')
setErrorMessage('')
try {
const response = await fetch('/api/early-access', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...leadData,
countryCode: 'AU', // Default to Australia based on business location
customMessage: `Interest Level: ${leadData.interestLevel}`,
}),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || `HTTP error! status: ${response.status}`)
}
setSubmitStatus('success')
// Auto-close modal after success
setTimeout(() => {
closeModal()
}, 2000)
return {
success: true,
message: data.message || 'Successfully joined the waitlist!',
leadId: data.leadId,
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Failed to submit request'
setErrorMessage(errorMsg)
setSubmitStatus('error')
return {
success: false,
message: 'Failed to join waitlist',
error: errorMsg,
}
} finally {
setIsSubmitting(false)
}
}, [closeModal])
const handleSuccess = useCallback((leadId: string) => {
console.log('Early access lead captured:', leadId)
// Track analytics event
if (typeof window !== 'undefined' && (window as any).gtag) {
(window as any).gtag('event', 'early_access_signup', {
event_category: 'conversion',
event_label: 'early_access_waitlist',
value: 1
})
}
}, [])
return {
isModalOpen,
currentLeadSource,
isSubmitting,
submitStatus,
errorMessage,
openModal,
closeModal,
submitEarlyAccess,
handleSuccess,
}
}

View File

@@ -0,0 +1,49 @@
'use client';
import { useEffect, useRef, useState } from 'react';
interface IntersectionObserverOptions {
threshold?: number;
rootMargin?: string;
triggerOnce?: boolean;
}
export function useIntersectionObserver(
options: IntersectionObserverOptions = {}
) {
const { threshold = 0.1, rootMargin = '0px 0px -100px 0px', triggerOnce = true } = options;
const [isIntersecting, setIsIntersecting] = useState(false);
const [hasIntersected, setHasIntersected] = useState(false);
const elementRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
const isVisible = entry.isIntersecting;
setIsIntersecting(isVisible);
if (isVisible && !hasIntersected) {
setHasIntersected(true);
}
},
{
threshold,
rootMargin,
}
);
observer.observe(element);
return () => {
observer.unobserve(element);
};
}, [threshold, rootMargin, hasIntersected]);
return {
elementRef,
isVisible: triggerOnce ? hasIntersected : isIntersecting,
};
}