Files
Dramlog-Prod/src/components/PasswordChangeForm.tsx
robin 9d6a8b358f feat: public split visibility, RLS recursion fixes, and consolidated tasting permission management
- Added public discovery section for active splits on the landing page
- Refactored split detail page for guest support and login redirects
- Extracted SplitCard component for reuse
- Consolidated RLS policies for bottles and tastings to resolve permission errors
- Added unified SQL consolidation script for RLS and naming fixes
- Enhanced service logging for better database error diagnostics
2025-12-28 22:02:46 +01:00

159 lines
6.5 KiB
TypeScript

'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';
import { useI18n } from '@/i18n/I18nContext';
export default function PasswordChangeForm() {
const { t } = useI18n();
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(t('settings.password.mismatch'));
return;
}
if (newPassword.length < 6) {
setStatus('error');
setError(t('settings.password.tooShort'));
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 || t('common.error'));
}
});
};
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" />
{t('settings.password.title')}
</h2>
<div className="space-y-4">
{/* New Password */}
<div>
<label className="block text-sm font-medium text-zinc-400 mb-2">
{t('settings.password.newPassword')}
</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">
{t('settings.password.confirmPassword')}
</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} />
{t('settings.password.match')}
</>
) : (
<>
<AlertCircle size={12} />
{t('settings.password.mismatch')}
</>
)}
</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} />
{t('settings.password.success')}
</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" />
{t('common.loading')}
</>
) : (
<>
<Lock size={18} />
{t('settings.password.change')}
</>
)}
</button>
</motion.form>
);
}