From 5c76ed42172c8afcec31df89dc5bcbebf1274ff6 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 30 Aug 2023 08:58:27 +0700 Subject: [PATCH 1/2] Add register view and user service --- javascript-practice/src/js/helpers/helpers.js | 7 +- .../src/js/services/firebaseService.js | 2 +- .../src/js/services/service.js | 7 + .../src/js/services/userService.js | 30 +++++ javascript-practice/src/js/view/commonView.js | 89 +++++++++++++ .../src/js/view/registerView.js | 122 ++++++++++++++++++ javascript-practice/src/js/view/view.js | 8 +- 7 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 javascript-practice/src/js/services/service.js create mode 100644 javascript-practice/src/js/services/userService.js create mode 100644 javascript-practice/src/js/view/commonView.js create mode 100644 javascript-practice/src/js/view/registerView.js diff --git a/javascript-practice/src/js/helpers/helpers.js b/javascript-practice/src/js/helpers/helpers.js index ab8e3a7..23c7db4 100644 --- a/javascript-practice/src/js/helpers/helpers.js +++ b/javascript-practice/src/js/helpers/helpers.js @@ -1,8 +1,5 @@ -import { - TIME_OUT_SEC, - TIME_OUT_ERROR, - REGEX_PASSWORD, -} from '../constants/constant'; +import { TIME_OUT_ERROR } from '../constants/constant'; +import { REGEX_PASSWORD, TIME_OUT_SEC } from '../constants/config'; import FirebaseService from '../services/firebaseService'; export const validatePassword = (password) => { diff --git a/javascript-practice/src/js/services/firebaseService.js b/javascript-practice/src/js/services/firebaseService.js index 8e8ebef..d56af31 100644 --- a/javascript-practice/src/js/services/firebaseService.js +++ b/javascript-practice/src/js/services/firebaseService.js @@ -12,7 +12,7 @@ import { DATABASE_URL } from '../constants/config'; class FirebaseService { constructor() { const firebaseConfig = { - DATABASE_URL, + databaseUrl: DATABASE_URL, }; this.app = initializeApp(firebaseConfig); this.db = getDatabase(this.app); diff --git a/javascript-practice/src/js/services/service.js b/javascript-practice/src/js/services/service.js new file mode 100644 index 0000000..dd10508 --- /dev/null +++ b/javascript-practice/src/js/services/service.js @@ -0,0 +1,7 @@ +import UserService from './userService'; + +export default class Service { + constructor() { + this.userService = new UserService(); + } +} diff --git a/javascript-practice/src/js/services/userService.js b/javascript-practice/src/js/services/userService.js new file mode 100644 index 0000000..98eceee --- /dev/null +++ b/javascript-practice/src/js/services/userService.js @@ -0,0 +1,30 @@ +import { timeOutConnect } from '../helpers/helpers'; +import FirebaseService from './firebaseService'; +import User from '../models/userModel'; + +export default class UserService { + constructor() { + this.path = 'users/'; + } + + async saveUser(user) { + FirebaseService.reconnect(); + const saveUser = FirebaseService.save( + user, + this.path + User.createIdUser(), + ); + await timeOutConnect(saveUser); + } + + async checkExistUserByEmail(email) { + FirebaseService.reconnect(); + const existUser = FirebaseService.findKeyByPropery( + this.path, + 'email', + email, + ); + const result = await timeOutConnect(existUser); + + return result; + } +} diff --git a/javascript-practice/src/js/view/commonView.js b/javascript-practice/src/js/view/commonView.js new file mode 100644 index 0000000..968ef5f --- /dev/null +++ b/javascript-practice/src/js/view/commonView.js @@ -0,0 +1,89 @@ +import { MARK_ICON, TYPE_FORM } from '../constants/constant'; + +export default class CommonView { + constructor() { + this.overlayMarkup = '
'; + + this.initPopup(); + this.initElementPopup(); + this.initLoader(); + this.handleEventBtnPopupAndOverlay(); + } + + initPopup() { + this.rootElement = document.querySelector('body'); + + const markup = ` + ${this.overlayMarkup} + + `; + + this.rootElement.insertAdjacentHTML('afterbegin', markup); + } + + initElementPopup() { + // Init element + this.modalBox = document.querySelector('.modal-box'); + this.popupIcon = document.querySelector('.mark'); + this.popupBtn = document.querySelector('.modal-box__redirect-btn'); + this.popupTitle = document.querySelector('.modal-box__title'); + this.popupContent = document.querySelector('.modal-box__message'); + this.overlay = document.querySelector('.overlay'); + } + + toogleLoaderSpinner() { + this.spinner.classList.toggle('hidden'); + } + + tooglePopupForm() { + this.overlay.classList.toggle('active'); + this.modalBox.classList.toggle('active'); + } + + initPopupContent(typeForm, title, content, btnContent) { + // Remove old typeForm class if haved + this.modalBox.classList.forEach((classItem) => + classItem === TYPE_FORM.success || classItem === TYPE_FORM.error + ? this.modalBox.classList.remove(classItem) + : '', + ); + + // Remove old icon popup if haved + this.popupIcon.classList.forEach((classItem) => + classItem === MARK_ICON.success || classItem === MARK_ICON.error + ? this.popupIcon.classList.remove(classItem) + : '', + ); + + // Init content popup + this.modalBox.classList.add( + typeForm === TYPE_FORM.success ? TYPE_FORM.success : TYPE_FORM.error, + ); + this.popupIcon.classList.add( + typeForm === TYPE_FORM.success ? MARK_ICON.success : MARK_ICON.error, + ); + this.popupTitle.textContent = title; + this.popupContent.textContent = content; + this.popupBtn.textContent = btnContent; + } + + handleEventBtnPopupAndOverlay() { + this.popupBtn.addEventListener('click', this.tooglePopupForm.bind(this)); + this.overlay.addEventListener('click', this.tooglePopupForm.bind(this)); + } + + initLoader() { + const markup = ``; + + this.rootElement.insertAdjacentHTML('afterbegin', markup); + + // Init element + this.spinner = document.querySelector('.loader'); + } +} diff --git a/javascript-practice/src/js/view/registerView.js b/javascript-practice/src/js/view/registerView.js new file mode 100644 index 0000000..c96c028 --- /dev/null +++ b/javascript-practice/src/js/view/registerView.js @@ -0,0 +1,122 @@ +import { validatePassword } from '../helpers/helpers'; +import * as CONSTANT from '../constants/constant'; +import CommonView from './commonView'; +import User from '../models/userModel'; + +export default class RegisterView extends CommonView { + constructor() { + super(); + + this.parentElement = document.querySelector('.form'); + this.inputField = document.querySelectorAll('.form__input'); + + this.messageDefault = CONSTANT.MESSAGE.ERROR_MESSAGE_DEFAULT; + } + + // Function get data from form + getDataFromForm() { + const { registerForm } = document.forms; + const formData = new FormData(registerForm); + const email = formData.get('email'); + const password = formData.get('password'); + const passwordConfirm = formData.get('password_confirm'); + + const account = { email, password, passwordConfirm }; + + if (this.validateForm(account)) { + const user = new User(account); + return user; + } + return null; + } + + validateForm(account) { + if (account.password === account.passwordConfirm) { + if (validatePassword(account.passwordConfirm)) { + return true; + } + this.showError(CONSTANT.MESSAGE.PASSWORD_NOT_STRONG); + return false; + } + this.showError(CONSTANT.MESSAGE.PASSWORD_NOT_MATCH); + return false; + } + + // Handler event submit form + addHandlerForm(handler) { + this.parentElement.addEventListener('submit', (e) => { + e.preventDefault(); + this.clearErrorMessage(); + handler(); + }); + } + + changeToStyleErrorInputPassword() { + for (let index = 1; index < this.inputField.length; index += 1) { + this.inputField[index].style.background = '#ffc3c3'; + this.inputField[index].style.borderColor = '#ce1414'; + } + } + + clearStyleErrorInputPassword() { + for (let index = 1; index < this.inputField.length; index += 1) { + this.inputField[index].style.background = '#f5f5f5'; + this.inputField[index].style.borderColor = '#f5f5f5'; + } + } + + // Reassign again to check error message element haved on page or not + reassignVariableErrorMessage() { + this.errorMessage = document.querySelector('.form__error-message'); + } + + clearErrorMessage() { + this.reassignVariableErrorMessage(); + + // If have error message on page, remove it + if (this.errorMessage) { + this.errorMessage.remove(); + this.clearStyleErrorInputPassword(); + } + } + + addHandlerInputFormChange() { + this.parentElement.addEventListener('input', () => { + this.clearErrorMessage(); + }); + } + + showError(message) { + this.renderError(message); + this.changeToStyleErrorInputPassword(); + } + + initRegisterSuccessPopup() { + const typeForm = CONSTANT.TYPE_FORM.success; + const title = 'Register Commpleted'; + const content = 'Please login to continue!'; + const btnContent = 'OK'; + + this.initPopupContent(typeForm, title, content, btnContent); + } + + initErrorPopup(content) { + const typeForm = CONSTANT.TYPE_FORM.error; + const title = 'Error'; + const btnContent = 'Got it!'; + + this.initPopupContent(typeForm, title, content, btnContent); + } + + renderError(message) { + const markup = ` +

${ + !message ? this.messageDefault : message + }

+ `; + + document + .querySelector('.form__title') + .insertAdjacentHTML('afterend', markup); + } +} diff --git a/javascript-practice/src/js/view/view.js b/javascript-practice/src/js/view/view.js index 70b786d..a2a1675 100644 --- a/javascript-practice/src/js/view/view.js +++ b/javascript-practice/src/js/view/view.js @@ -1 +1,7 @@ -// TODO +import RegisterView from './registerView'; + +export default class View { + constructor() { + this.registerView = new RegisterView(); + } +} From f7de0d435198e93d6e13dfb4a6466cf82eea3c23 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 30 Aug 2023 13:52:36 +0700 Subject: [PATCH 2/2] Add comments and rename folder view to views --- .../src/js/constants/constant.js | 7 +-- javascript-practice/src/js/controller.js | 1 - javascript-practice/src/js/helpers/helpers.js | 27 ++++++--- .../src/js/services/firebaseService.js | 21 ++++++- .../src/js/services/userService.js | 9 +++ .../src/js/{view => views}/commonView.js | 38 ++++++++++--- .../src/js/{view => views}/registerView.js | 55 ++++++++++++++++--- .../src/js/{view => views}/view.js | 0 8 files changed, 129 insertions(+), 29 deletions(-) rename javascript-practice/src/js/{view => views}/commonView.js (71%) rename javascript-practice/src/js/{view => views}/registerView.js (67%) rename javascript-practice/src/js/{view => views}/view.js (100%) diff --git a/javascript-practice/src/js/constants/constant.js b/javascript-practice/src/js/constants/constant.js index 08aa875..397a4ef 100644 --- a/javascript-practice/src/js/constants/constant.js +++ b/javascript-practice/src/js/constants/constant.js @@ -3,10 +3,7 @@ export const MESSAGE = { PASSWORD_NOT_STRONG: 'Password must at least one uppercase, one lowercase letter, one number and one special character!', ERROR_MESSAGE_DEFAULT: 'Something went wrong!', + TIME_OUT_ERROR: 'Connection time out! Please try again!', }; -export const TIME_OUT_ERROR = { - status: 408, - message: 'Connection time out! Please try again!', -}; -export const TYPE_FORM = { success: 'success', error: 'error' }; +export const TYPE_POPUP = { success: 'success', error: 'error' }; export const MARK_ICON = { success: 'check', error: 'error' }; diff --git a/javascript-practice/src/js/controller.js b/javascript-practice/src/js/controller.js index a420803..e69de29 100644 --- a/javascript-practice/src/js/controller.js +++ b/javascript-practice/src/js/controller.js @@ -1 +0,0 @@ -console.log('Hello World!'); diff --git a/javascript-practice/src/js/helpers/helpers.js b/javascript-practice/src/js/helpers/helpers.js index 23c7db4..c6adaf1 100644 --- a/javascript-practice/src/js/helpers/helpers.js +++ b/javascript-practice/src/js/helpers/helpers.js @@ -1,26 +1,37 @@ -import { TIME_OUT_ERROR } from '../constants/constant'; +import { MESSAGE } from '../constants/constant'; import { REGEX_PASSWORD, TIME_OUT_SEC } from '../constants/config'; import FirebaseService from '../services/firebaseService'; +/** + * Validate password + * @param {string} password Password input + * @returns {boolean} Return true if validate password success, otherwise return false + */ export const validatePassword = (password) => { return REGEX_PASSWORD.test(password); }; +/** + * A waiting function with s second + * @param {number} s The time will be waiting + * @returns {Promise} A Promise will be only reject after s second + */ export const timeout = (s) => { - return new Promise((reject) => { + return new Promise((_, reject) => { setTimeout(() => { - reject(TIME_OUT_ERROR); + FirebaseService.disconnect(); + reject(MESSAGE.TIME_OUT_ERROR); }, s * 1000); }); }; +/** + * A function waiting the action need to be perform and throw error after s second. + * @param {Function} action The action need to be perform. + * @returns { Object || Error } Return the any object from Firebase or Error + */ export const timeOutConnect = async (action) => { const result = await Promise.race([action, timeout(TIME_OUT_SEC)]); - if (result && result.status === TIME_OUT_ERROR.status) { - FirebaseService.disconnect(); - throw new Error(`${result.message}`); - } - return result; }; diff --git a/javascript-practice/src/js/services/firebaseService.js b/javascript-practice/src/js/services/firebaseService.js index d56af31..700e5ad 100644 --- a/javascript-practice/src/js/services/firebaseService.js +++ b/javascript-practice/src/js/services/firebaseService.js @@ -12,24 +12,43 @@ import { DATABASE_URL } from '../constants/config'; class FirebaseService { constructor() { const firebaseConfig = { - databaseUrl: DATABASE_URL, + databaseURL: DATABASE_URL, }; this.app = initializeApp(firebaseConfig); this.db = getDatabase(this.app); } + /** + * Save data in database + * @param {Object} data The object need to save into database + * @param {string} path The path of database need to be save + * @returns {Promise} Return the relsoves when write to database completed + */ save(data, path) { return set(ref(this.db, path), data); } + /** + * Disconnect to database + */ disconnect() { goOffline(this.db); } + /** + * Reconnect to database + */ reconnect() { goOnline(this.db); } + /** + * Find the key of value by property in database + * @param {string} path The path of database to be found + * @param {string} property The property of the value need to be found + * @param {value} value The value to compare in database + * @returns {Promise} Return the relsoves when find completed + */ findKeyByPropery(path, property, value) { return new Promise((resolve) => { onValue( diff --git a/javascript-practice/src/js/services/userService.js b/javascript-practice/src/js/services/userService.js index 98eceee..65341ba 100644 --- a/javascript-practice/src/js/services/userService.js +++ b/javascript-practice/src/js/services/userService.js @@ -7,6 +7,10 @@ export default class UserService { this.path = 'users/'; } + /** + * Save user into database + * @param {Object} user The user object need to be saved into databae + */ async saveUser(user) { FirebaseService.reconnect(); const saveUser = FirebaseService.save( @@ -16,6 +20,11 @@ export default class UserService { await timeOutConnect(saveUser); } + /** + * Check user exist in database by email + * @param {string} email Email need to be check + * @returns {boolean} Return true if email exist and otherwise is false + */ async checkExistUserByEmail(email) { FirebaseService.reconnect(); const existUser = FirebaseService.findKeyByPropery( diff --git a/javascript-practice/src/js/view/commonView.js b/javascript-practice/src/js/views/commonView.js similarity index 71% rename from javascript-practice/src/js/view/commonView.js rename to javascript-practice/src/js/views/commonView.js index 968ef5f..083f01f 100644 --- a/javascript-practice/src/js/view/commonView.js +++ b/javascript-practice/src/js/views/commonView.js @@ -1,4 +1,4 @@ -import { MARK_ICON, TYPE_FORM } from '../constants/constant'; +import { MARK_ICON, TYPE_POPUP } from '../constants/constant'; export default class CommonView { constructor() { @@ -10,6 +10,9 @@ export default class CommonView { this.handleEventBtnPopupAndOverlay(); } + /** + * Implement popup in site + */ initPopup() { this.rootElement = document.querySelector('body'); @@ -27,8 +30,10 @@ export default class CommonView { this.rootElement.insertAdjacentHTML('afterbegin', markup); } + /** + * Assgign element in popup to property + */ initElementPopup() { - // Init element this.modalBox = document.querySelector('.modal-box'); this.popupIcon = document.querySelector('.mark'); this.popupBtn = document.querySelector('.modal-box__redirect-btn'); @@ -37,19 +42,32 @@ export default class CommonView { this.overlay = document.querySelector('.overlay'); } + /** + * Show or hide loader screen + */ toogleLoaderSpinner() { this.spinner.classList.toggle('hidden'); } + /** + * Show or hide popup + */ tooglePopupForm() { this.overlay.classList.toggle('active'); this.modalBox.classList.toggle('active'); } - initPopupContent(typeForm, title, content, btnContent) { - // Remove old typeForm class if haved + /** + * Add popup content + * @param {TYPE_POPUP} typePopup Type of the popup + * @param {string} title Title of popup + * @param {string} content Content of popup + * @param {string} btnContent Content of button + */ + initPopupContent(typePopup, title, content, btnContent) { + // Remove old typePopup class if haved this.modalBox.classList.forEach((classItem) => - classItem === TYPE_FORM.success || classItem === TYPE_FORM.error + classItem === TYPE_POPUP.success || classItem === TYPE_POPUP.error ? this.modalBox.classList.remove(classItem) : '', ); @@ -63,21 +81,27 @@ export default class CommonView { // Init content popup this.modalBox.classList.add( - typeForm === TYPE_FORM.success ? TYPE_FORM.success : TYPE_FORM.error, + typePopup === TYPE_POPUP.success ? TYPE_POPUP.success : TYPE_POPUP.error, ); this.popupIcon.classList.add( - typeForm === TYPE_FORM.success ? MARK_ICON.success : MARK_ICON.error, + typePopup === TYPE_POPUP.success ? MARK_ICON.success : MARK_ICON.error, ); this.popupTitle.textContent = title; this.popupContent.textContent = content; this.popupBtn.textContent = btnContent; } + /** + * Add event listener for popup and overlay + */ handleEventBtnPopupAndOverlay() { this.popupBtn.addEventListener('click', this.tooglePopupForm.bind(this)); this.overlay.addEventListener('click', this.tooglePopupForm.bind(this)); } + /** + * Implement loader screen + */ initLoader() { const markup = ``; diff --git a/javascript-practice/src/js/view/registerView.js b/javascript-practice/src/js/views/registerView.js similarity index 67% rename from javascript-practice/src/js/view/registerView.js rename to javascript-practice/src/js/views/registerView.js index c96c028..aa0576e 100644 --- a/javascript-practice/src/js/view/registerView.js +++ b/javascript-practice/src/js/views/registerView.js @@ -13,7 +13,10 @@ export default class RegisterView extends CommonView { this.messageDefault = CONSTANT.MESSAGE.ERROR_MESSAGE_DEFAULT; } - // Function get data from form + /** + * Get data from user input + * @returns {Object || null} Return user object or null + */ getDataFromForm() { const { registerForm } = document.forms; const formData = new FormData(registerForm); @@ -30,6 +33,11 @@ export default class RegisterView extends CommonView { return null; } + /** + * Validate user input data + * @param {Object} account The account object with email, password, passwordConfirm field + * @returns {boolean} Return true if validate success and return false if validate not success + */ validateForm(account) { if (account.password === account.passwordConfirm) { if (validatePassword(account.passwordConfirm)) { @@ -42,7 +50,10 @@ export default class RegisterView extends CommonView { return false; } - // Handler event submit form + /** + * Add event listener for form input + * @param {Function} handler The function need to be set event + */ addHandlerForm(handler) { this.parentElement.addEventListener('submit', (e) => { e.preventDefault(); @@ -51,13 +62,20 @@ export default class RegisterView extends CommonView { }); } + /** + * Show error style input password + */ changeToStyleErrorInputPassword() { + // Index = 1 because it should skip email input field for (let index = 1; index < this.inputField.length; index += 1) { this.inputField[index].style.background = '#ffc3c3'; this.inputField[index].style.borderColor = '#ce1414'; } } + /** + * Clear error style input password + */ clearStyleErrorInputPassword() { for (let index = 1; index < this.inputField.length; index += 1) { this.inputField[index].style.background = '#f5f5f5'; @@ -65,11 +83,16 @@ export default class RegisterView extends CommonView { } } - // Reassign again to check error message element haved on page or not + /** + * Reassign again to check error message element haved on page or not + */ reassignVariableErrorMessage() { this.errorMessage = document.querySelector('.form__error-message'); } + /** + * Clear error message at form + */ clearErrorMessage() { this.reassignVariableErrorMessage(); @@ -80,34 +103,52 @@ export default class RegisterView extends CommonView { } } + /** + * Add event listener for input field at form + */ addHandlerInputFormChange() { this.parentElement.addEventListener('input', () => { this.clearErrorMessage(); }); } + /** + * Show error message with error style input password. + * @param {string} message The error message you want show in form. + */ showError(message) { this.renderError(message); this.changeToStyleErrorInputPassword(); } + /** + * Implement register success popup in site + */ initRegisterSuccessPopup() { - const typeForm = CONSTANT.TYPE_FORM.success; + const typePopup = CONSTANT.TYPE_POPUP.success; const title = 'Register Commpleted'; const content = 'Please login to continue!'; const btnContent = 'OK'; - this.initPopupContent(typeForm, title, content, btnContent); + this.initPopupContent(typePopup, title, content, btnContent); } + /** + * Implement error popup in site + * @param {string} content The content will show in error popup + */ initErrorPopup(content) { - const typeForm = CONSTANT.TYPE_FORM.error; + const typePopup = CONSTANT.TYPE_POPUP.error; const title = 'Error'; const btnContent = 'Got it!'; - this.initPopupContent(typeForm, title, content, btnContent); + this.initPopupContent(typePopup, title, content, btnContent); } + /** + * Show error message in form + * @param {*} message The message will show in form + */ renderError(message) { const markup = `

${ diff --git a/javascript-practice/src/js/view/view.js b/javascript-practice/src/js/views/view.js similarity index 100% rename from javascript-practice/src/js/view/view.js rename to javascript-practice/src/js/views/view.js