feat: Epic 1 completion - Marketing refactor met timeline en Bento Grid login
Epic 1 Stories (E1.S1 t/m E1.S6): ✨ E1.S1 - Verwijder EPD demo pagina - Removed /epd route en credentials-box component - Updated navigation: EPD Prototype → Login link - Removed demo_users references from middleware en auth libs - Cleaned up TypeScript errors in hero-section-2 ✨ E1.S2 - Homepage vereenvoudigen - Replaced lange manifesto (8000+ woorden) met statement section - Added problem-solution-proof format (3 paragrafen) - Removed comparison table, experiment CTA, manifesto content - Timeline placeholder toegevoegd (completed in E1.S3) ✨ E1.S3 - Timeline component integreren - Created BuildTimeline component (Aceternity UI pattern) - Added timeline.json met 4 weken data (Week 1 completed) - Features per week met icons, metrics, achievements - Scroll-based animation met Framer Motion - Responsive design (sticky titles, mobile/desktop layouts) ✨ E1.S4 - Timeline content structuur - Structured JSON data in content/nl/timeline.json - 15 Lucide icons voor feature types - Week status badges (completed/in_progress/planned) - Time savings display (< 5 sec vs 30 min) ✨ E1.S5 - Login pagina refactor - Split-screen layout (60% features, 40% login) - Teal gradient showcase met 4 feature cards - Time savings badges per feature - Responsive stack layout voor mobile - Footer met AI Speedrun branding ✨ E1.S6 - Bento Grid showcase - Replaced feature grid met Bento Grid layout - Variable card sizes voor visual hierarchy (col-span-2, col-span-1) - Dark slate background (from-slate-900) - Gradient backgrounds per card (teal, amber, purple) - Stats footer met 3 key metrics (90%+, < 5 sec, 4 weken) - Hover animations en glassmorphism effects 🎨 Design System: - Teal-first brand colors (#0D9488) - Amber accents voor AI features (#F59E0B) - Slate scale voor neutral colors - WCAG AA compliant contrast 📦 Components: - BuildTimeline (app/(marketing)/components) - BentoGrid & BentoCard (components/ui) - Button variants (components/ui) 📄 Content: - timeline.json met volledige 4-weken data - Features array per week - Metrics en achievements tracking 🔧 Technical: - Removed demo user scripts en migrations - Updated middleware routes (removed /epd) - TypeScript type fixes (hero-section-2, auth) - Build succesvol: 13 routes generated 🚀 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
148
app/epd/clients/components/client-form.tsx
Normal file
148
app/epd/clients/components/client-form.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Save, Loader2 } from 'lucide-react';
|
||||
import type { ClientFormData } from '@/lib/types/client';
|
||||
import type { Client } from '@/lib/types/client';
|
||||
import { createClient, updateClient } from '../actions';
|
||||
|
||||
interface ClientFormProps {
|
||||
client?: Client;
|
||||
}
|
||||
|
||||
export function ClientForm({ client }: ClientFormProps) {
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState<ClientFormData>({
|
||||
first_name: client?.first_name || '',
|
||||
last_name: client?.last_name || '',
|
||||
birth_date: client?.birth_date || '',
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
if (client) {
|
||||
await updateClient(client.id, formData);
|
||||
} else {
|
||||
await createClient(formData);
|
||||
}
|
||||
router.push('/epd/clients');
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
console.error('Form submission error:', err);
|
||||
setError('Er is een fout opgetreden. Probeer het opnieuw.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof ClientFormData, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form Card */}
|
||||
<div className="bg-white rounded-lg border border-slate-200 p-6 space-y-6">
|
||||
{/* First Name */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="first_name"
|
||||
className="block text-sm font-medium text-slate-700 mb-2"
|
||||
>
|
||||
Voornaam <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="first_name"
|
||||
required
|
||||
value={formData.first_name}
|
||||
onChange={(e) => handleChange('first_name', e.target.value)}
|
||||
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
placeholder="bijv. Jan"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Last Name */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="last_name"
|
||||
className="block text-sm font-medium text-slate-700 mb-2"
|
||||
>
|
||||
Achternaam <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="last_name"
|
||||
required
|
||||
value={formData.last_name}
|
||||
onChange={(e) => handleChange('last_name', e.target.value)}
|
||||
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
placeholder="bijv. de Vries"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Birth Date */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="birth_date"
|
||||
className="block text-sm font-medium text-slate-700 mb-2"
|
||||
>
|
||||
Geboortedatum <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="birth_date"
|
||||
required
|
||||
value={formData.birth_date}
|
||||
onChange={(e) => handleChange('birth_date', e.target.value)}
|
||||
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
disabled={isSubmitting}
|
||||
className="px-6 py-2 border border-slate-300 text-slate-700 font-medium rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Annuleren
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="inline-flex items-center gap-2 px-6 py-2 bg-gradient-to-r from-teal-600 to-teal-700 hover:from-teal-700 hover:to-teal-800 text-white font-medium rounded-lg shadow-sm hover:shadow-md transition-all disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>Bezig...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4" />
|
||||
<span>{client ? 'Bijwerken' : 'Opslaan'}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user