Implement extensive localization improvements across forms and components
- Refactored `it.json` translations with added keys for authentication, admin panel, and settings. - Updated authentication forms (`LoginForm`, `RegisterForm`, `PasswordResetConfirmForm`) to use localized strings via `next-intl`. - Enhanced password validation schemas with dynamic translations and refined error messages. - Adjusted `Header` and related components to include localized navigation and status elements. - Improved placeholder hints, button labels, and inline validation messages for seamless localization.
This commit is contained in:
@@ -25,11 +25,14 @@
|
|||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"logout": "Logout",
|
"logout": "Log out",
|
||||||
|
"loggingOut": "Logging out...",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
"demos": "Demos",
|
"demos": "Demos",
|
||||||
"design": "Design System"
|
"design": "Design System",
|
||||||
|
"admin": "Admin",
|
||||||
|
"adminPanel": "Admin Panel"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": {
|
"login": {
|
||||||
@@ -41,11 +44,13 @@
|
|||||||
"passwordPlaceholder": "Enter your password",
|
"passwordPlaceholder": "Enter your password",
|
||||||
"rememberMe": "Remember me",
|
"rememberMe": "Remember me",
|
||||||
"forgotPassword": "Forgot password?",
|
"forgotPassword": "Forgot password?",
|
||||||
"loginButton": "Sign In",
|
"loginButton": "Sign in",
|
||||||
|
"loginButtonLoading": "Signing in...",
|
||||||
"noAccount": "Don't have an account?",
|
"noAccount": "Don't have an account?",
|
||||||
"registerLink": "Sign up",
|
"registerLink": "Sign up",
|
||||||
"success": "Successfully logged in",
|
"success": "Successfully logged in",
|
||||||
"error": "Invalid email or password"
|
"error": "Invalid email or password",
|
||||||
|
"unexpectedError": "An unexpected error occurred. Please try again."
|
||||||
},
|
},
|
||||||
"register": {
|
"register": {
|
||||||
"title": "Create an account",
|
"title": "Create an account",
|
||||||
@@ -53,63 +58,142 @@
|
|||||||
"firstNameLabel": "First Name",
|
"firstNameLabel": "First Name",
|
||||||
"firstNamePlaceholder": "John",
|
"firstNamePlaceholder": "John",
|
||||||
"lastNameLabel": "Last Name",
|
"lastNameLabel": "Last Name",
|
||||||
"lastNamePlaceholder": "Doe",
|
"lastNamePlaceholder": "Doe (optional)",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
"emailPlaceholder": "name@example.com",
|
"emailPlaceholder": "you@example.com",
|
||||||
"passwordLabel": "Password",
|
"passwordLabel": "Password",
|
||||||
"passwordPlaceholder": "Create a strong password",
|
"passwordPlaceholder": "Create a strong password",
|
||||||
"confirmPasswordLabel": "Confirm Password",
|
"confirmPasswordLabel": "Confirm Password",
|
||||||
"confirmPasswordPlaceholder": "Re-enter your password",
|
"confirmPasswordPlaceholder": "Confirm your password",
|
||||||
"phoneLabel": "Phone Number",
|
"phoneLabel": "Phone Number",
|
||||||
"phonePlaceholder": "+1 (555) 000-0000",
|
"phonePlaceholder": "+1 (555) 000-0000",
|
||||||
"registerButton": "Create Account",
|
"registerButton": "Create account",
|
||||||
|
"registerButtonLoading": "Creating account...",
|
||||||
"hasAccount": "Already have an account?",
|
"hasAccount": "Already have an account?",
|
||||||
"loginLink": "Sign in",
|
"loginLink": "Sign in",
|
||||||
"success": "Account created successfully",
|
"success": "Account created successfully",
|
||||||
"error": "Failed to create account"
|
"error": "Failed to create account",
|
||||||
|
"unexpectedError": "An unexpected error occurred. Please try again.",
|
||||||
|
"passwordRequirements": {
|
||||||
|
"minLength": "At least 8 characters",
|
||||||
|
"hasNumber": "Contains a number",
|
||||||
|
"hasUppercase": "Contains an uppercase letter"
|
||||||
|
},
|
||||||
|
"required": "*",
|
||||||
|
"firstNameRequired": "First name is required",
|
||||||
|
"firstNameMinLength": "First name must be at least 2 characters",
|
||||||
|
"firstNameMaxLength": "First name must not exceed 50 characters",
|
||||||
|
"lastNameMaxLength": "Last name must not exceed 50 characters",
|
||||||
|
"passwordRequired": "Password is required",
|
||||||
|
"passwordMinLength": "Password must be at least 8 characters",
|
||||||
|
"passwordNumber": "Password must contain at least one number",
|
||||||
|
"passwordUppercase": "Password must contain at least one uppercase letter",
|
||||||
|
"confirmPasswordRequired": "Please confirm your password",
|
||||||
|
"passwordMismatch": "Passwords do not match"
|
||||||
},
|
},
|
||||||
"passwordReset": {
|
"passwordReset": {
|
||||||
"title": "Reset your password",
|
"title": "Reset your password",
|
||||||
"subtitle": "Enter your email address and we'll send you a reset link",
|
"subtitle": "Enter your email address and we'll send you a reset link",
|
||||||
|
"instructions": "Enter your email address and we'll send you instructions to reset your password.",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
"emailPlaceholder": "name@example.com",
|
"emailPlaceholder": "you@example.com",
|
||||||
"sendButton": "Send Reset Link",
|
"sendButton": "Send Reset Instructions",
|
||||||
|
"sendButtonLoading": "Sending...",
|
||||||
"backToLogin": "Back to login",
|
"backToLogin": "Back to login",
|
||||||
"success": "Password reset link sent to your email",
|
"rememberPassword": "Remember your password?",
|
||||||
"error": "Failed to send reset link"
|
"success": "Password reset instructions have been sent to your email address. Please check your inbox.",
|
||||||
|
"error": "Failed to send reset link",
|
||||||
|
"unexpectedError": "An unexpected error occurred. Please try again.",
|
||||||
|
"required": "*"
|
||||||
},
|
},
|
||||||
"passwordChange": {
|
"passwordChange": {
|
||||||
"title": "Change Password",
|
"title": "Change Password",
|
||||||
"currentPasswordLabel": "Current Password",
|
"currentPasswordLabel": "Current Password",
|
||||||
"newPasswordLabel": "New Password",
|
"newPasswordLabel": "New Password",
|
||||||
|
"newPasswordPlaceholder": "Enter new password",
|
||||||
"confirmPasswordLabel": "Confirm New Password",
|
"confirmPasswordLabel": "Confirm New Password",
|
||||||
|
"confirmPasswordPlaceholder": "Re-enter new password",
|
||||||
"changeButton": "Change Password",
|
"changeButton": "Change Password",
|
||||||
"success": "Password changed successfully",
|
"success": "Password changed successfully",
|
||||||
"error": "Failed to change password"
|
"error": "Failed to change password"
|
||||||
|
},
|
||||||
|
"passwordResetConfirm": {
|
||||||
|
"title": "Reset Password",
|
||||||
|
"instructions": "Enter your new password below. Make sure it meets all security requirements.",
|
||||||
|
"newPasswordLabel": "New Password",
|
||||||
|
"newPasswordPlaceholder": "Enter new password",
|
||||||
|
"confirmPasswordLabel": "Confirm Password",
|
||||||
|
"confirmPasswordPlaceholder": "Re-enter new password",
|
||||||
|
"resetButton": "Reset Password",
|
||||||
|
"resetButtonLoading": "Resetting Password...",
|
||||||
|
"rememberPassword": "Remember your password?",
|
||||||
|
"backToLogin": "Back to login",
|
||||||
|
"success": "Your password has been successfully reset. You can now log in with your new password.",
|
||||||
|
"unexpectedError": "An unexpected error occurred. Please try again.",
|
||||||
|
"required": "*",
|
||||||
|
"tokenRequired": "Reset token is required",
|
||||||
|
"passwordRequired": "New password is required",
|
||||||
|
"passwordMinLength": "Password must be at least 8 characters",
|
||||||
|
"passwordNumber": "Password must contain at least one number",
|
||||||
|
"passwordUppercase": "Password must contain at least one uppercase letter",
|
||||||
|
"confirmPasswordRequired": "Please confirm your password",
|
||||||
|
"passwordMismatch": "Passwords do not match",
|
||||||
|
"passwordRequirements": {
|
||||||
|
"minLength": "At least 8 characters",
|
||||||
|
"hasNumber": "Contains a number",
|
||||||
|
"hasUppercase": "Contains an uppercase letter"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Profile",
|
"title": "Profile Information",
|
||||||
"subtitle": "Manage your profile information",
|
"subtitle": "Update your personal information. Your email address is read-only.",
|
||||||
"firstNameLabel": "First Name",
|
"firstNameLabel": "First Name",
|
||||||
|
"firstNamePlaceholder": "John",
|
||||||
"lastNameLabel": "Last Name",
|
"lastNameLabel": "Last Name",
|
||||||
|
"lastNamePlaceholder": "Doe",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
|
"emailDescription": "Your email address cannot be changed from this form",
|
||||||
"phoneLabel": "Phone Number",
|
"phoneLabel": "Phone Number",
|
||||||
"updateButton": "Update Profile",
|
"updateButton": "Save Changes",
|
||||||
|
"updateButtonLoading": "Saving...",
|
||||||
|
"resetButton": "Reset",
|
||||||
"success": "Profile updated successfully",
|
"success": "Profile updated successfully",
|
||||||
"error": "Failed to update profile"
|
"error": "Failed to update profile",
|
||||||
|
"unexpectedError": "An unexpected error occurred. Please try again.",
|
||||||
|
"firstNameRequired": "First name is required",
|
||||||
|
"firstNameMinLength": "First name must be at least 2 characters",
|
||||||
|
"firstNameMaxLength": "First name must not exceed 50 characters",
|
||||||
|
"lastNameMaxLength": "Last name must not exceed 50 characters",
|
||||||
|
"emailInvalid": "Invalid email address"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Password",
|
"title": "Change Password",
|
||||||
"subtitle": "Change your account password",
|
"subtitle": "Update your password to keep your account secure. Make sure it's strong and unique.",
|
||||||
"currentPasswordLabel": "Current Password",
|
"currentPasswordLabel": "Current Password",
|
||||||
|
"currentPasswordPlaceholder": "Enter your current password",
|
||||||
"newPasswordLabel": "New Password",
|
"newPasswordLabel": "New Password",
|
||||||
|
"newPasswordPlaceholder": "Enter your new password",
|
||||||
|
"newPasswordDescription": "At least 8 characters with uppercase, lowercase, number, and special character",
|
||||||
"confirmPasswordLabel": "Confirm New Password",
|
"confirmPasswordLabel": "Confirm New Password",
|
||||||
"updateButton": "Update Password",
|
"confirmPasswordPlaceholder": "Confirm your new password",
|
||||||
|
"updateButton": "Change Password",
|
||||||
|
"updateButtonLoading": "Changing Password...",
|
||||||
|
"cancelButton": "Cancel",
|
||||||
"success": "Password updated successfully",
|
"success": "Password updated successfully",
|
||||||
"error": "Failed to update password"
|
"error": "Failed to update password",
|
||||||
|
"unexpectedError": "An unexpected error occurred. Please try again.",
|
||||||
|
"currentPasswordRequired": "Current password is required",
|
||||||
|
"newPasswordRequired": "New password is required",
|
||||||
|
"newPasswordMinLength": "Password must be at least 8 characters",
|
||||||
|
"newPasswordNumber": "Password must contain at least one number",
|
||||||
|
"newPasswordUppercase": "Password must contain at least one uppercase letter",
|
||||||
|
"newPasswordLowercase": "Password must contain at least one lowercase letter",
|
||||||
|
"newPasswordSpecial": "Password must contain at least one special character",
|
||||||
|
"confirmPasswordRequired": "Please confirm your new password",
|
||||||
|
"passwordMismatch": "Passwords do not match"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
"title": "Sessions",
|
"title": "Sessions",
|
||||||
|
|||||||
@@ -26,10 +26,13 @@
|
|||||||
"settings": "Impostazioni",
|
"settings": "Impostazioni",
|
||||||
"profile": "Profilo",
|
"profile": "Profilo",
|
||||||
"logout": "Disconnetti",
|
"logout": "Disconnetti",
|
||||||
|
"loggingOut": "Disconnessione...",
|
||||||
"login": "Accedi",
|
"login": "Accedi",
|
||||||
"register": "Registrati",
|
"register": "Registrati",
|
||||||
"demos": "Demo",
|
"demos": "Demo",
|
||||||
"design": "Design System"
|
"design": "Design System",
|
||||||
|
"admin": "Admin",
|
||||||
|
"adminPanel": "Pannello Admin"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": {
|
"login": {
|
||||||
@@ -42,10 +45,12 @@
|
|||||||
"rememberMe": "Ricordami",
|
"rememberMe": "Ricordami",
|
||||||
"forgotPassword": "Password dimenticata?",
|
"forgotPassword": "Password dimenticata?",
|
||||||
"loginButton": "Accedi",
|
"loginButton": "Accedi",
|
||||||
|
"loginButtonLoading": "Accesso in corso...",
|
||||||
"noAccount": "Non hai un account?",
|
"noAccount": "Non hai un account?",
|
||||||
"registerLink": "Registrati",
|
"registerLink": "Registrati",
|
||||||
"success": "Accesso effettuato con successo",
|
"success": "Accesso effettuato con successo",
|
||||||
"error": "Email o password non validi"
|
"error": "Email o password non validi",
|
||||||
|
"unexpectedError": "Si è verificato un errore imprevisto. Riprova."
|
||||||
},
|
},
|
||||||
"register": {
|
"register": {
|
||||||
"title": "Crea un account",
|
"title": "Crea un account",
|
||||||
@@ -53,63 +58,142 @@
|
|||||||
"firstNameLabel": "Nome",
|
"firstNameLabel": "Nome",
|
||||||
"firstNamePlaceholder": "Mario",
|
"firstNamePlaceholder": "Mario",
|
||||||
"lastNameLabel": "Cognome",
|
"lastNameLabel": "Cognome",
|
||||||
"lastNamePlaceholder": "Rossi",
|
"lastNamePlaceholder": "Rossi (facoltativo)",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
"emailPlaceholder": "nome@esempio.com",
|
"emailPlaceholder": "nome@esempio.com",
|
||||||
"passwordLabel": "Password",
|
"passwordLabel": "Password",
|
||||||
"passwordPlaceholder": "Crea una password sicura",
|
"passwordPlaceholder": "Crea una password sicura",
|
||||||
"confirmPasswordLabel": "Conferma Password",
|
"confirmPasswordLabel": "Conferma Password",
|
||||||
"confirmPasswordPlaceholder": "Reinserisci la tua password",
|
"confirmPasswordPlaceholder": "Conferma la tua password",
|
||||||
"phoneLabel": "Numero di Telefono",
|
"phoneLabel": "Numero di Telefono",
|
||||||
"phonePlaceholder": "+39 123 456 7890",
|
"phonePlaceholder": "+39 123 456 7890",
|
||||||
"registerButton": "Crea Account",
|
"registerButton": "Crea account",
|
||||||
|
"registerButtonLoading": "Creazione account...",
|
||||||
"hasAccount": "Hai già un account?",
|
"hasAccount": "Hai già un account?",
|
||||||
"loginLink": "Accedi",
|
"loginLink": "Accedi",
|
||||||
"success": "Account creato con successo",
|
"success": "Account creato con successo",
|
||||||
"error": "Impossibile creare l'account"
|
"error": "Impossibile creare l'account",
|
||||||
|
"unexpectedError": "Si è verificato un errore imprevisto. Riprova.",
|
||||||
|
"passwordRequirements": {
|
||||||
|
"minLength": "Almeno 8 caratteri",
|
||||||
|
"hasNumber": "Contiene un numero",
|
||||||
|
"hasUppercase": "Contiene una lettera maiuscola"
|
||||||
|
},
|
||||||
|
"required": "*",
|
||||||
|
"firstNameRequired": "Il nome è obbligatorio",
|
||||||
|
"firstNameMinLength": "Il nome deve essere di almeno 2 caratteri",
|
||||||
|
"firstNameMaxLength": "Il nome non deve superare i 50 caratteri",
|
||||||
|
"lastNameMaxLength": "Il cognome non deve superare i 50 caratteri",
|
||||||
|
"passwordRequired": "La password è obbligatoria",
|
||||||
|
"passwordMinLength": "La password deve essere di almeno 8 caratteri",
|
||||||
|
"passwordNumber": "La password deve contenere almeno un numero",
|
||||||
|
"passwordUppercase": "La password deve contenere almeno una lettera maiuscola",
|
||||||
|
"confirmPasswordRequired": "Conferma la tua password",
|
||||||
|
"passwordMismatch": "Le password non corrispondono"
|
||||||
},
|
},
|
||||||
"passwordReset": {
|
"passwordReset": {
|
||||||
"title": "Reimposta la tua password",
|
"title": "Reimposta la tua password",
|
||||||
"subtitle": "Inserisci il tuo indirizzo email e ti invieremo un link per reimpostare la password",
|
"subtitle": "Inserisci il tuo indirizzo email e ti invieremo un link per reimpostare la password",
|
||||||
|
"instructions": "Inserisci il tuo indirizzo email e ti invieremo le istruzioni per reimpostare la password.",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
"emailPlaceholder": "nome@esempio.com",
|
"emailPlaceholder": "nome@esempio.com",
|
||||||
"sendButton": "Invia Link di Reset",
|
"sendButton": "Invia Istruzioni di Reset",
|
||||||
|
"sendButtonLoading": "Invio in corso...",
|
||||||
"backToLogin": "Torna al login",
|
"backToLogin": "Torna al login",
|
||||||
"success": "Link di reset password inviato alla tua email",
|
"rememberPassword": "Ricordi la tua password?",
|
||||||
"error": "Impossibile inviare il link di reset"
|
"success": "Le istruzioni per il reset della password sono state inviate al tuo indirizzo email. Controlla la tua casella di posta.",
|
||||||
|
"error": "Impossibile inviare il link di reset",
|
||||||
|
"unexpectedError": "Si è verificato un errore imprevisto. Riprova.",
|
||||||
|
"required": "*"
|
||||||
},
|
},
|
||||||
"passwordChange": {
|
"passwordChange": {
|
||||||
"title": "Cambia Password",
|
"title": "Cambia Password",
|
||||||
"currentPasswordLabel": "Password Attuale",
|
"currentPasswordLabel": "Password Attuale",
|
||||||
"newPasswordLabel": "Nuova Password",
|
"newPasswordLabel": "Nuova Password",
|
||||||
|
"newPasswordPlaceholder": "Inserisci nuova password",
|
||||||
"confirmPasswordLabel": "Conferma Nuova Password",
|
"confirmPasswordLabel": "Conferma Nuova Password",
|
||||||
|
"confirmPasswordPlaceholder": "Reinserisci nuova password",
|
||||||
"changeButton": "Cambia Password",
|
"changeButton": "Cambia Password",
|
||||||
"success": "Password cambiata con successo",
|
"success": "Password cambiata con successo",
|
||||||
"error": "Impossibile cambiare la password"
|
"error": "Impossibile cambiare la password"
|
||||||
|
},
|
||||||
|
"passwordResetConfirm": {
|
||||||
|
"title": "Reimposta Password",
|
||||||
|
"instructions": "Inserisci la tua nuova password qui sotto. Assicurati che soddisfi tutti i requisiti di sicurezza.",
|
||||||
|
"newPasswordLabel": "Nuova Password",
|
||||||
|
"newPasswordPlaceholder": "Inserisci nuova password",
|
||||||
|
"confirmPasswordLabel": "Conferma Password",
|
||||||
|
"confirmPasswordPlaceholder": "Reinserisci nuova password",
|
||||||
|
"resetButton": "Reimposta Password",
|
||||||
|
"resetButtonLoading": "Reimpostazione Password...",
|
||||||
|
"rememberPassword": "Ricordi la tua password?",
|
||||||
|
"backToLogin": "Torna al login",
|
||||||
|
"success": "La tua password è stata reimpostata con successo. Ora puoi accedere con la tua nuova password.",
|
||||||
|
"unexpectedError": "Si è verificato un errore imprevisto. Riprova.",
|
||||||
|
"required": "*",
|
||||||
|
"tokenRequired": "Token di reset è obbligatorio",
|
||||||
|
"passwordRequired": "La nuova password è obbligatoria",
|
||||||
|
"passwordMinLength": "La password deve essere di almeno 8 caratteri",
|
||||||
|
"passwordNumber": "La password deve contenere almeno un numero",
|
||||||
|
"passwordUppercase": "La password deve contenere almeno una lettera maiuscola",
|
||||||
|
"confirmPasswordRequired": "Conferma la tua password",
|
||||||
|
"passwordMismatch": "Le password non corrispondono",
|
||||||
|
"passwordRequirements": {
|
||||||
|
"minLength": "Almeno 8 caratteri",
|
||||||
|
"hasNumber": "Contiene un numero",
|
||||||
|
"hasUppercase": "Contiene una lettera maiuscola"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Impostazioni",
|
"title": "Impostazioni",
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Profilo",
|
"title": "Informazioni Profilo",
|
||||||
"subtitle": "Gestisci le informazioni del tuo profilo",
|
"subtitle": "Aggiorna le tue informazioni personali. Il tuo indirizzo email è di sola lettura.",
|
||||||
"firstNameLabel": "Nome",
|
"firstNameLabel": "Nome",
|
||||||
|
"firstNamePlaceholder": "Mario",
|
||||||
"lastNameLabel": "Cognome",
|
"lastNameLabel": "Cognome",
|
||||||
|
"lastNamePlaceholder": "Rossi",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
|
"emailDescription": "Il tuo indirizzo email non può essere modificato da questo modulo",
|
||||||
"phoneLabel": "Numero di Telefono",
|
"phoneLabel": "Numero di Telefono",
|
||||||
"updateButton": "Aggiorna Profilo",
|
"updateButton": "Salva Modifiche",
|
||||||
|
"updateButtonLoading": "Salvataggio...",
|
||||||
|
"resetButton": "Ripristina",
|
||||||
"success": "Profilo aggiornato con successo",
|
"success": "Profilo aggiornato con successo",
|
||||||
"error": "Impossibile aggiornare il profilo"
|
"error": "Impossibile aggiornare il profilo",
|
||||||
|
"unexpectedError": "Si è verificato un errore imprevisto. Riprova.",
|
||||||
|
"firstNameRequired": "Il nome è obbligatorio",
|
||||||
|
"firstNameMinLength": "Il nome deve essere di almeno 2 caratteri",
|
||||||
|
"firstNameMaxLength": "Il nome non deve superare i 50 caratteri",
|
||||||
|
"lastNameMaxLength": "Il cognome non deve superare i 50 caratteri",
|
||||||
|
"emailInvalid": "Indirizzo email non valido"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Password",
|
"title": "Cambia Password",
|
||||||
"subtitle": "Cambia la password del tuo account",
|
"subtitle": "Aggiorna la tua password per mantenere il tuo account sicuro. Assicurati che sia forte e univoca.",
|
||||||
"currentPasswordLabel": "Password Attuale",
|
"currentPasswordLabel": "Password Attuale",
|
||||||
|
"currentPasswordPlaceholder": "Inserisci la tua password attuale",
|
||||||
"newPasswordLabel": "Nuova Password",
|
"newPasswordLabel": "Nuova Password",
|
||||||
|
"newPasswordPlaceholder": "Inserisci la tua nuova password",
|
||||||
|
"newPasswordDescription": "Almeno 8 caratteri con maiuscole, minuscole, numeri e caratteri speciali",
|
||||||
"confirmPasswordLabel": "Conferma Nuova Password",
|
"confirmPasswordLabel": "Conferma Nuova Password",
|
||||||
"updateButton": "Aggiorna Password",
|
"confirmPasswordPlaceholder": "Conferma la tua nuova password",
|
||||||
|
"updateButton": "Cambia Password",
|
||||||
|
"updateButtonLoading": "Cambio Password...",
|
||||||
|
"cancelButton": "Annulla",
|
||||||
"success": "Password aggiornata con successo",
|
"success": "Password aggiornata con successo",
|
||||||
"error": "Impossibile aggiornare la password"
|
"error": "Impossibile aggiornare la password",
|
||||||
|
"unexpectedError": "Si è verificato un errore imprevisto. Riprova.",
|
||||||
|
"currentPasswordRequired": "La password attuale è obbligatoria",
|
||||||
|
"newPasswordRequired": "La nuova password è obbligatoria",
|
||||||
|
"newPasswordMinLength": "La password deve essere di almeno 8 caratteri",
|
||||||
|
"newPasswordNumber": "La password deve contenere almeno un numero",
|
||||||
|
"newPasswordUppercase": "La password deve contenere almeno una lettera maiuscola",
|
||||||
|
"newPasswordLowercase": "La password deve contenere almeno una lettera minuscola",
|
||||||
|
"newPasswordSpecial": "La password deve contenere almeno un carattere speciale",
|
||||||
|
"confirmPasswordRequired": "Conferma la tua nuova password",
|
||||||
|
"passwordMismatch": "Le password non corrispondono"
|
||||||
},
|
},
|
||||||
"sessions": {
|
"sessions": {
|
||||||
"title": "Sessioni",
|
"title": "Sessioni",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Link } from '@/lib/i18n/routing';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -23,17 +24,18 @@ import config from '@/config/app.config';
|
|||||||
// Validation Schema
|
// Validation Schema
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const createLoginSchema = (t: (key: string) => string) =>
|
||||||
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
|
z.object({
|
||||||
password: z
|
email: z.string().min(1, t('validation.required')).email(t('validation.email')),
|
||||||
.string()
|
password: z
|
||||||
.min(1, 'Password is required')
|
.string()
|
||||||
.min(8, 'Password must be at least 8 characters')
|
.min(1, t('validation.required'))
|
||||||
.regex(/[0-9]/, 'Password must contain at least one number')
|
.min(8, t('validation.minLength').replace('{count}', '8'))
|
||||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter'),
|
.regex(/[0-9]/, t('errors.validation.passwordWeak'))
|
||||||
});
|
.regex(/[A-Z]/, t('errors.validation.passwordWeak')),
|
||||||
|
});
|
||||||
|
|
||||||
type LoginFormData = z.infer<typeof loginSchema>;
|
type LoginFormData = z.infer<ReturnType<typeof createLoginSchema>>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Component
|
// Component
|
||||||
@@ -74,9 +76,22 @@ export function LoginForm({
|
|||||||
showPasswordResetLink = true,
|
showPasswordResetLink = true,
|
||||||
className,
|
className,
|
||||||
}: LoginFormProps) {
|
}: LoginFormProps) {
|
||||||
|
const t = useTranslations('auth.login');
|
||||||
|
const tValidation = useTranslations('validation');
|
||||||
|
const tErrors = useTranslations('errors.validation');
|
||||||
const [serverError, setServerError] = useState<string | null>(null);
|
const [serverError, setServerError] = useState<string | null>(null);
|
||||||
const loginMutation = useLogin();
|
const loginMutation = useLogin();
|
||||||
|
|
||||||
|
const loginSchema = createLoginSchema((key: string) => {
|
||||||
|
if (key.startsWith('validation.')) {
|
||||||
|
return tValidation(key.replace('validation.', ''));
|
||||||
|
}
|
||||||
|
if (key.startsWith('errors.validation.')) {
|
||||||
|
return tErrors(key.replace('errors.validation.', ''));
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm<LoginFormData>({
|
const form = useForm<LoginFormData>({
|
||||||
resolver: zodResolver(loginSchema),
|
resolver: zodResolver(loginSchema),
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
@@ -116,7 +131,7 @@ export function LoginForm({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Unexpected error format
|
// Unexpected error format
|
||||||
setServerError('An unexpected error occurred. Please try again.');
|
setServerError(t('unexpectedError'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -135,11 +150,11 @@ export function LoginForm({
|
|||||||
|
|
||||||
{/* Email Field */}
|
{/* Email Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email">Email</Label>
|
<Label htmlFor="email">{t('emailLabel')}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="you@example.com"
|
placeholder={t('emailPlaceholder')}
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...form.register('email')}
|
{...form.register('email')}
|
||||||
@@ -156,20 +171,20 @@ export function LoginForm({
|
|||||||
{/* Password Field */}
|
{/* Password Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label htmlFor="password">Password</Label>
|
<Label htmlFor="password">{t('passwordLabel')}</Label>
|
||||||
{showPasswordResetLink && (
|
{showPasswordResetLink && (
|
||||||
<Link
|
<Link
|
||||||
href="/password-reset"
|
href="/password-reset"
|
||||||
className="text-sm text-muted-foreground hover:text-primary underline-offset-4 hover:underline"
|
className="text-sm text-muted-foreground hover:text-primary underline-offset-4 hover:underline"
|
||||||
>
|
>
|
||||||
Forgot password?
|
{t('forgotPassword')}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter your password"
|
placeholder={t('passwordPlaceholder')}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...form.register('password')}
|
{...form.register('password')}
|
||||||
@@ -185,18 +200,18 @@ export function LoginForm({
|
|||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||||
{isSubmitting ? 'Signing in...' : 'Sign in'}
|
{isSubmitting ? t('loginButtonLoading') : t('loginButton')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Registration Link */}
|
{/* Registration Link */}
|
||||||
{showRegisterLink && config.features.enableRegistration && (
|
{showRegisterLink && config.features.enableRegistration && (
|
||||||
<p className="text-center text-sm text-muted-foreground">
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
Don't have an account?{' '}
|
{t('noAccount')}{' '}
|
||||||
<Link
|
<Link
|
||||||
href={config.routes.register}
|
href={config.routes.register}
|
||||||
className="text-primary underline-offset-4 hover:underline font-medium"
|
className="text-primary underline-offset-4 hover:underline font-medium"
|
||||||
>
|
>
|
||||||
Sign up
|
{t('registerLink')}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Link } from '@/lib/i18n/routing';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -22,23 +23,24 @@ import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/erro
|
|||||||
// Validation Schema
|
// Validation Schema
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const resetConfirmSchema = z
|
const createResetConfirmSchema = (t: (key: string) => string) =>
|
||||||
.object({
|
z
|
||||||
token: z.string().min(1, 'Reset token is required'),
|
.object({
|
||||||
new_password: z
|
token: z.string().min(1, t('tokenRequired')),
|
||||||
.string()
|
new_password: z
|
||||||
.min(1, 'New password is required')
|
.string()
|
||||||
.min(8, 'Password must be at least 8 characters')
|
.min(1, t('passwordRequired'))
|
||||||
.regex(/[0-9]/, 'Password must contain at least one number')
|
.min(8, t('passwordMinLength'))
|
||||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter'),
|
.regex(/[0-9]/, t('passwordNumber'))
|
||||||
confirm_password: z.string().min(1, 'Please confirm your password'),
|
.regex(/[A-Z]/, t('passwordUppercase')),
|
||||||
})
|
confirm_password: z.string().min(1, t('confirmPasswordRequired')),
|
||||||
.refine((data) => data.new_password === data.confirm_password, {
|
})
|
||||||
message: 'Passwords do not match',
|
.refine((data) => data.new_password === data.confirm_password, {
|
||||||
path: ['confirm_password'],
|
message: t('passwordMismatch'),
|
||||||
});
|
path: ['confirm_password'],
|
||||||
|
});
|
||||||
|
|
||||||
type ResetConfirmFormData = z.infer<typeof resetConfirmSchema>;
|
type ResetConfirmFormData = z.infer<ReturnType<typeof createResetConfirmSchema>>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
@@ -104,10 +106,13 @@ export function PasswordResetConfirmForm({
|
|||||||
showLoginLink = true,
|
showLoginLink = true,
|
||||||
className,
|
className,
|
||||||
}: PasswordResetConfirmFormProps) {
|
}: PasswordResetConfirmFormProps) {
|
||||||
|
const t = useTranslations('auth.passwordResetConfirm');
|
||||||
const [serverError, setServerError] = useState<string | null>(null);
|
const [serverError, setServerError] = useState<string | null>(null);
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
const resetMutation = usePasswordResetConfirm();
|
const resetMutation = usePasswordResetConfirm();
|
||||||
|
|
||||||
|
const resetConfirmSchema = createResetConfirmSchema((key: string) => t(key));
|
||||||
|
|
||||||
const form = useForm<ResetConfirmFormData>({
|
const form = useForm<ResetConfirmFormData>({
|
||||||
resolver: zodResolver(resetConfirmSchema),
|
resolver: zodResolver(resetConfirmSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -134,9 +139,7 @@ export function PasswordResetConfirmForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
setSuccessMessage(
|
setSuccessMessage(t('success'));
|
||||||
'Your password has been successfully reset. You can now log in with your new password.'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
form.reset({ token, new_password: '', confirm_password: '' });
|
form.reset({ token, new_password: '', confirm_password: '' });
|
||||||
@@ -161,7 +164,7 @@ export function PasswordResetConfirmForm({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Unexpected error format
|
// Unexpected error format
|
||||||
setServerError('An unexpected error occurred. Please try again.');
|
setServerError(t('unexpectedError'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -186,9 +189,7 @@ export function PasswordResetConfirmForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Instructions */}
|
{/* Instructions */}
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">{t('instructions')}</p>
|
||||||
Enter your new password below. Make sure it meets all security requirements.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Hidden Token Field (for form submission) */}
|
{/* Hidden Token Field (for form submission) */}
|
||||||
<input type="hidden" {...form.register('token')} />
|
<input type="hidden" {...form.register('token')} />
|
||||||
@@ -196,12 +197,12 @@ export function PasswordResetConfirmForm({
|
|||||||
{/* New Password Field */}
|
{/* New Password Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="new_password">
|
<Label htmlFor="new_password">
|
||||||
New Password <span className="text-destructive">*</span>
|
{t('newPasswordLabel')} <span className="text-destructive">{t('required')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="new_password"
|
id="new_password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter new password"
|
placeholder={t('newPasswordPlaceholder')}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...form.register('new_password')}
|
{...form.register('new_password')}
|
||||||
@@ -240,7 +241,7 @@ export function PasswordResetConfirmForm({
|
|||||||
: 'text-muted-foreground'
|
: 'text-muted-foreground'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{passwordStrength.hasMinLength ? '✓' : '○'} At least 8 characters
|
{passwordStrength.hasMinLength ? '✓' : '○'} {t('passwordRequirements.minLength')}
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className={
|
className={
|
||||||
@@ -249,7 +250,7 @@ export function PasswordResetConfirmForm({
|
|||||||
: 'text-muted-foreground'
|
: 'text-muted-foreground'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{passwordStrength.hasNumber ? '✓' : '○'} Contains a number
|
{passwordStrength.hasNumber ? '✓' : '○'} {t('passwordRequirements.hasNumber')}
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className={
|
className={
|
||||||
@@ -258,7 +259,8 @@ export function PasswordResetConfirmForm({
|
|||||||
: 'text-muted-foreground'
|
: 'text-muted-foreground'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{passwordStrength.hasUppercase ? '✓' : '○'} Contains an uppercase letter
|
{passwordStrength.hasUppercase ? '✓' : '○'}{' '}
|
||||||
|
{t('passwordRequirements.hasUppercase')}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,12 +270,12 @@ export function PasswordResetConfirmForm({
|
|||||||
{/* Confirm Password Field */}
|
{/* Confirm Password Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="confirm_password">
|
<Label htmlFor="confirm_password">
|
||||||
Confirm Password <span className="text-destructive">*</span>
|
{t('confirmPasswordLabel')} <span className="text-destructive">{t('required')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="confirm_password"
|
id="confirm_password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Re-enter new password"
|
placeholder={t('confirmPasswordPlaceholder')}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...form.register('confirm_password')}
|
{...form.register('confirm_password')}
|
||||||
@@ -292,18 +294,18 @@ export function PasswordResetConfirmForm({
|
|||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||||
{isSubmitting ? 'Resetting Password...' : 'Reset Password'}
|
{isSubmitting ? t('resetButtonLoading') : t('resetButton')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Login Link */}
|
{/* Login Link */}
|
||||||
{showLoginLink && (
|
{showLoginLink && (
|
||||||
<p className="text-center text-sm text-muted-foreground">
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
Remember your password?{' '}
|
{t('rememberPassword')}{' '}
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
className="text-primary underline-offset-4 hover:underline font-medium"
|
className="text-primary underline-offset-4 hover:underline font-medium"
|
||||||
>
|
>
|
||||||
Back to login
|
{t('backToLogin')}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Link } from '@/lib/i18n/routing';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -22,11 +23,12 @@ import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/erro
|
|||||||
// Validation Schema
|
// Validation Schema
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const resetRequestSchema = z.object({
|
const createResetRequestSchema = (t: (key: string) => string) =>
|
||||||
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
|
z.object({
|
||||||
});
|
email: z.string().min(1, t('validation.required')).email(t('validation.email')),
|
||||||
|
});
|
||||||
|
|
||||||
type ResetRequestFormData = z.infer<typeof resetRequestSchema>;
|
type ResetRequestFormData = z.infer<ReturnType<typeof createResetRequestSchema>>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Component
|
// Component
|
||||||
@@ -64,10 +66,19 @@ export function PasswordResetRequestForm({
|
|||||||
showLoginLink = true,
|
showLoginLink = true,
|
||||||
className,
|
className,
|
||||||
}: PasswordResetRequestFormProps) {
|
}: PasswordResetRequestFormProps) {
|
||||||
|
const t = useTranslations('auth.passwordReset');
|
||||||
|
const tValidation = useTranslations('validation');
|
||||||
const [serverError, setServerError] = useState<string | null>(null);
|
const [serverError, setServerError] = useState<string | null>(null);
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
const resetMutation = usePasswordResetRequest();
|
const resetMutation = usePasswordResetRequest();
|
||||||
|
|
||||||
|
const resetRequestSchema = createResetRequestSchema((key: string) => {
|
||||||
|
if (key.startsWith('validation.')) {
|
||||||
|
return tValidation(key.replace('validation.', ''));
|
||||||
|
}
|
||||||
|
return t(key);
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm<ResetRequestFormData>({
|
const form = useForm<ResetRequestFormData>({
|
||||||
resolver: zodResolver(resetRequestSchema),
|
resolver: zodResolver(resetRequestSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -86,9 +97,7 @@ export function PasswordResetRequestForm({
|
|||||||
await resetMutation.mutateAsync({ email: data.email });
|
await resetMutation.mutateAsync({ email: data.email });
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
setSuccessMessage(
|
setSuccessMessage(t('success'));
|
||||||
'Password reset instructions have been sent to your email address. Please check your inbox.'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
form.reset();
|
form.reset();
|
||||||
@@ -113,7 +122,7 @@ export function PasswordResetRequestForm({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Unexpected error format
|
// Unexpected error format
|
||||||
setServerError('An unexpected error occurred. Please try again.');
|
setServerError(t('unexpectedError'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -138,19 +147,17 @@ export function PasswordResetRequestForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Instructions */}
|
{/* Instructions */}
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">{t('instructions')}</p>
|
||||||
Enter your email address and we'll send you instructions to reset your password.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Email Field */}
|
{/* Email Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email">
|
<Label htmlFor="email">
|
||||||
Email <span className="text-destructive">*</span>
|
{t('emailLabel')} <span className="text-destructive">{t('required')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="you@example.com"
|
placeholder={t('emailPlaceholder')}
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...form.register('email')}
|
{...form.register('email')}
|
||||||
@@ -167,18 +174,18 @@ export function PasswordResetRequestForm({
|
|||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||||
{isSubmitting ? 'Sending...' : 'Send Reset Instructions'}
|
{isSubmitting ? t('sendButtonLoading') : t('sendButton')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Login Link */}
|
{/* Login Link */}
|
||||||
{showLoginLink && (
|
{showLoginLink && (
|
||||||
<p className="text-center text-sm text-muted-foreground">
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
Remember your password?{' '}
|
{t('rememberPassword')}{' '}
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
className="text-primary underline-offset-4 hover:underline font-medium"
|
className="text-primary underline-offset-4 hover:underline font-medium"
|
||||||
>
|
>
|
||||||
Back to login
|
{t('backToLogin')}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Link } from '@/lib/i18n/routing';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -23,33 +24,34 @@ import config from '@/config/app.config';
|
|||||||
// Validation Schema
|
// Validation Schema
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const registerSchema = z
|
const createRegisterSchema = (t: (key: string) => string) =>
|
||||||
.object({
|
z
|
||||||
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
|
.object({
|
||||||
first_name: z
|
email: z.string().min(1, t('validation.required')).email(t('validation.email')),
|
||||||
.string()
|
first_name: z
|
||||||
.min(1, 'First name is required')
|
.string()
|
||||||
.min(2, 'First name must be at least 2 characters')
|
.min(1, t('firstNameRequired'))
|
||||||
.max(50, 'First name must not exceed 50 characters'),
|
.min(2, t('firstNameMinLength'))
|
||||||
last_name: z
|
.max(50, t('firstNameMaxLength')),
|
||||||
.string()
|
last_name: z
|
||||||
.max(50, 'Last name must not exceed 50 characters')
|
.string()
|
||||||
.optional()
|
.max(50, t('lastNameMaxLength'))
|
||||||
.or(z.literal('')), // Allow empty string
|
.optional()
|
||||||
password: z
|
.or(z.literal('')), // Allow empty string
|
||||||
.string()
|
password: z
|
||||||
.min(1, 'Password is required')
|
.string()
|
||||||
.min(8, 'Password must be at least 8 characters')
|
.min(1, t('passwordRequired'))
|
||||||
.regex(/[0-9]/, 'Password must contain at least one number')
|
.min(8, t('passwordMinLength'))
|
||||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter'),
|
.regex(/[0-9]/, t('passwordNumber'))
|
||||||
confirmPassword: z.string().min(1, 'Please confirm your password'),
|
.regex(/[A-Z]/, t('passwordUppercase')),
|
||||||
})
|
confirmPassword: z.string().min(1, t('confirmPasswordRequired')),
|
||||||
.refine((data) => data.password === data.confirmPassword, {
|
})
|
||||||
message: 'Passwords do not match',
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
path: ['confirmPassword'],
|
message: t('passwordMismatch'),
|
||||||
});
|
path: ['confirmPassword'],
|
||||||
|
});
|
||||||
|
|
||||||
type RegisterFormData = z.infer<typeof registerSchema>;
|
type RegisterFormData = z.infer<ReturnType<typeof createRegisterSchema>>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Component
|
// Component
|
||||||
@@ -84,9 +86,18 @@ interface RegisterFormProps {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function RegisterForm({ onSuccess, showLoginLink = true, className }: RegisterFormProps) {
|
export function RegisterForm({ onSuccess, showLoginLink = true, className }: RegisterFormProps) {
|
||||||
|
const t = useTranslations('auth.register');
|
||||||
|
const tValidation = useTranslations('validation');
|
||||||
const [serverError, setServerError] = useState<string | null>(null);
|
const [serverError, setServerError] = useState<string | null>(null);
|
||||||
const registerMutation = useRegister();
|
const registerMutation = useRegister();
|
||||||
|
|
||||||
|
const registerSchema = createRegisterSchema((key: string) => {
|
||||||
|
if (key.startsWith('validation.')) {
|
||||||
|
return tValidation(key.replace('validation.', ''));
|
||||||
|
}
|
||||||
|
return t(key);
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm<RegisterFormData>({
|
const form = useForm<RegisterFormData>({
|
||||||
resolver: zodResolver(registerSchema),
|
resolver: zodResolver(registerSchema),
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
@@ -133,7 +144,7 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Unexpected error format
|
// Unexpected error format
|
||||||
setServerError('An unexpected error occurred. Please try again.');
|
setServerError(t('unexpectedError'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -159,12 +170,12 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg
|
|||||||
{/* First Name Field */}
|
{/* First Name Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="first_name">
|
<Label htmlFor="first_name">
|
||||||
First Name <span className="text-destructive">*</span>
|
{t('firstNameLabel')} <span className="text-destructive">{t('required')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="first_name"
|
id="first_name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="John"
|
placeholder={t('firstNamePlaceholder')}
|
||||||
autoComplete="given-name"
|
autoComplete="given-name"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...form.register('first_name')}
|
{...form.register('first_name')}
|
||||||
@@ -180,11 +191,11 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg
|
|||||||
|
|
||||||
{/* Last Name Field */}
|
{/* Last Name Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="last_name">Last Name</Label>
|
<Label htmlFor="last_name">{t('lastNameLabel')}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="last_name"
|
id="last_name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Doe (optional)"
|
placeholder={t('lastNamePlaceholder')}
|
||||||
autoComplete="family-name"
|
autoComplete="family-name"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...form.register('last_name')}
|
{...form.register('last_name')}
|
||||||
@@ -201,12 +212,12 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg
|
|||||||
{/* Email Field */}
|
{/* Email Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email">
|
<Label htmlFor="email">
|
||||||
Email <span className="text-destructive">*</span>
|
{t('emailLabel')} <span className="text-destructive">{t('required')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="you@example.com"
|
placeholder={t('emailPlaceholder')}
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...form.register('email')}
|
{...form.register('email')}
|
||||||
@@ -223,12 +234,12 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg
|
|||||||
{/* Password Field */}
|
{/* Password Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="password">
|
<Label htmlFor="password">
|
||||||
Password <span className="text-destructive">*</span>
|
{t('passwordLabel')} <span className="text-destructive">{t('required')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Create a strong password"
|
placeholder={t('passwordPlaceholder')}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...form.register('password')}
|
{...form.register('password')}
|
||||||
@@ -253,21 +264,21 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg
|
|||||||
hasMinLength ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'
|
hasMinLength ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{hasMinLength ? '✓' : '○'} At least 8 characters
|
{hasMinLength ? '✓' : '○'} {t('passwordRequirements.minLength')}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={
|
className={
|
||||||
hasNumber ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'
|
hasNumber ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{hasNumber ? '✓' : '○'} Contains a number
|
{hasNumber ? '✓' : '○'} {t('passwordRequirements.hasNumber')}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={
|
className={
|
||||||
hasUppercase ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'
|
hasUppercase ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{hasUppercase ? '✓' : '○'} Contains an uppercase letter
|
{hasUppercase ? '✓' : '○'} {t('passwordRequirements.hasUppercase')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -276,12 +287,12 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg
|
|||||||
{/* Confirm Password Field */}
|
{/* Confirm Password Field */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="confirmPassword">
|
<Label htmlFor="confirmPassword">
|
||||||
Confirm Password <span className="text-destructive">*</span>
|
{t('confirmPasswordLabel')} <span className="text-destructive">{t('required')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Confirm your password"
|
placeholder={t('confirmPasswordPlaceholder')}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...form.register('confirmPassword')}
|
{...form.register('confirmPassword')}
|
||||||
@@ -299,18 +310,18 @@ export function RegisterForm({ onSuccess, showLoginLink = true, className }: Reg
|
|||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||||
{isSubmitting ? 'Creating account...' : 'Create account'}
|
{isSubmitting ? t('registerButtonLoading') : t('registerButton')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Login Link */}
|
{/* Login Link */}
|
||||||
{showLoginLink && (
|
{showLoginLink && (
|
||||||
<p className="text-center text-sm text-muted-foreground">
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
Already have an account?{' '}
|
{t('hasAccount')}{' '}
|
||||||
<Link
|
<Link
|
||||||
href={config.routes.login}
|
href={config.routes.login}
|
||||||
className="text-primary underline-offset-4 hover:underline font-medium"
|
className="text-primary underline-offset-4 hover:underline font-medium"
|
||||||
>
|
>
|
||||||
Sign in
|
{t('loginLink')}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Link } from '@/lib/i18n/routing';
|
|||||||
import { usePathname } from '@/lib/i18n/routing';
|
import { usePathname } from '@/lib/i18n/routing';
|
||||||
import { useAuth } from '@/lib/auth/AuthContext';
|
import { useAuth } from '@/lib/auth/AuthContext';
|
||||||
import { useLogout } from '@/lib/api/hooks/useAuth';
|
import { useLogout } from '@/lib/api/hooks/useAuth';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -68,6 +69,7 @@ function NavLink({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
|
const t = useTranslations('navigation');
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { mutate: logout, isPending: isLoggingOut } = useLogout();
|
const { mutate: logout, isPending: isLoggingOut } = useLogout();
|
||||||
|
|
||||||
@@ -87,9 +89,9 @@ export function Header() {
|
|||||||
{/* Navigation Links */}
|
{/* Navigation Links */}
|
||||||
<nav className="hidden md:flex items-center space-x-1">
|
<nav className="hidden md:flex items-center space-x-1">
|
||||||
<NavLink href="/" exact>
|
<NavLink href="/" exact>
|
||||||
Home
|
{t('home')}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{user?.is_superuser && <NavLink href="/admin">Admin</NavLink>}
|
{user?.is_superuser && <NavLink href="/admin">{t('admin')}</NavLink>}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -120,20 +122,20 @@ export function Header() {
|
|||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/settings/profile" className="cursor-pointer">
|
<Link href="/settings/profile" className="cursor-pointer">
|
||||||
<User className="mr-2 h-4 w-4" />
|
<User className="mr-2 h-4 w-4" />
|
||||||
Profile
|
{t('profile')}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/settings/password" className="cursor-pointer">
|
<Link href="/settings/password" className="cursor-pointer">
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
Settings
|
{t('settings')}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{user?.is_superuser && (
|
{user?.is_superuser && (
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/admin" className="cursor-pointer">
|
<Link href="/admin" className="cursor-pointer">
|
||||||
<Shield className="mr-2 h-4 w-4" />
|
<Shield className="mr-2 h-4 w-4" />
|
||||||
Admin Panel
|
{t('adminPanel')}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
@@ -144,7 +146,7 @@ export function Header() {
|
|||||||
disabled={isLoggingOut}
|
disabled={isLoggingOut}
|
||||||
>
|
>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
{isLoggingOut ? 'Logging out...' : 'Log out'}
|
{isLoggingOut ? t('loggingOut') : t('logout')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Alert } from '@/components/ui/alert';
|
import { Alert } from '@/components/ui/alert';
|
||||||
@@ -22,25 +23,26 @@ import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/erro
|
|||||||
// Validation Schema
|
// Validation Schema
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const passwordChangeSchema = z
|
const createPasswordChangeSchema = (t: (key: string) => string) =>
|
||||||
.object({
|
z
|
||||||
current_password: z.string().min(1, 'Current password is required'),
|
.object({
|
||||||
new_password: z
|
current_password: z.string().min(1, t('currentPasswordRequired')),
|
||||||
.string()
|
new_password: z
|
||||||
.min(1, 'New password is required')
|
.string()
|
||||||
.min(8, 'Password must be at least 8 characters')
|
.min(1, t('newPasswordRequired'))
|
||||||
.regex(/[0-9]/, 'Password must contain at least one number')
|
.min(8, t('newPasswordMinLength'))
|
||||||
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
.regex(/[0-9]/, t('newPasswordNumber'))
|
||||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
.regex(/[A-Z]/, t('newPasswordUppercase'))
|
||||||
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
|
.regex(/[a-z]/, t('newPasswordLowercase'))
|
||||||
confirm_password: z.string().min(1, 'Please confirm your new password'),
|
.regex(/[^A-Za-z0-9]/, t('newPasswordSpecial')),
|
||||||
})
|
confirm_password: z.string().min(1, t('confirmPasswordRequired')),
|
||||||
.refine((data) => data.new_password === data.confirm_password, {
|
})
|
||||||
message: 'Passwords do not match',
|
.refine((data) => data.new_password === data.confirm_password, {
|
||||||
path: ['confirm_password'],
|
message: t('passwordMismatch'),
|
||||||
});
|
path: ['confirm_password'],
|
||||||
|
});
|
||||||
|
|
||||||
type PasswordChangeFormData = z.infer<typeof passwordChangeSchema>;
|
type PasswordChangeFormData = z.infer<ReturnType<typeof createPasswordChangeSchema>>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Component
|
// Component
|
||||||
@@ -72,6 +74,7 @@ interface PasswordChangeFormProps {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormProps) {
|
export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormProps) {
|
||||||
|
const t = useTranslations('settings.password');
|
||||||
const [serverError, setServerError] = useState<string | null>(null);
|
const [serverError, setServerError] = useState<string | null>(null);
|
||||||
const passwordChangeMutation = usePasswordChange((message) => {
|
const passwordChangeMutation = usePasswordChange((message) => {
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
@@ -79,6 +82,8 @@ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormP
|
|||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const passwordChangeSchema = createPasswordChangeSchema((key: string) => t(key));
|
||||||
|
|
||||||
const form = useForm<PasswordChangeFormData>({
|
const form = useForm<PasswordChangeFormData>({
|
||||||
resolver: zodResolver(passwordChangeSchema),
|
resolver: zodResolver(passwordChangeSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -122,7 +127,7 @@ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormP
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Unexpected error format
|
// Unexpected error format
|
||||||
setServerError('An unexpected error occurred. Please try again.');
|
setServerError(t('unexpectedError'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -133,10 +138,8 @@ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormP
|
|||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Change Password</CardTitle>
|
<CardTitle>{t('title')}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>{t('subtitle')}</CardDescription>
|
||||||
Update your password to keep your account secure. Make sure it's strong and unique.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
@@ -149,9 +152,9 @@ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormP
|
|||||||
|
|
||||||
{/* Current Password Field */}
|
{/* Current Password Field */}
|
||||||
<FormField
|
<FormField
|
||||||
label="Current Password"
|
label={t('currentPasswordLabel')}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter your current password"
|
placeholder={t('currentPasswordPlaceholder')}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
required
|
required
|
||||||
@@ -161,22 +164,22 @@ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormP
|
|||||||
|
|
||||||
{/* New Password Field */}
|
{/* New Password Field */}
|
||||||
<FormField
|
<FormField
|
||||||
label="New Password"
|
label={t('newPasswordLabel')}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter your new password"
|
placeholder={t('newPasswordPlaceholder')}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
required
|
required
|
||||||
description="At least 8 characters with uppercase, lowercase, number, and special character"
|
description={t('newPasswordDescription')}
|
||||||
error={form.formState.errors.new_password}
|
error={form.formState.errors.new_password}
|
||||||
{...form.register('new_password')}
|
{...form.register('new_password')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Confirm Password Field */}
|
{/* Confirm Password Field */}
|
||||||
<FormField
|
<FormField
|
||||||
label="Confirm New Password"
|
label={t('confirmPasswordLabel')}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Confirm your new password"
|
placeholder={t('confirmPasswordPlaceholder')}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
required
|
required
|
||||||
@@ -187,12 +190,12 @@ export function PasswordChangeForm({ onSuccess, className }: PasswordChangeFormP
|
|||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button type="submit" disabled={isSubmitting || !isDirty}>
|
<Button type="submit" disabled={isSubmitting || !isDirty}>
|
||||||
{isSubmitting ? 'Changing Password...' : 'Change Password'}
|
{isSubmitting ? t('updateButtonLoading') : t('updateButton')}
|
||||||
</Button>
|
</Button>
|
||||||
{/* istanbul ignore next - Cancel button requires isDirty state, tested in E2E */}
|
{/* istanbul ignore next - Cancel button requires isDirty state, tested in E2E */}
|
||||||
{isDirty && !isSubmitting && (
|
{isDirty && !isSubmitting && (
|
||||||
<Button type="button" variant="outline" onClick={() => form.reset()}>
|
<Button type="button" variant="outline" onClick={() => form.reset()}>
|
||||||
Cancel
|
{t('cancelButton')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Alert } from '@/components/ui/alert';
|
import { Alert } from '@/components/ui/alert';
|
||||||
@@ -23,21 +24,18 @@ import { getGeneralError, getFieldErrors, isAPIErrorArray } from '@/lib/api/erro
|
|||||||
// Validation Schema
|
// Validation Schema
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const profileSchema = z.object({
|
const createProfileSchema = (t: (key: string) => string) =>
|
||||||
first_name: z
|
z.object({
|
||||||
.string()
|
first_name: z
|
||||||
.min(1, 'First name is required')
|
.string()
|
||||||
.min(2, 'First name must be at least 2 characters')
|
.min(1, t('firstNameRequired'))
|
||||||
.max(50, 'First name must not exceed 50 characters'),
|
.min(2, t('firstNameMinLength'))
|
||||||
last_name: z
|
.max(50, t('firstNameMaxLength')),
|
||||||
.string()
|
last_name: z.string().max(50, t('lastNameMaxLength')).optional().or(z.literal('')),
|
||||||
.max(50, 'Last name must not exceed 50 characters')
|
email: z.string().email(t('emailInvalid')),
|
||||||
.optional()
|
});
|
||||||
.or(z.literal('')),
|
|
||||||
email: z.string().email('Invalid email address'),
|
|
||||||
});
|
|
||||||
|
|
||||||
type ProfileFormData = z.infer<typeof profileSchema>;
|
type ProfileFormData = z.infer<ReturnType<typeof createProfileSchema>>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Component
|
// Component
|
||||||
@@ -67,6 +65,7 @@ interface ProfileSettingsFormProps {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFormProps) {
|
export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFormProps) {
|
||||||
|
const t = useTranslations('settings.profile');
|
||||||
const [serverError, setServerError] = useState<string | null>(null);
|
const [serverError, setServerError] = useState<string | null>(null);
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const updateProfileMutation = useUpdateProfile((message) => {
|
const updateProfileMutation = useUpdateProfile((message) => {
|
||||||
@@ -74,6 +73,8 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor
|
|||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const profileSchema = createProfileSchema((key: string) => t(key));
|
||||||
|
|
||||||
const form = useForm<ProfileFormData>({
|
const form = useForm<ProfileFormData>({
|
||||||
resolver: zodResolver(profileSchema),
|
resolver: zodResolver(profileSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -135,7 +136,7 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Unexpected error format
|
// Unexpected error format
|
||||||
setServerError('An unexpected error occurred. Please try again.');
|
setServerError(t('unexpectedError'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -146,10 +147,8 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor
|
|||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Profile Information</CardTitle>
|
<CardTitle>{t('title')}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>{t('subtitle')}</CardDescription>
|
||||||
Update your personal information. Your email address is read-only.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
@@ -162,9 +161,9 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor
|
|||||||
|
|
||||||
{/* First Name Field */}
|
{/* First Name Field */}
|
||||||
<FormField
|
<FormField
|
||||||
label="First Name"
|
label={t('firstNameLabel')}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="John"
|
placeholder={t('firstNamePlaceholder')}
|
||||||
autoComplete="given-name"
|
autoComplete="given-name"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
required
|
required
|
||||||
@@ -174,9 +173,9 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor
|
|||||||
|
|
||||||
{/* Last Name Field */}
|
{/* Last Name Field */}
|
||||||
<FormField
|
<FormField
|
||||||
label="Last Name"
|
label={t('lastNameLabel')}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Doe"
|
placeholder={t('lastNamePlaceholder')}
|
||||||
autoComplete="family-name"
|
autoComplete="family-name"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
error={form.formState.errors.last_name}
|
error={form.formState.errors.last_name}
|
||||||
@@ -185,11 +184,11 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor
|
|||||||
|
|
||||||
{/* Email Field (Read-only) */}
|
{/* Email Field (Read-only) */}
|
||||||
<FormField
|
<FormField
|
||||||
label="Email"
|
label={t('emailLabel')}
|
||||||
type="email"
|
type="email"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
disabled
|
disabled
|
||||||
description="Your email address cannot be changed from this form"
|
description={t('emailDescription')}
|
||||||
error={form.formState.errors.email}
|
error={form.formState.errors.email}
|
||||||
{...form.register('email')}
|
{...form.register('email')}
|
||||||
/>
|
/>
|
||||||
@@ -197,12 +196,12 @@ export function ProfileSettingsForm({ onSuccess, className }: ProfileSettingsFor
|
|||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button type="submit" disabled={isSubmitting || !isDirty}>
|
<Button type="submit" disabled={isSubmitting || !isDirty}>
|
||||||
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
{isSubmitting ? t('updateButtonLoading') : t('updateButton')}
|
||||||
</Button>
|
</Button>
|
||||||
{/* istanbul ignore next - Reset button requires isDirty state, tested in E2E */}
|
{/* istanbul ignore next - Reset button requires isDirty state, tested in E2E */}
|
||||||
{isDirty && !isSubmitting && (
|
{isDirty && !isSubmitting && (
|
||||||
<Button type="button" variant="outline" onClick={() => form.reset()}>
|
<Button type="button" variant="outline" onClick={() => form.reset()}>
|
||||||
Reset
|
{t('resetButton')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user