- 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
159 lines
6.5 KiB
TypeScript
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>
|
|
);
|
|
}
|