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>
144 lines
3.3 KiB
TypeScript
144 lines
3.3 KiB
TypeScript
'use server';
|
|
|
|
/**
|
|
* Client CRUD Server Actions
|
|
*
|
|
* Server-side actions for client management
|
|
*/
|
|
|
|
import { revalidatePath } from 'next/cache';
|
|
import { createClient as createSupabaseClient } from '@/lib/auth/server';
|
|
import type { ClientFormData, ClientFilters } from '@/lib/types/client';
|
|
|
|
/**
|
|
* Get all clients with optional filtering
|
|
*/
|
|
export async function getClients(filters?: ClientFilters) {
|
|
const supabase = await createSupabaseClient();
|
|
|
|
let query = supabase
|
|
.from('clients')
|
|
.select('*');
|
|
|
|
// Apply search filter
|
|
if (filters?.search) {
|
|
const search = `%${filters.search}%`;
|
|
query = query.or(`first_name.ilike.${search},last_name.ilike.${search}`);
|
|
}
|
|
|
|
// Apply sorting
|
|
const sortBy = filters?.sortBy || 'created_at';
|
|
const sortOrder = filters?.sortOrder || 'desc';
|
|
|
|
if (sortBy === 'name') {
|
|
query = query.order('last_name', { ascending: sortOrder === 'asc' });
|
|
query = query.order('first_name', { ascending: sortOrder === 'asc' });
|
|
} else if (sortBy === 'created_at') {
|
|
query = query.order('created_at', { ascending: sortOrder === 'asc' });
|
|
} else if (sortBy === 'age') {
|
|
// Sort by birth_date (newest birth = youngest age)
|
|
query = query.order('birth_date', { ascending: sortOrder === 'desc' });
|
|
}
|
|
|
|
const { data, error } = await query;
|
|
|
|
if (error) {
|
|
console.error('Error fetching clients:', error);
|
|
throw new Error('Failed to fetch clients');
|
|
}
|
|
|
|
return data || [];
|
|
}
|
|
|
|
/**
|
|
* Get single client by ID
|
|
*/
|
|
export async function getClient(id: string) {
|
|
const supabase = await createSupabaseClient();
|
|
|
|
const { data, error } = await supabase
|
|
.from('clients')
|
|
.select('*')
|
|
.eq('id', id)
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('Error fetching client:', error);
|
|
throw new Error('Failed to fetch client');
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Create new client
|
|
*/
|
|
export async function createClient(formData: ClientFormData) {
|
|
const supabase = await createSupabaseClient();
|
|
|
|
const { data, error } = await supabase
|
|
.from('clients')
|
|
.insert({
|
|
first_name: formData.first_name.trim(),
|
|
last_name: formData.last_name.trim(),
|
|
birth_date: formData.birth_date,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('Error creating client:', error);
|
|
throw new Error('Failed to create client');
|
|
}
|
|
|
|
revalidatePath('/epd/clients');
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Update existing client
|
|
*/
|
|
export async function updateClient(id: string, formData: ClientFormData) {
|
|
const supabase = await createSupabaseClient();
|
|
|
|
const { data, error } = await supabase
|
|
.from('clients')
|
|
.update({
|
|
first_name: formData.first_name.trim(),
|
|
last_name: formData.last_name.trim(),
|
|
birth_date: formData.birth_date,
|
|
})
|
|
.eq('id', id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('Error updating client:', error);
|
|
throw new Error('Failed to update client');
|
|
}
|
|
|
|
revalidatePath('/epd/clients');
|
|
revalidatePath(`/epd/clients/${id}`);
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Delete client
|
|
*/
|
|
export async function deleteClient(id: string) {
|
|
const supabase = await createSupabaseClient();
|
|
|
|
const { error } = await supabase
|
|
.from('clients')
|
|
.delete()
|
|
.eq('id', id);
|
|
|
|
if (error) {
|
|
console.error('Error deleting client:', error);
|
|
throw new Error('Failed to delete client');
|
|
}
|
|
|
|
revalidatePath('/epd/clients');
|
|
return { success: true };
|
|
}
|