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:
155
src/components/PasswordChangeForm.tsx
Normal file
155
src/components/PasswordChangeForm.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Lock, Eye, EyeOff, Loader2, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import { changePassword } from '@/services/profile-actions';
|
||||
|
||||
export default function PasswordChangeForm() {
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('idle');
|
||||
setError(null);
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
setStatus('error');
|
||||
setError('Passwörter stimmen nicht überein');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
setStatus('error');
|
||||
setError('Passwort muss mindestens 6 Zeichen lang sein');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.set('new_password', newPassword);
|
||||
formData.set('confirm_password', confirmPassword);
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await changePassword(formData);
|
||||
if (result.success) {
|
||||
setStatus('success');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setTimeout(() => setStatus('idle'), 3000);
|
||||
} else {
|
||||
setStatus('error');
|
||||
setError(result.error || 'Fehler beim Ändern');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.form
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="bg-zinc-900 border border-zinc-800 rounded-2xl p-6"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<h2 className="text-lg font-bold text-white mb-6 flex items-center gap-2">
|
||||
<Lock size={20} className="text-orange-500" />
|
||||
Passwort ändern
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* New Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-400 mb-2">
|
||||
Neues Passwort
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-3 pr-12 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300"
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirm Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-400 mb-2">
|
||||
Passwort bestätigen
|
||||
</label>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password match indicator */}
|
||||
{confirmPassword && (
|
||||
<div className={`mt-2 text-xs flex items-center gap-1 ${newPassword === confirmPassword ? 'text-green-500' : 'text-red-500'
|
||||
}`}>
|
||||
{newPassword === confirmPassword ? (
|
||||
<>
|
||||
<CheckCircle size={12} />
|
||||
Passwörter stimmen überein
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle size={12} />
|
||||
Passwörter stimmen nicht überein
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status messages */}
|
||||
{status === 'success' && (
|
||||
<div className="mt-4 p-3 bg-green-500/10 border border-green-500/20 rounded-xl flex items-center gap-2 text-green-500 text-sm">
|
||||
<CheckCircle size={16} />
|
||||
Passwort erfolgreich geändert!
|
||||
</div>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-2 text-red-500 text-sm">
|
||||
<AlertCircle size={16} />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending || !newPassword || !confirmPassword}
|
||||
className="mt-6 w-full px-4 py-3 bg-orange-500 hover:bg-orange-400 disabled:opacity-50 disabled:cursor-not-allowed text-zinc-900 font-bold rounded-xl flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 size={18} className="animate-spin" />
|
||||
Ändern...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Lock size={18} />
|
||||
Passwort ändern
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</motion.form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user