feat: Add EU cookie banner and user settings page

Cookie Banner:
- GDPR-compliant consent banner
- Shows on first visit, stores consent in localStorage
- Two options: Accept All / Only Essential
- Dark theme with smooth animations

Settings Page (/settings):
- Profile form: display name editing
- Password change with validation
- Cookie settings info section
- Privacy links and account info

New files:
- src/hooks/useCookieConsent.ts
- src/components/CookieBanner.tsx
- src/components/ProfileForm.tsx
- src/components/PasswordChangeForm.tsx
- src/services/profile-actions.ts
- src/app/settings/page.tsx
This commit is contained in:
2025-12-26 21:30:00 +01:00
parent 9c5f538efb
commit 6c37481d17
8 changed files with 692 additions and 1 deletions

View File

@@ -0,0 +1,150 @@
'use server';
import { createClient } from '@/lib/supabase/server';
import { revalidatePath } from 'next/cache';
export interface ProfileUpdateResult {
success: boolean;
error?: string;
}
/**
* Update user profile (display_name, avatar_url)
*/
export async function updateProfile(formData: FormData): Promise<ProfileUpdateResult> {
const supabase = await createClient();
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return { success: false, error: 'Nicht autorisiert' };
}
const displayName = formData.get('display_name') as string;
const { error } = await supabase
.from('profiles')
.upsert({
id: user.id,
display_name: displayName?.trim() || null,
updated_at: new Date().toISOString(),
});
if (error) {
console.error('[updateProfile] Error:', error);
return { success: false, error: error.message };
}
revalidatePath('/settings');
return { success: true };
} catch (error) {
console.error('[updateProfile] Error:', error);
return { success: false, error: 'Profil konnte nicht aktualisiert werden' };
}
}
/**
* Change user password
*/
export async function changePassword(formData: FormData): Promise<ProfileUpdateResult> {
const supabase = await createClient();
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return { success: false, error: 'Nicht autorisiert' };
}
const newPassword = formData.get('new_password') as string;
const confirmPassword = formData.get('confirm_password') as string;
if (!newPassword || newPassword.length < 6) {
return { success: false, error: 'Passwort muss mindestens 6 Zeichen lang sein' };
}
if (newPassword !== confirmPassword) {
return { success: false, error: 'Passwörter stimmen nicht überein' };
}
const { error } = await supabase.auth.updateUser({
password: newPassword
});
if (error) {
console.error('[changePassword] Error:', error);
return { success: false, error: error.message };
}
return { success: true };
} catch (error) {
console.error('[changePassword] Error:', error);
return { success: false, error: 'Passwort konnte nicht geändert werden' };
}
}
/**
* Get user profile data
*/
export async function getProfile() {
const supabase = await createClient();
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return null;
}
const { data: profile } = await supabase
.from('profiles')
.select('id, display_name, avatar_url, created_at, updated_at')
.eq('id', user.id)
.single();
return {
id: user.id,
email: user.email,
display_name: profile?.display_name || null,
avatar_url: profile?.avatar_url || null,
created_at: user.created_at,
};
} catch (error) {
console.error('[getProfile] Error:', error);
return null;
}
}
/**
* Delete user account (marks for deletion, doesn't immediately delete)
*/
export async function requestAccountDeletion(): Promise<ProfileUpdateResult> {
const supabase = await createClient();
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return { success: false, error: 'Nicht autorisiert' };
}
// Mark profile for deletion (actual deletion handled separately)
const { error } = await supabase
.from('profiles')
.update({
deletion_requested_at: new Date().toISOString(),
})
.eq('id', user.id);
if (error) {
console.error('[requestAccountDeletion] Error:', error);
return { success: false, error: error.message };
}
return { success: true };
} catch (error) {
console.error('[requestAccountDeletion] Error:', error);
return { success: false, error: 'Löschanfrage konnte nicht erstellt werden' };
}
}