RLS policies implementeren, Demo auth flow

This commit is contained in:
colinislit
2025-11-15 23:59:38 +01:00
parent b1cfb339a2
commit 99804656d7
49 changed files with 5841 additions and 123 deletions

View File

@@ -0,0 +1,326 @@
'use client'
import { useState } from 'react'
import { Send, CheckCircle, XCircle } from 'lucide-react'
import type { FormSection } from '@/content/schemas/manifesto'
import type { LeadFormData } from '@/app/api/leads/route'
interface ContactFormProps {
content: FormSection
}
type FormStatus = 'idle' | 'submitting' | 'success' | 'error'
export function ContactForm({ content }: ContactFormProps) {
const [status, setStatus] = useState<FormStatus>('idle')
const [errors, setErrors] = useState<Record<string, string>>({})
const [formData, setFormData] = useState<LeadFormData>({
name: '',
email: '',
company: '',
projectType: '',
budget: '',
message: '',
})
const validateField = (name: keyof LeadFormData, value: string): string | null => {
const field = content.fields[name]
if (field.required && !value.trim()) {
return field.error || `${field.label} is verplicht`
}
if (name === 'email' && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) {
return 'Ongeldig email adres'
}
}
if (name === 'message' && value && field.minLength) {
if (value.length < field.minLength) {
return `Minimaal ${field.minLength} karakters vereist`
}
}
return null
}
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
// Clear error on change
if (errors[name]) {
setErrors(prev => {
const newErrors = { ...prev }
delete newErrors[name]
return newErrors
})
}
}
const handleBlur = (
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target
const error = validateField(name as keyof LeadFormData, value)
if (error) {
setErrors(prev => ({ ...prev, [name]: error }))
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Validate all fields
const newErrors: Record<string, string> = {}
Object.keys(formData).forEach(key => {
const error = validateField(key as keyof LeadFormData, formData[key as keyof LeadFormData] || '')
if (error) {
newErrors[key] = error
}
})
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
// Submit form
setStatus('submitting')
try {
const response = await fetch('/api/leads', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Er ging iets mis')
}
setStatus('success')
// Reset form
setFormData({
name: '',
email: '',
company: '',
projectType: '',
budget: '',
message: '',
})
} catch (error) {
console.error('Form submission error:', error)
setStatus('error')
}
}
// Success state
if (status === 'success') {
return (
<div className="bg-green-50 border-2 border-green-200 rounded-lg p-8 text-center">
<CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
<h3 className="text-2xl font-bold text-slate-900 mb-2">
{content.success.title}
</h3>
<p className="text-slate-600 mb-6">
{content.success.message}
</p>
<a
href="/"
className="inline-block px-6 py-3 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors"
>
{content.success.cta}
</a>
</div>
)
}
// Error state
if (status === 'error') {
return (
<div className="bg-red-50 border-2 border-red-200 rounded-lg p-8 text-center">
<XCircle className="w-16 h-16 text-red-600 mx-auto mb-4" />
<h3 className="text-2xl font-bold text-slate-900 mb-2">
{content.error.title}
</h3>
<p className="text-slate-600 mb-6">
{content.error.message}
</p>
<button
onClick={() => setStatus('idle')}
className="inline-block px-6 py-3 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors"
>
{content.error.retry}
</button>
</div>
)
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Name */}
<div>
<label htmlFor="name" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.name.label} {content.fields.name.required && <span className="text-red-500">*</span>}
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
onBlur={handleBlur}
placeholder={content.fields.name.placeholder}
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors ${
errors.name ? 'border-red-500' : 'border-slate-300'
}`}
disabled={status === 'submitting'}
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
{/* Email */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.email.label} {content.fields.email.required && <span className="text-red-500">*</span>}
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
placeholder={content.fields.email.placeholder}
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors ${
errors.email ? 'border-red-500' : 'border-slate-300'
}`}
disabled={status === 'submitting'}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
{/* Company */}
<div>
<label htmlFor="company" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.company.label}
</label>
<input
type="text"
id="company"
name="company"
value={formData.company}
onChange={handleChange}
placeholder={content.fields.company.placeholder}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors"
disabled={status === 'submitting'}
/>
</div>
{/* Project Type */}
<div>
<label htmlFor="projectType" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.projectType.label} {content.fields.projectType.required && <span className="text-red-500">*</span>}
</label>
<select
id="projectType"
name="projectType"
value={formData.projectType}
onChange={handleChange}
onBlur={handleBlur}
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors ${
errors.projectType ? 'border-red-500' : 'border-slate-300'
}`}
disabled={status === 'submitting'}
>
<option value="">{content.fields.projectType.placeholder}</option>
{content.fields.projectType.options?.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
{errors.projectType && (
<p className="mt-1 text-sm text-red-600">{errors.projectType}</p>
)}
</div>
{/* Budget */}
<div>
<label htmlFor="budget" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.budget.label}
</label>
<select
id="budget"
name="budget"
value={formData.budget}
onChange={handleChange}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors"
disabled={status === 'submitting'}
>
<option value="">{content.fields.budget.placeholder}</option>
{content.fields.budget.options?.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
{/* Message */}
<div>
<label htmlFor="message" className="block text-sm font-medium text-slate-700 mb-2">
{content.fields.message.label} {content.fields.message.required && <span className="text-red-500">*</span>}
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
onBlur={handleBlur}
placeholder={content.fields.message.placeholder}
rows={6}
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors ${
errors.message ? 'border-red-500' : 'border-slate-300'
}`}
disabled={status === 'submitting'}
/>
{errors.message && (
<p className="mt-1 text-sm text-red-600">{errors.message}</p>
)}
<p className="mt-1 text-sm text-slate-500">
{formData.message.length} / {content.fields.message.minLength} minimum
</p>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={status === 'submitting'}
className="w-full px-8 py-4 bg-green-600 hover:bg-green-700 disabled:bg-slate-400 text-white font-medium rounded-lg transition-colors flex items-center justify-center gap-2"
>
{status === 'submitting' ? (
<>
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
{content.buttons.submitting}
</>
) : (
<>
<Send className="w-5 h-5" />
{content.buttons.submit}
</>
)}
</button>
</form>
)
}

View File

@@ -0,0 +1,146 @@
/**
* Contact Page
*
* Lead capture form with benefits and FAQ
*/
import type { Metadata } from 'next'
import { getContent } from '@/lib/content/loader'
import type { ContactContent } from '@/content/schemas/manifesto'
import { Clock, Euro, Code, Eye, ChevronDown } from 'lucide-react'
import { ContactForm } from './contact-form'
// Generate metadata for SEO
export async function generateMetadata(): Promise<Metadata> {
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://aispeedrun.vercel.app'
return {
title: 'Contact - AI Speedrun',
description: 'Van idee naar werkend prototype in 4 weken voor €200. Start je speedrun vandaag.',
openGraph: {
type: 'website',
title: 'Contact - AI Speedrun',
description: 'Van idee naar werkend prototype in 4 weken voor €200',
images: [`${siteUrl}/og-image.png`],
siteName: 'AI Speedrun',
locale: 'nl_NL',
},
alternates: {
canonical: `${siteUrl}/contact`,
},
}
}
export default async function ContactPage() {
const content = await getContent<ContactContent>('nl', 'contact')
const iconMap = {
clock: Clock,
euro: Euro,
code: Code,
eye: Eye,
}
return (
<>
{/* Hero Section */}
<section className="relative min-h-[40vh] flex items-center justify-center bg-gradient-to-br from-green-50 to-white px-4 pt-32 pb-16">
<div className="max-w-3xl mx-auto text-center">
<h1 className="text-4xl md:text-5xl font-bold text-slate-900 mb-4">
{content.hero.title}
</h1>
<p className="text-xl md:text-2xl text-green-600 font-medium mb-4">
{content.hero.subtitle}
</p>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
{content.hero.description}
</p>
</div>
</section>
{/* Main Content - Two Column Layout */}
<section className="py-16 px-4">
<div className="max-w-6xl mx-auto grid md:grid-cols-2 gap-12">
{/* Left Column - Form */}
<div>
<h2 className="text-3xl font-bold text-slate-900 mb-6">
{content.form.title}
</h2>
<ContactForm content={content.form} />
</div>
{/* Right Column - Benefits & FAQ */}
<div className="space-y-12">
{/* Benefits */}
<div>
<h3 className="text-2xl font-bold text-slate-900 mb-6">
{content.benefits.title}
</h3>
<div className="space-y-4">
{content.benefits.items.map((benefit, index) => {
const Icon = iconMap[benefit.icon as keyof typeof iconMap] || Clock
return (
<div key={index} className="flex gap-4">
<div className="flex-shrink-0 w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
<Icon className="w-6 h-6 text-green-600" />
</div>
<div>
<h4 className="font-semibold text-slate-900 mb-1">
{benefit.title}
</h4>
<p className="text-slate-600 text-sm">
{benefit.description}
</p>
</div>
</div>
)
})}
</div>
</div>
{/* FAQ */}
<div>
<h3 className="text-2xl font-bold text-slate-900 mb-6">
{content.faq.title}
</h3>
<div className="space-y-4">
{content.faq.items.map((item, index) => (
<details
key={index}
className="group bg-slate-50 rounded-lg border border-slate-200 overflow-hidden"
>
<summary className="flex justify-between items-center cursor-pointer px-6 py-4 hover:bg-slate-100 transition-colors">
<span className="font-medium text-slate-900">
{item.question}
</span>
<ChevronDown className="w-5 h-5 text-slate-500 group-open:rotate-180 transition-transform" />
</summary>
<div className="px-6 py-4 border-t border-slate-200 text-slate-600">
{item.answer}
</div>
</details>
))}
</div>
</div>
{/* Additional CTA */}
<div className="bg-gradient-to-br from-green-50 to-white border-2 border-green-200 rounded-lg p-6 text-center">
<p className="text-slate-700 mb-4">
<strong>Nog vragen?</strong> Stuur een email naar{' '}
<a
href="mailto:contact@speedrun.nl"
className="text-green-600 hover:text-green-700 underline"
>
contact@speedrun.nl
</a>
</p>
<p className="text-sm text-slate-600">
We reageren binnen 24 uur
</p>
</div>
</div>
</div>
</section>
</>
)
}