From 49e0f915e11217ba368a689b68003bde8be1d5bb Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Thu, 29 May 2025 17:42:32 -0300 Subject: [PATCH] Fix/410 fix languages (#483) * feat: handle languages and save it in the backend. Issue #418. Issue #410 * test: fix test cases * test: add test cases in the backend --- .../__test__/utils/PortugueseUtils.test.ts | 1 + .../src/__test__/utils/RussianUtils.test.ts | 1 + .../src/__test__/utils/SpanishUtils.test.ts | 2 + client/src/__test__/views/Account.test.tsx | 22 ++--- client/src/components/LoginForm/index.tsx | 6 +- client/src/constants/english.ts | 22 ++++- client/src/constants/languageConstants.ts | 5 +- client/src/constants/portuguese.ts | 22 ++++- client/src/constants/russian.ts | 22 ++++- client/src/constants/serverResponses.ts | 3 +- client/src/constants/spanish.ts | 22 ++++- client/src/context/AuthContext.ts | 3 +- client/src/context/AuthProvider.tsx | 7 +- client/src/lang-service/LangHandler.ts | 5 +- client/src/types/SigninResponse.ts | 1 + client/src/types/UserPatchRequest.ts | 1 + client/src/types/UserRegistration.ts | 6 ++ client/src/types/UserResponse.ts | 1 + client/src/views/About/index.tsx | 19 +++-- client/src/views/Account/index.tsx | 80 ++++++++++++------- client/src/views/Home/index.tsx | 2 +- .../tasknoteapp/server/entity/UserEntity.java | 5 +- .../exception/BadLanguageException.java | 14 ++++ .../server/request/LoginRequest.java | 7 ++ .../server/request/UserPatchRequest.java | 3 +- .../response/UserResponseWithToken.java | 6 +- .../server/service/AuthService.java | 43 +++++++--- .../server/service/MailgunEmailService.java | 23 ++++++ .../server/templates/MailgunTemplate.java | 5 ++ .../MailgunTemplateEmailChanged.java | 44 ++++++++++ .../V202505280632__add_user_lang.sql | 2 + .../AuthenticationControllerTest.java | 22 ++--- .../server/controller/UserControllerTest.java | 2 +- .../server/service/AuthServiceTest.java | 21 ++--- .../service/MailgunEmailServiceTest.java | 15 ++++ .../server/templates/MailgunTemplateTest.java | 13 +++ 36 files changed, 375 insertions(+), 103 deletions(-) create mode 100644 client/src/types/UserRegistration.ts create mode 100644 server/src/main/java/br/com/tasknoteapp/server/exception/BadLanguageException.java create mode 100644 server/src/main/java/br/com/tasknoteapp/server/templates/MailgunTemplateEmailChanged.java create mode 100644 server/src/main/resources/db/migration/V202505280632__add_user_lang.sql diff --git a/client/src/__test__/utils/PortugueseUtils.test.ts b/client/src/__test__/utils/PortugueseUtils.test.ts index 96d898f..730719c 100644 --- a/client/src/__test__/utils/PortugueseUtils.test.ts +++ b/client/src/__test__/utils/PortugueseUtils.test.ts @@ -55,5 +55,6 @@ describe('Portuguese Utils unit tests', () => { expect(translateServerResponse(keys[17], 'pt_br')).toBe('Identificação incorreta ou faltando'); expect(translateServerResponse(keys[18], 'pt_br')).toBe('Informação errada ou incompleta!'); expect(translateServerResponse(keys[19], 'pt_br')).toBe('E-mail ou senha inválidos!'); + expect(translateServerResponse(keys[20], 'pt_br')).toBe('Nada para atualizar!'); }); }); diff --git a/client/src/__test__/utils/RussianUtils.test.ts b/client/src/__test__/utils/RussianUtils.test.ts index 75115ae..be56d0f 100644 --- a/client/src/__test__/utils/RussianUtils.test.ts +++ b/client/src/__test__/utils/RussianUtils.test.ts @@ -76,5 +76,6 @@ describe('Russian Utils unit tests', () => { expect(translateServerResponse(keys[17], 'ru')).toBe('Неправильная или отсутствующая идентификация'); expect(translateServerResponse(keys[18], 'ru')).toBe('Неверная или отсутствующая информация!'); expect(translateServerResponse(keys[19], 'ru')).toBe('Неправильный пользователь или пароль'); + expect(translateServerResponse(keys[20], 'ru')).toBe('Нечего обновлять!'); }); }); diff --git a/client/src/__test__/utils/SpanishUtils.test.ts b/client/src/__test__/utils/SpanishUtils.test.ts index 875a70d..f018a44 100644 --- a/client/src/__test__/utils/SpanishUtils.test.ts +++ b/client/src/__test__/utils/SpanishUtils.test.ts @@ -54,5 +54,7 @@ describe('Spanish Utils unit tests', () => { expect(translateServerResponse(keys[16], 'es')).toBe('Error desconocido'); expect(translateServerResponse(keys[17], 'es')).toBe('Identificación incorrecta o faltante'); expect(translateServerResponse(keys[18], 'es')).toBe('¡Información incorrecta o incompleta!'); + expect(translateServerResponse(keys[19], 'es')).toBe('¡Usuario o contraseña incorrectos!'); + expect(translateServerResponse(keys[20], 'es')).toBe('¡Nada que actualizar!'); }); }); diff --git a/client/src/__test__/views/Account.test.tsx b/client/src/__test__/views/Account.test.tsx index bc0ad41..b586df0 100644 --- a/client/src/__test__/views/Account.test.tsx +++ b/client/src/__test__/views/Account.test.tsx @@ -36,7 +36,8 @@ const authContextMock = { email: 'test@example.com', admin: false, createdAt: new Date(), - gravatarImageUrl: 'http://image.com' + gravatarImageUrl: 'http://image.com', + lang: 'en' }, checkCurrentAuthUser: vi.fn(), signIn: vi.fn(), @@ -61,7 +62,7 @@ describe('Account Component', () => { it('should render the Account component', () => { const { getByText } = renderAccount(); - expect(getByText('Update only what you need. Blank fields will not be updated')).toBeDefined(); + expect(getByText('account_data_update_header')).toBeDefined(); }); it('should change language when a language button is clicked', () => { @@ -96,12 +97,12 @@ describe('Account Component', () => { const { getByLabelText, getByText, getByTestId } = renderAccount(); - fireEvent.change(getByLabelText(/First name/i), { target: { value: 'Jane' } }); - fireEvent.change(getByLabelText(/Email/i), { target: { value: 'jane.doe@example.com' } }); + fireEvent.change(getByLabelText('account_form_first_name_label'), { target: { value: 'Jane' } }); + fireEvent.change(getByLabelText('login_email_label'), { target: { value: 'jane.doe@example.com' } }); fireEvent.change(getByTestId('account-password-one'), { target: { value: 'password123' } }); - fireEvent.change(getByLabelText(/Repeat password/i), { target: { value: 'password123' } }); + fireEvent.change(getByLabelText(/register_password_repeat_label/i), { target: { value: 'password123' } }); - fireEvent.click(getByText(/Save/i)); + fireEvent.click(getByText('account_form_save')); await waitFor(() => { expect(mockPatchJSON).toHaveBeenCalledWith(expect.any(String), { @@ -109,6 +110,7 @@ describe('Account Component', () => { email: 'jane.doe@example.com', password: 'password123', passwordAgain: 'password123', + lang: '' }); }); @@ -118,10 +120,10 @@ describe('Account Component', () => { it('should render text based on new contentHeader component', () => { const { getByText } = renderAccount(); - expect(getByText('My')).toBeDefined(); - expect(getByText('Account')).toBeDefined(); + expect(getByText('account_header_my')).toBeDefined(); + expect(getByText('account_header_account')).toBeDefined(); expect(getByText('account_my_account_hello')).toBeDefined(); - expect(getByText('Update and Manage, Your')).toBeDefined(); - expect(getByText('Data')).toBeDefined(); + expect(getByText('account_header_update_manage')).toBeDefined(); + expect(getByText('account_header_data')).toBeDefined(); }); }); diff --git a/client/src/components/LoginForm/index.tsx b/client/src/components/LoginForm/index.tsx index 025e6af..5fc230b 100644 --- a/client/src/components/LoginForm/index.tsx +++ b/client/src/components/LoginForm/index.tsx @@ -39,6 +39,7 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { const [passwordAgain, setPasswordAgain] = useState(''); const [secondsLeft, setSecondsLeft] = useState(0); const [isResendEnabled, setIsResendEnabled] = useState(true); + const [langToSave, setLangToSave] = useState(''); /** * Navigates to the specified page. @@ -87,7 +88,7 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { goTo('/home'); } else if (prefix === 'register') { - await register(email, password, passwordAgain); + await register({ email, password, passwordAgain, lang: langToSave }); // Do not clear the email, because user might request to resend setPassword(''); setPasswordAgain(''); @@ -139,7 +140,8 @@ function LoginForm({ prefix }: { prefix: string }): React.ReactNode { }; useEffect(() => { - handleDefaultLang(); + const lang = handleDefaultLang(); + setLangToSave(lang); }, [formInvalid]); useEffect(() => { diff --git a/client/src/constants/english.ts b/client/src/constants/english.ts index 20a866d..226d021 100644 --- a/client/src/constants/english.ts +++ b/client/src/constants/english.ts @@ -114,6 +114,11 @@ const enTranslations = { note_table_btn_edit: 'Edit', note_table_btn_delete: 'Delete', + about_page_title_one: 'About the', + about_page_title_two: 'TaskNote App', + about_page_title_three: 'Tasks and notes made', + about_page_title_four: 'Easy', + about_page_subtitle: 'Find more information about us and the app', about_app_title: 'About TaskNote', about_app_description: `TaskNote is your go-to application for managing tasks and notes in one convenient place. Whether you're keeping track of @@ -146,13 +151,26 @@ const enTranslations = { passionate about building applications that make life easier and more organized. You can reach out to me at `, about_dev_description_two: ' for any questions or feedback.', + about_buy_coffee_one: 'You can also ', + about_buy_coffee_link: 'Buy me a coffee', - account_my_account_title: 'My Account', + account_header_my: 'My', + account_header_account: 'Account', + account_header_update_manage: 'Update and Manage, Your', + account_header_data: 'Data', + account_data_update_header: 'Update only what you need. Blank fields will not be updated', + account_form_first_name_label: 'Fist name', + account_form_first_name_placeholder: 'Your name', + account_form_save: 'Save', + account_form_gravatar_one: 'If you want to display your picture, we support Gravatar. Please head to ', + account_form_gravatar_two: ' to register or update your profile picture. Once updated, please wait a few seconds to see it here.', account_my_account_hello: 'Hello! This is where you can manage your preferences', account_my_account_logged: 'You\'re logged in as: ', - account_app_lang_title: 'App Language', + account_app_lang_title: 'Change the app language', account_app_lang_description: 'You can choose one the languages below', + account_app_lang_available: 'Available languages', account_privacy_little: 'Your Privacy Matters', + account_privacy_subtitle: 'You decide when to delete your data', account_privacy_text: `We're committed to protecting your privacy and giving you full control over your data. You can request complete account deletion at any time. Once processed, all your personal information will be diff --git a/client/src/constants/languageConstants.ts b/client/src/constants/languageConstants.ts index 76b6fcb..dcc4e22 100644 --- a/client/src/constants/languageConstants.ts +++ b/client/src/constants/languageConstants.ts @@ -84,6 +84,7 @@ export const serverResponsesTranslations: Record = { UNKNOWN_pt_br: 'Erro desconhecido', WRONG_IDENTIFICATION_pt_br: 'Identificação incorreta ou faltando', WRONG_OR_MISSING_INFO_pt_br: 'Informação errada ou incompleta!', + NOTHING_TO_UPDATE_pt_br: 'Nada para atualizar!', BAD_PASSWORD_3_es: 'Contraseña inválida: La contraseña debe tener al menos 8 caracteres, 1 mayúscula y 1 carácter especial', BAD_PASSWORD_2_es: 'Contraseña inválida: La contraseña debe tener al menos 1 mayúscula y 1 carácter especial', @@ -105,6 +106,7 @@ export const serverResponsesTranslations: Record = { UNKNOWN_es: 'Error desconocido', WRONG_IDENTIFICATION_es: 'Identificación incorrecta o faltante', WRONG_OR_MISSING_INFO_es: '¡Información incorrecta o incompleta!', + NOTHING_TO_UPDATE_es: '¡Nada que actualizar!', BAD_PASSWORD_3_ru: 'Неправильный пароль: Пароль должен содержать не менее 8 символов, 1 заглавную букву, 1 специальный символ.', BAD_PASSWORD_2_ru: 'Неправильный пароль: Пароль должен содержать как минимум 1 заглавную букву и 1 специальный символ.', @@ -125,5 +127,6 @@ export const serverResponsesTranslations: Record = { RECOVER_PASSWORD_ru: 'Если введенный вами адрес электронной почты связан с учетной записью, вы вскоре получите ссылку для сброса пароля.', UNKNOWN_ru: 'Неизвестная ошибка', WRONG_IDENTIFICATION_ru: 'Неправильная или отсутствующая идентификация', - WRONG_OR_MISSING_INFO_ru: 'Неверная или отсутствующая информация!' + WRONG_OR_MISSING_INFO_ru: 'Неверная или отсутствующая информация!', + NOTHING_TO_UPDATE_ru: 'Нечего обновлять!' }; diff --git a/client/src/constants/portuguese.ts b/client/src/constants/portuguese.ts index 01f988f..3aa132b 100644 --- a/client/src/constants/portuguese.ts +++ b/client/src/constants/portuguese.ts @@ -114,6 +114,11 @@ const ptBrTranslations = { note_table_btn_edit: 'Alterar', note_table_btn_delete: 'Excluir', + about_page_title_one: 'Sobre o', + about_page_title_two: 'App TaskNote', + about_page_title_three: 'Tarefas e notas de forma', + about_page_title_four: 'Fácil', + about_page_subtitle: 'Saiba mais sobre nós e sobre o app', about_app_title: 'Sobre o TaskNote', about_app_description: `TaskNote é sua aplicação ideal para gerenciamento de tarefas e notas em apenas um lugar. Esteja você buscando registrar sua @@ -147,13 +152,26 @@ const ptBrTranslations = { Sou apaixonado por criar aplicações que fazem a vida mais fácil e organizada. Você pode me contactar em `, about_dev_description_two: ' para qualquer dúvida ou comentário.', + about_buy_coffee_one: 'Você também pode ', + about_buy_coffee_link: 'Me pagar um café', - account_my_account_title: 'Minha conta', + account_header_my: 'Minha', + account_header_account: 'Conta', + account_header_update_manage: 'Atualize e Gerencie, Seus', + account_header_data: 'Dados', + account_data_update_header: 'Atualize apenas o que você precisa. Campos em branco não serão atualizados', + account_form_first_name_label: 'Primeiro nome', + account_form_first_name_placeholder: 'Seu nome', + account_form_save: 'Salvar', + account_form_gravatar_one: 'Se você quer adicionar sua foto, nós suportamos Gravatar. Por favor acesse ', + account_form_gravatar_two: ' para se registrar ou atualizar seu perfil. Uma vez atualizado, por favor aguarde alguns segundos para vê-la aqui.', account_my_account_hello: 'Olá! Aqui é onde você pode gerenciar suas preferências.', account_my_account_logged: 'Você está logado como: ', - account_app_lang_title: 'Idioma do App', + account_app_lang_title: 'Mude o idioma do app', account_app_lang_description: 'Você pode escolher um destes idiomas:', + account_app_lang_available: 'Idiomas disponívels', account_privacy_little: 'Sua Privacidade Importa', + account_privacy_subtitle: 'Você decide quanto excluir seus dados', account_privacy_text: `Estamos comprometidos em proteger sua privacidade e te dar controle total sobre seus dados. Você pode solicitar a remoção completa da sua conta a qualquer hora. Uma vez processado, todos os seus dados pessoais serão excluídos diff --git a/client/src/constants/russian.ts b/client/src/constants/russian.ts index 2696b27..ea005cc 100644 --- a/client/src/constants/russian.ts +++ b/client/src/constants/russian.ts @@ -114,6 +114,11 @@ const ruTranslations = { note_table_btn_edit: 'Редактировать', note_table_btn_delete: 'Удалить', + about_page_title_one: 'около', + about_page_title_two: 'TaskNote App', + about_page_title_three: 'Задачи и заметки стали', + about_page_title_four: 'проще', + about_page_subtitle: ' Узнайте больше о нас и приложении', about_app_title: 'О TaskNote', about_app_description: `TaskNote — это ваше приложение для управления задачами и заметками в одном удобном месте. Независимо от того, отслеживаете ли вы @@ -146,13 +151,26 @@ const ruTranslations = { которые делают жизнь проще и более организованной. Вы можете связаться со мной по адресу `, about_dev_description_two: ' для любых вопросов или отзывов.', + about_buy_coffee_one: 'Ты также можешь ', + about_buy_coffee_link: 'купить мне кофе.', - account_my_account_title: 'Мой аккаунт', + account_header_my: 'Мой', + account_header_account: 'аккаунт', + account_header_update_manage: 'Обновление и управление', + account_header_data: 'данные', + account_data_update_header: 'Обновляйте только то, что вам нужно. Пустые поля не будут обновлены', + account_form_first_name_label: 'имя', + account_form_first_name_placeholder: 'Ваше имя', + account_form_save: 'Сохранить', + account_form_gravatar_one: 'Если вы хотите показать свою фотографию, мы поддерживаем Gravatar. Пожалуйста, перейдите на ', + account_form_gravatar_two: ' для регистрации или обновления вашего фото профиля. После обновления, пожалуйста, подождите несколько секунд, чтобы увидеть его здесь.', account_my_account_hello: 'Привет! Здесь вы можете управлять своими предпочтениями.', account_my_account_logged: 'Вы вошли как:', - account_app_lang_title: 'Язык приложения', + account_app_lang_title: 'Изменить язык приложения', account_app_lang_description: 'Вы можете выбрать один из этих языков:', + account_app_lang_available: 'Доступные языки', account_privacy_little: 'Ваша конфиденциальность имеет значение', + account_privacy_subtitle: 'Вы сами решаете, когда удалять свои данные', account_privacy_text: `Мы стремимся защищать вашу конфиденциальность и предоставлять вам полный контроль над вашими данными. Вы можете запросить полное удаление аккаунта в любое время. После обработки вся ваша личная информация будет diff --git a/client/src/constants/serverResponses.ts b/client/src/constants/serverResponses.ts index aee6899..e0bb741 100644 --- a/client/src/constants/serverResponses.ts +++ b/client/src/constants/serverResponses.ts @@ -18,5 +18,6 @@ export const serverResponses: Record = { 'Unknown error': 'UNKNOWN', 'Wrong or missing identification': 'WRONG_IDENTIFICATION', 'Wrong or missing information!': 'WRONG_OR_MISSING_INFO', - 'Invalid credentials': 'INVALID_CREDENTIALS' + 'Invalid credentials': 'INVALID_CREDENTIALS', + 'Nothing to update!': 'NOTHING_TO_UPDATE' }; diff --git a/client/src/constants/spanish.ts b/client/src/constants/spanish.ts index 43e94e6..1e64b13 100644 --- a/client/src/constants/spanish.ts +++ b/client/src/constants/spanish.ts @@ -114,6 +114,11 @@ const esTranslations = { note_table_btn_edit: 'Editar', note_table_btn_delete: 'Eliminar', + about_page_title_one: 'Acerca de', + about_page_title_two: 'TaskNote App', + about_page_title_three: 'Tareas y notas', + about_page_title_four: 'Simplificadas', + about_page_subtitle: 'Encuentre más información sobre nosotros y la aplicación.', about_app_title: 'Acerca de TaskNote', about_app_description: `TaskNote es tu aplicación ideal para gestionar tareas y notas en un solo lugar. Ya sea que estés organizando tareas diarias @@ -146,13 +151,26 @@ const esTranslations = { Me apasiona crear aplicaciones que faciliten la vida y ayuden a organizarse. Puedes contactarme en `, about_dev_description_two: ' para cualquier duda o comentario.', + about_buy_coffee_one: 'También puedes ', + about_buy_coffee_link: 'Comprarme un café', - account_my_account_title: 'Mi Cuenta', + account_header_my: 'Mi', + account_header_account: 'Cuenta', + account_header_update_manage: 'Actualize y Administre, Sus', + account_header_data: 'Datos', + account_data_update_header: 'Actualice solo lo necesario. Los campos en blanco no se actualizarán', + account_form_first_name_label: 'Nombre de pila', + account_form_first_name_placeholder: 'Su nombre', + account_form_save: 'Guardar', + account_form_gravatar_one: 'Si quieres mostrar tu foto, admitimos Gravatar. Visita ', + account_form_gravatar_two: ' Para registrarte o actualizar tu foto de perfil. Una vez actualizada, espera unos segundos para verla aquí.', account_my_account_hello: '¡Hola! Aquí puedes gestionar tus preferencias.', account_my_account_logged: 'Has iniciado sesión como: ', - account_app_lang_title: 'Idioma del App', + account_app_lang_title: 'Cambia el idioma del App', account_app_lang_description: 'Puedes elegir uno de estos idiomas:', + account_app_lang_available: 'Idiomas disponíbles', account_privacy_little: 'Su Privacidad es Importante', + account_privacy_subtitle: 'Tú decides cuándo eliminar tus datos', account_privacy_text: `Nos comprometemos a proteger su privacidad y a brindarle control total sobre sus datos. Puedes solicitar la eliminación completa de su cuenta en cualquier momento. Una vez procesada, toda su información personal diff --git a/client/src/context/AuthContext.ts b/client/src/context/AuthContext.ts index 85a0c31..0a16691 100644 --- a/client/src/context/AuthContext.ts +++ b/client/src/context/AuthContext.ts @@ -1,5 +1,6 @@ import { createContext } from 'react'; import { UserResponse } from '../types/UserResponse'; +import { UserRegistration } from '../types/UserRegistration'; export interface AuthContextData { signed: boolean; @@ -7,7 +8,7 @@ export interface AuthContextData { checkCurrentAuthUser: (pathname: string) => Promise; signIn: (email: string, password: string) => Promise; signOut: () => void; - register: (email: string, password: string, passwordAgain: string) => Promise; + register: (payload: UserRegistration) => Promise; isAdmin: boolean; updateUser: (userUpdated: UserResponse) => void; } diff --git a/client/src/context/AuthProvider.tsx b/client/src/context/AuthProvider.tsx index f7e9067..b93b3f8 100644 --- a/client/src/context/AuthProvider.tsx +++ b/client/src/context/AuthProvider.tsx @@ -5,6 +5,7 @@ import { SigninResponse } from '../types/SigninResponse'; import api from '../api-service/api'; import ApiConfig from '../api-service/apiConfig'; import { UserResponse } from '../types/UserResponse'; +import { UserRegistration } from '../types/UserRegistration'; interface Props { children: React.ReactNode; @@ -72,9 +73,8 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro } }; - const register = async (email: string, password: string, passwordAgain: string): Promise => { + const register = async (payload: UserRegistration): Promise => { try { - const payload = { email, password, passwordAgain }; await api.putJSON(ApiConfig.registerUrl, payload); return Promise.resolve('OK'); } @@ -96,7 +96,8 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro email: registerResponse.email, admin: registerResponse.admin, createdAt: new Date(registerResponse.createdAt), - gravatarImageUrl: registerResponse.gravatarImageUrl + gravatarImageUrl: registerResponse.gravatarImageUrl, + lang: registerResponse.lang }; setSigned(true); diff --git a/client/src/lang-service/LangHandler.ts b/client/src/lang-service/LangHandler.ts index 2f1cf95..54ca27d 100644 --- a/client/src/lang-service/LangHandler.ts +++ b/client/src/lang-service/LangHandler.ts @@ -11,11 +11,12 @@ function handleLanguage(lang: string) { setDefaultLang(lang); } -const handleDefaultLang = () => { - const lang = getDefaultLang(); +const handleDefaultLang = (langFromServer?: string): string => { + const lang = langFromServer ?? getDefaultLang(); if (lang !== 'en') { handleLanguage(lang); } + return lang; }; export { handleDefaultLang }; diff --git a/client/src/types/SigninResponse.ts b/client/src/types/SigninResponse.ts index 9e318dc..5efb5bc 100644 --- a/client/src/types/SigninResponse.ts +++ b/client/src/types/SigninResponse.ts @@ -6,4 +6,5 @@ export type SigninResponse = { createdAt: Date; token: string; gravatarImageUrl: string; + lang: string; }; diff --git a/client/src/types/UserPatchRequest.ts b/client/src/types/UserPatchRequest.ts index e868f44..153a79b 100644 --- a/client/src/types/UserPatchRequest.ts +++ b/client/src/types/UserPatchRequest.ts @@ -3,4 +3,5 @@ export type UserPatchRequest = { email: string | null; password: string | null; passwordAgain: string | null; + lang: string | null; }; diff --git a/client/src/types/UserRegistration.ts b/client/src/types/UserRegistration.ts new file mode 100644 index 0000000..7820025 --- /dev/null +++ b/client/src/types/UserRegistration.ts @@ -0,0 +1,6 @@ +export type UserRegistration = { + email: string; + password: string; + passwordAgain: string; + lang: string; +}; diff --git a/client/src/types/UserResponse.ts b/client/src/types/UserResponse.ts index 1bfc1a4..cbf5ae5 100644 --- a/client/src/types/UserResponse.ts +++ b/client/src/types/UserResponse.ts @@ -5,4 +5,5 @@ export type UserResponse = { admin: boolean; createdAt: Date; gravatarImageUrl: string; + lang: string; }; diff --git a/client/src/views/About/index.tsx b/client/src/views/About/index.tsx index 9a69652..6986f1e 100644 --- a/client/src/views/About/index.tsx +++ b/client/src/views/About/index.tsx @@ -17,11 +17,11 @@ function About(): React.ReactNode { return ( @@ -67,20 +67,19 @@ function About(): React.ReactNode {

{t('about_dev_title')}

{t('about_dev_description')} - - ricardompcampos@gmail.com + + gravatar.com/ricardormcampos {t('about_dev_description_two')}

- You can also - {' '} + {t('about_buy_coffee_one')} - Buy me a coffee + {t('about_buy_coffee_link')}

diff --git a/client/src/views/Account/index.tsx b/client/src/views/Account/index.tsx index d405a5d..06b4b11 100644 --- a/client/src/views/Account/index.tsx +++ b/client/src/views/Account/index.tsx @@ -40,6 +40,7 @@ function Account(): React.ReactNode { const handleLanguage = (lang: string): void => { i18n.changeLanguage(lang); setDefaultLang(lang); + patchLanguage(lang); }; /** @@ -92,31 +93,48 @@ function Account(): React.ReactNode { setValidated(false); }; - const handleSubmit = async (event: React.FormEvent): Promise => { - event.preventDefault(); - event.stopPropagation(); + const patchLanguage = async (lang: string): Promise => { + const patchPayload: UserPatchRequest = { + name: null, + email: '', + password: '', + passwordAgain: '', + lang: lang + }; + + await patchUserInfo(patchPayload); + }; + + const handleSubmit = async (event?: React.FormEvent): Promise => { + event?.preventDefault(); + event?.stopPropagation(); setValidated(true); setErrorMessage(''); - const form = event.currentTarget; + const form = event?.currentTarget; const patchPayload: UserPatchRequest = { name: userName ? DOMPurify.sanitize(userName) : null, email: userEmail, password: userPassword, - passwordAgain: userPasswordAgain + passwordAgain: userPasswordAgain, + lang: '' }; const sizeOfPayload = JSON.stringify(patchPayload).length; - if (sizeOfPayload === 57) { - setErrorMessage('Nothing to update!'); + if (sizeOfPayload === 67) { + setErrorMessage(translateServerResponse('Nothing to update!', i18n.language)); return; } const updated: UserResponse | undefined = await patchUserInfo(patchPayload); if (updated) { - form.reset(); + form?.reset(); resetInputs(updated); + if (userEmail !== '') { + signOut(); + clearStorage(); + } } }; @@ -125,11 +143,11 @@ function Account(): React.ReactNode { return ( @@ -137,7 +155,7 @@ function Account(): React.ReactNode { - Update only what you need. Blank fields will not be updated + {t('account_data_update_header')} {/* User name */} ) => { setUserName(e.target.value); @@ -161,7 +179,7 @@ function Account(): React.ReactNode { {/* User email */} ) => { setUserPassword(e.target.value); }} @@ -188,12 +210,16 @@ function Account(): React.ReactNode { {/* User password again */} ) => { setUserPasswordAgain(e.target.value); }} @@ -204,19 +230,15 @@ function Account(): React.ReactNode { type="submit" className="home-new-item task-note-btn" > - Save + {t('account_form_save')}

- If you want to display your picture, we support Gravatar. - Please head to - {' '} + {t('account_form_gravatar_one')} Gravatar - {' '} - to register or update your profile picture. Once updated, please wait a few - minutes to see it here. + {t('account_form_gravatar_two')}

@@ -224,9 +246,9 @@ function Account(): React.ReactNode { - Change the app language + {t('account_app_lang_title')} {t('account_app_lang_description')} -
Available languages
+
{t('account_app_lang_available')}
{languages.map((lang: LangAvailable) => (