From cb12f1f4a69c999f0ac767bbb727c01233ac16c9 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Thu, 18 Jan 2024 15:51:51 -0600 Subject: [PATCH 01/21] Update profile code implemetation --- scripts/apis/user.js | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/scripts/apis/user.js b/scripts/apis/user.js index f67854ef..72ff1887 100644 --- a/scripts/apis/user.js +++ b/scripts/apis/user.js @@ -47,6 +47,56 @@ async function fetchUserProfile(username) { return json; } +/** + * Attempt to update the user profile. If successful, also update session copy. + * Caller must look at response to see if it was successful, etc. + * @param {Object} Updated user profile + * @returns response object with status, null if user not logged in + */ +export async function updateProfile(profile) { + const userDetails = getUserDetails(); + if (userDetails === null) { + return null; + } + const existingProfile = userDetails.profile; + + // Update profile in backend, post object as name/value pairs + const time = new Date().getTime(); + const url = `${API_URL}/cregUserProfile?Email=${encodeURIComponent(userDetails.username)}&_=${time}`; + const postBody = { + FirstName: profile.firstName, + LastName: profile.lastName, + MobilePhone: profile.mobilePhone, + HomePhone: profile.homePhone, + Email: profile.email, + EmailNotifications: profile.emailNotifications || existingProfile.emailNotifications || false, + ContactKey: existingProfile.contactKey, + signInScheme: profile.signInScheme || existingProfile.signInScheme || 'default', + HomeAddress1: profile.homeAddress1, + HomeAddress2: profile.homeAddress2, + HomeCity: profile.homeCity, + HomeStateOrProvince: profile.homeStateOrProvince, + HomePostalCode: profile.homePostalCode, + Language: profile.language, + Currency: profile.currency, + UnitOfMeasure: profile.measure, + }; + const response = fetch(url, { + method: 'PUT', + credentials: 'include', + mode: 'cors', + body: new URLSearchParams(postBody).toString(), + }); + + if (response.ok) { + // Update profile in session storage using a merge + userDetails.profile = { ...existingProfile, ...profile }; + sessionStorage.setItem('userDetails', JSON.stringify(userDetails)); + } + + return response; +} + /** * Logs the user out silently. */ From 92b3d8f0340eee82fa4d6c422923f979097075ca Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Thu, 18 Jan 2024 17:26:25 -0600 Subject: [PATCH 02/21] Start of profile UI implementation and saving routine --- blocks/profile/profile.css | 39 +++++++ blocks/profile/profile.js | 201 +++++++++++++++++++++++++++++++++++++ scripts/apis/user.js | 21 ++++ 3 files changed, 261 insertions(+) create mode 100644 blocks/profile/profile.css create mode 100644 blocks/profile/profile.js diff --git a/blocks/profile/profile.css b/blocks/profile/profile.css new file mode 100644 index 00000000..888f1be6 --- /dev/null +++ b/blocks/profile/profile.css @@ -0,0 +1,39 @@ +/* Styles for tabs and form fields used in profile page */ +.profile .tabs { + margin-bottom: 1em; +} + +.profile .tabs ul { + margin: 0; + padding: 0; + list-style-type: none; +} + +.profile .tabs li { + display: inline; + margin: 0; + padding: 0; +} + +.profile .tabs li a { + padding: 0.2em 0.5em; + margin-right: 0.2em; + border: 1px solid #ddd; + border-bottom: none; + background-color: #eee; + text-decoration: none; +} + +.profile .tabs li a:hover { + background-color: #ddd; +} + +.profile .tabs li.selected a { + background-color: white; + border-bottom: 1px solid purple; + font-weight: bold; +} + +.profile .tabs li.selected a:hover { + background-color: white; +} \ No newline at end of file diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js new file mode 100644 index 00000000..7491fa31 --- /dev/null +++ b/blocks/profile/profile.js @@ -0,0 +1,201 @@ +import { getUserDetails, requestPasswordReset, updateProfile, isLoggedIn } from '../../scripts/apis/user.js'; + +function prepareTabs(block) { + const tabs = block.querySelector('tabs'); + const tabNames = tabs.querySelectorAll('tab-name'); + const tabContents = tabs.querySelectorAll('content'); + const tabButtons = []; + const tabContentsArray = []; + + tabNames.forEach((tabName) => { + tabButtons.push(tabName); + }); + + tabContents.forEach((tabContent) => { + tabContentsArray.push(tabContent); + }); + + tabButtons.forEach((tabButton) => { + tabButton.addEventListener('click', () => { + const tabName = tabButton.innerHTML; + tabButtons.forEach((button) => { + button.classList.remove('active'); + }); + tabButton.classList.add('active'); + tabContentsArray.forEach((tabContent) => { + if (tabContent.innerHTML.includes(tabName)) { + tabContent.classList.add('active'); + } else { + tabContent.classList.remove('active'); + } + }); + }); + }); + + tabButtons[0].click(); +} + +function populateDropdowns(block) { + const countryDropdown = block.querySelector('select[name="country"]'); + const languageDropdown = block.querySelector('select[name="language"]'); + const currencyDropdown = block.querySelector('select[name="currency"]'); + const measureDropdown = block.querySelector('select[name="measure"]'); + const countries = [ + { name: 'United States', value: 'US' }, + { name: 'Canada', value: 'CA' }, + ]; + const languages = [ + { name: 'English', value: 'en' }, + { name: 'French', value: 'fr' }, + ]; + const currencies = [ + { name: 'US Dollar', value: 'USD' }, + { name: 'Canadian Dollar', value: 'CAD' }, + ]; + const measures = [ + { name: 'Imperial', value: 'imperial' }, + { name: 'Metric', value: 'metric' }, + ]; + + countries.forEach((country) => { + const option = document.createElement('option'); + option.value = country.value; + option.innerHTML = country.name; + countryDropdown.append(option); + }); + + languages.forEach((language) => { + const option = document.createElement('option'); + option.value = language.value; + option.innerHTML = language.name; + languageDropdown.append(option); + }); + + currencies.forEach((currency) => { + const option = document.createElement('option'); + + option.value = currency.value; + option.innerHTML = currency.name; + currencyDropdown.append(option); + }); + + measures.forEach((measure) => { + const option = document.createElement('option'); + option.value = measure.value; + option.innerHTML = measure.name; + measureDropdown.append(option); + }); +} + +function setupPasswordReset(block) { + const resetPassword = block.querySelector('.reset-password'); + resetPassword.addEventListener('click', () => { + requestPasswordReset(); + } +} + +function populateForm(block) { + const profile = getUserDetails().profile; + + if (profile) { + const firstName = block.querySelector('input[name="firstName"]'); + const lastName = block.querySelector('input[name="lastName"]'); + const email = block.querySelector('input[name="email"]'); + const mobilePhone = block.querySelector('input[name="mobilePhone"]'); + const homePhone = block.querySelector('input[name="homePhone"]'); + const country = block.querySelector('select[name="country"]'); + const address1 = block.querySelector('input[name="address1"]'); + const address2 = block.querySelector('input[name="address2"]'); + const city = block.querySelector('input[name="city"]'); + const stateOrProvince = block.querySelector('input[name="stateOrProvince"]'); + const postalCode = block.querySelector('input[name="postalCode"]'); + const language = block.querySelector('select[name="language"]'); + const currency = block.querySelector('select[name="currency"]'); + const measure = block.querySelector('select[name="measure"]'); + + firstName.value = profile.firstName; + lastName.value = profile.lastName; + email.value = profile.email; + mobilePhone.value = profile.mobilePhone; + homePhone.value = profile.homePhone; + country.value = profile.country; + address1.value = profile.address1; + + if (profile.address2) { + address2.value = profile.address2; + } + + city.value = profile.city; + stateOrProvince.value = profile.stateOrProvince; + postalCode.value = profile.postalCode; + language.value = profile.language; + currency.value = profile.currency; + measure.value = profile.measure; + } +} + +export default async function decorate(block) { + block.innerHTML = ` + + Contact Info + + + + +

Please manage your email preferences by using "Unsubscribe" option at the bottom of emails you receive.

+ + +

+ By providing your telephone number, you are giving permission to Berkshire Hathaway HomeServices and a franchisee + member of the Berkshire Hathaway HomeServices real estate network to communicate with you by phone or text, + including automated means, even if your telephone number appears on any "Do Not Call" list. A phone number is not + required in order to receive real estate brokerage services. Message/data rates may apply. For more about how we will + use your contact information, please review our Terms of Use + and Privacy Policy. +

+
+
+ Change Password + +

Click reset, and we will send you a email containing a reset password link.

+ +
+
+ Address + + + + + + + + + + Regional Preferences + +

Set the language for emails and this site.

+

Language

+ +

Set the language and unit of measurement for this site. +

Currency

+ +

Unit of Measurement

+ +
+
+
+ `; + + prepareTabs(block); + setupPasswordReset(block); + populateDropdowns(block); + populateForm(block); +} diff --git a/scripts/apis/user.js b/scripts/apis/user.js index 72ff1887..588df47e 100644 --- a/scripts/apis/user.js +++ b/scripts/apis/user.js @@ -97,6 +97,27 @@ export async function updateProfile(profile) { return response; } +/** + * Request a password reset email. + * @returns response object with status, null if user not logged in + */ +export async function requestPasswordReset() { + const userDetails = getUserDetails(); + if (userDetails === null) { + return null; + } + + const time = new Date().getTime(); + const url = `${API_URL}/cregPasswordReset?Email=${encodeURIComponent(userDetails.username)}&_=${time}`; + const response = fetch(url, { + method: 'POST', + credentials: 'include', + mode: 'cors', + }); + + return response; +} + /** * Logs the user out silently. */ From a226af37d71635d9a70f6f024faad081cfdff6bf Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Fri, 19 Jan 2024 11:51:54 -0600 Subject: [PATCH 03/21] Tabs are working, needs more form styling and validation though --- blocks/profile/profile.css | 45 +++++---------- blocks/profile/profile.js | 112 +++++++++++++++++++------------------ 2 files changed, 71 insertions(+), 86 deletions(-) diff --git a/blocks/profile/profile.css b/blocks/profile/profile.css index 888f1be6..63446af1 100644 --- a/blocks/profile/profile.css +++ b/blocks/profile/profile.css @@ -1,39 +1,22 @@ /* Styles for tabs and form fields used in profile page */ -.profile .tabs { - margin-bottom: 1em; +.profile nav button { + border: none; + line-height: var(--line-height-m); + font-size: var(--body-font-size-m); + margin: 2em 2em 2em 0; + background-color: unset; + text-transform: uppercase; } -.profile .tabs ul { - margin: 0; - padding: 0; - list-style-type: none; -} - -.profile .tabs li { - display: inline; - margin: 0; - padding: 0; -} - -.profile .tabs li a { - padding: 0.2em 0.5em; - margin-right: 0.2em; - border: 1px solid #ddd; - border-bottom: none; - background-color: #eee; - text-decoration: none; -} - -.profile .tabs li a:hover { - background-color: #ddd; +.profile nav button.active { + font-weight: bold; + border-bottom: 3px solid var(--primary-color); } -.profile .tabs li.selected a { - background-color: white; - border-bottom: 1px solid purple; - font-weight: bold; +.profile tab, .profile tab tab-name { + display: none; } -.profile .tabs li.selected a:hover { - background-color: white; +.profile tab.active { + display: block; } \ No newline at end of file diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index 7491fa31..e73a8058 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -1,37 +1,37 @@ import { getUserDetails, requestPasswordReset, updateProfile, isLoggedIn } from '../../scripts/apis/user.js'; +let form = {}; + +function asHtml(string) { + const div = document.createElement('div'); + div.innerHTML = string; + return div.firstChild; +} + function prepareTabs(block) { - const tabs = block.querySelector('tabs'); - const tabNames = tabs.querySelectorAll('tab-name'); - const tabContents = tabs.querySelectorAll('content'); + const tabContainer = block.querySelector('tabs'); + const tabs = tabContainer.querySelectorAll('tab'); const tabButtons = []; - const tabContentsArray = []; - tabNames.forEach((tabName) => { - tabButtons.push(tabName); - }); + tabs.forEach((tab) => { + const tabName = tab.querySelector('tab-name'); + const tabContents = tab.querySelector('content'); + tabContents.tabName = tabName.textContent; - tabContents.forEach((tabContent) => { - tabContentsArray.push(tabContent); - }); + const tabButton = asHtml(``); + tabButtons.push(tabButton); - tabButtons.forEach((tabButton) => { - tabButton.addEventListener('click', () => { - const tabName = tabButton.innerHTML; - tabButtons.forEach((button) => { - button.classList.remove('active'); - }); + tabButton.onclick = () => { + tabButtons.forEach((button) => button.classList.remove('active')); tabButton.classList.add('active'); - tabContentsArray.forEach((tabContent) => { - if (tabContent.innerHTML.includes(tabName)) { - tabContent.classList.add('active'); - } else { - tabContent.classList.remove('active'); - } - }); - }); + tabs.forEach((tabContent) => tabContent.classList.remove('active')); + tab.classList.add('active'); + }; }); + const buttonBar = asHtml(''); + tabButtons.forEach((button) => buttonBar.append(button)); + tabContainer.insertBefore(buttonBar, tabContainer.firstChild); tabButtons[0].click(); } @@ -91,46 +91,48 @@ function setupPasswordReset(block) { const resetPassword = block.querySelector('.reset-password'); resetPassword.addEventListener('click', () => { requestPasswordReset(); - } + }); } function populateForm(block) { - const profile = getUserDetails().profile; + const { profile } = getUserDetails(); if (profile) { - const firstName = block.querySelector('input[name="firstName"]'); - const lastName = block.querySelector('input[name="lastName"]'); - const email = block.querySelector('input[name="email"]'); - const mobilePhone = block.querySelector('input[name="mobilePhone"]'); - const homePhone = block.querySelector('input[name="homePhone"]'); - const country = block.querySelector('select[name="country"]'); - const address1 = block.querySelector('input[name="address1"]'); - const address2 = block.querySelector('input[name="address2"]'); - const city = block.querySelector('input[name="city"]'); - const stateOrProvince = block.querySelector('input[name="stateOrProvince"]'); - const postalCode = block.querySelector('input[name="postalCode"]'); - const language = block.querySelector('select[name="language"]'); - const currency = block.querySelector('select[name="currency"]'); - const measure = block.querySelector('select[name="measure"]'); - - firstName.value = profile.firstName; - lastName.value = profile.lastName; - email.value = profile.email; - mobilePhone.value = profile.mobilePhone; - homePhone.value = profile.homePhone; - country.value = profile.country; - address1.value = profile.address1; + form = { + firstName: block.querySelector('input[name="firstName"]'), + lastName: block.querySelector('input[name="lastName"]'), + email: block.querySelector('input[name="email"]'), + mobilePhone: block.querySelector('input[name="mobilePhone"]'), + homePhone: block.querySelector('input[name="homePhone"]'), + country: block.querySelector('select[name="country"]'), + address1: block.querySelector('input[name="address1"]'), + address2: block.querySelector('input[name="address2"]'), + city: block.querySelector('input[name="city"]'), + stateOrProvince: block.querySelector('input[name="stateOrProvince"]'), + postalCode: block.querySelector('input[name="postalCode"]'), + language: block.querySelector('select[name="language"]'), + currency: block.querySelector('select[name="currency"]'), + measure: block.querySelector('select[name="measure"]'), + }; + + form.firstName.value = profile.firstName; + form.lastName.value = profile.lastName; + form.email.value = profile.email; + form.mobilePhone.value = profile.mobilePhone; + form.homePhone.value = profile.homePhone; + form.country.value = profile.country; + form.address1.value = profile.address1; if (profile.address2) { - address2.value = profile.address2; + form.address2.value = profile.address2; } - city.value = profile.city; - stateOrProvince.value = profile.stateOrProvince; - postalCode.value = profile.postalCode; - language.value = profile.language; - currency.value = profile.currency; - measure.value = profile.measure; + form.city.value = profile.city; + form.stateOrProvince.value = profile.stateOrProvince; + form.postalCode.value = profile.postalCode; + form.language.value = profile.language; + form.currency.value = profile.currency; + form.measure.value = profile.measure; } } From 2f95dd49c99e4f6fb3a2363966aceb6f65e36929 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Fri, 19 Jan 2024 13:41:47 -0600 Subject: [PATCH 04/21] Style improvements, also adjusted selectors to make linter happy --- blocks/profile/profile.css | 45 +++++++++++- blocks/profile/profile.js | 145 ++++++++++++++++--------------------- 2 files changed, 107 insertions(+), 83 deletions(-) diff --git a/blocks/profile/profile.css b/blocks/profile/profile.css index 63446af1..d636ab28 100644 --- a/blocks/profile/profile.css +++ b/blocks/profile/profile.css @@ -1,4 +1,14 @@ /* Styles for tabs and form fields used in profile page */ +.profile { + max-width: 45em; +} + +.profile h4 { + line-height: var(--line-height-m); + padding-bottom: 0.5em; + font-family: var(--font-family-georgia); +} + .profile nav button { border: none; line-height: var(--line-height-m); @@ -13,10 +23,41 @@ border-bottom: 3px solid var(--primary-color); } -.profile tab, .profile tab tab-name { +.profile .tab { display: none; } -.profile tab.active { +.profile .tab.active { display: block; +} + +.profile p { + margin-top: 1em; +} + +.profile .help { + font-size: var(--body-font-size-xs); + line-height: var(--line-height-m); + color: var(--dark-grey); + +} + +.profile input, .profile select { + line-height: var(--line-height-xl); + margin-bottom: 1em; + padding-left: 1em; + width: 22em; + margin-right: 0.5em; +} + +.profile select { + height: var(--line-height-xxl); +} + +.profile input[name='lastName'] { + margin-right: 0; +} + +.profile input[name='email'] { + width: calc(100% - 0.25em); } \ No newline at end of file diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index e73a8058..3740f1d7 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -11,28 +11,23 @@ function asHtml(string) { function prepareTabs(block) { const tabContainer = block.querySelector('tabs'); const tabs = tabContainer.querySelectorAll('tab'); - const tabButtons = []; + const buttonBar = asHtml(''); tabs.forEach((tab) => { - const tabName = tab.querySelector('tab-name'); - const tabContents = tab.querySelector('content'); - tabContents.tabName = tabName.textContent; - - const tabButton = asHtml(``); - tabButtons.push(tabButton); + tab.classList.add('tab'); + const tabButton = asHtml(``); + buttonBar.append(tabButton); tabButton.onclick = () => { - tabButtons.forEach((button) => button.classList.remove('active')); - tabButton.classList.add('active'); + buttonBar.childNodes.forEach((button) => button.classList.remove('active')); tabs.forEach((tabContent) => tabContent.classList.remove('active')); + tabButton.classList.add('active'); tab.classList.add('active'); }; }); - const buttonBar = asHtml(''); - tabButtons.forEach((button) => buttonBar.append(button)); tabContainer.insertBefore(buttonBar, tabContainer.firstChild); - tabButtons[0].click(); + buttonBar.childNodes[0].click(); } function populateDropdowns(block) { @@ -95,7 +90,7 @@ function setupPasswordReset(block) { } function populateForm(block) { - const { profile } = getUserDetails(); + const { profile } = getUserDetails() || { profile: {} }; if (profile) { form = { @@ -115,83 +110,71 @@ function populateForm(block) { measure: block.querySelector('select[name="measure"]'), }; - form.firstName.value = profile.firstName; - form.lastName.value = profile.lastName; - form.email.value = profile.email; - form.mobilePhone.value = profile.mobilePhone; - form.homePhone.value = profile.homePhone; - form.country.value = profile.country; - form.address1.value = profile.address1; - - if (profile.address2) { - form.address2.value = profile.address2; - } - - form.city.value = profile.city; - form.stateOrProvince.value = profile.stateOrProvince; - form.postalCode.value = profile.postalCode; - form.language.value = profile.language; - form.currency.value = profile.currency; - form.measure.value = profile.measure; + form.firstName.value = profile.firstName || ''; + form.lastName.value = profile.lastName || ''; + form.email.value = profile.email || ''; + form.mobilePhone.value = profile.mobilePhone || ''; + form.homePhone.value = profile.homePhone || ''; + form.country.value = profile.country || ''; + form.address1.value = profile.address1 || ''; + form.address2.value = profile.address2 || ''; + form.city.value = profile.city || ''; + form.stateOrProvince.value = profile.stateOrProvince || ''; + form.postalCode.value = profile.postalCode || ''; + form.language.value = profile.language || ''; + form.currency.value = profile.currency || ''; + form.measure.value = profile.measure || ''; } } export default async function decorate(block) { block.innerHTML = ` - Contact Info - - - - -

Please manage your email preferences by using "Unsubscribe" option at the bottom of emails you receive.

- - -

- By providing your telephone number, you are giving permission to Berkshire Hathaway HomeServices and a franchisee - member of the Berkshire Hathaway HomeServices real estate network to communicate with you by phone or text, - including automated means, even if your telephone number appears on any "Do Not Call" list. A phone number is not - required in order to receive real estate brokerage services. Message/data rates may apply. For more about how we will - use your contact information, please review our Terms of Use - and Privacy Policy. -

-
+ + + + +

Please manage your email preferences by using "Unsubscribe" option at the bottom of emails you receive.

+ + +

+ By providing your telephone number, you are giving permission to Berkshire Hathaway HomeServices and a franchisee + member of the Berkshire Hathaway HomeServices real estate network to communicate with you by phone or text, + including automated means, even if your telephone number appears on any "Do Not Call" list. A phone number is not + required in order to receive real estate brokerage services. Message/data rates may apply. For more about how we will + use your contact information, please review our Terms of Use + and Privacy Policy. +

- Change Password - -

Click reset, and we will send you a email containing a reset password link.

- -
+ +

Click reset, and we will send you a email containing a reset password link.

+
- Address - - - - - - - - + + + + + + + - Regional Preferences - -

Set the language for emails and this site.

-

Language

- -

Set the language and unit of measurement for this site. -

Currency

- -

Unit of Measurement

- -
+ +

Set the language for emails and this site.

+

Language

+ +

Set the currency and unit of measurement for this site. +

Currency

+ +

Unit of Measurement

+
`; From 0635e4138cfe54485eda07cb8f35f1c348c1f8ee Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Fri, 19 Jan 2024 15:21:41 -0600 Subject: [PATCH 05/21] Dropdown values implemented --- blocks/profile/profile.js | 76 +++++++++++++++------------------------ 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index 3740f1d7..051ea197 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -8,6 +8,15 @@ function asHtml(string) { return div.firstChild; } +let cachedDropdownValues = {}; +async function getDropdownValues() { + if (Object.keys(cachedDropdownValues).length === 0) { + const response = await fetch('/account/dropdown-values.json'); + cachedDropdownValues = await response.json(); + } + return cachedDropdownValues; +} + function prepareTabs(block) { const tabContainer = block.querySelector('tabs'); const tabs = tabContainer.querySelectorAll('tab'); @@ -30,56 +39,27 @@ function prepareTabs(block) { buttonBar.childNodes[0].click(); } -function populateDropdowns(block) { +function populateDropdown(select, data) { + const options = data.map((d) => ``); + select.innerHTML += options.join(''); +} + +async function populateDropdowns(block) { + const dropdownValues = await getDropdownValues(); const countryDropdown = block.querySelector('select[name="country"]'); const languageDropdown = block.querySelector('select[name="language"]'); const currencyDropdown = block.querySelector('select[name="currency"]'); const measureDropdown = block.querySelector('select[name="measure"]'); - const countries = [ - { name: 'United States', value: 'US' }, - { name: 'Canada', value: 'CA' }, - ]; - const languages = [ - { name: 'English', value: 'en' }, - { name: 'French', value: 'fr' }, - ]; - const currencies = [ - { name: 'US Dollar', value: 'USD' }, - { name: 'Canadian Dollar', value: 'CAD' }, - ]; - const measures = [ - { name: 'Imperial', value: 'imperial' }, - { name: 'Metric', value: 'metric' }, - ]; - - countries.forEach((country) => { - const option = document.createElement('option'); - option.value = country.value; - option.innerHTML = country.name; - countryDropdown.append(option); - }); - - languages.forEach((language) => { - const option = document.createElement('option'); - option.value = language.value; - option.innerHTML = language.name; - languageDropdown.append(option); - }); - - currencies.forEach((currency) => { - const option = document.createElement('option'); - - option.value = currency.value; - option.innerHTML = currency.name; - currencyDropdown.append(option); - }); - - measures.forEach((measure) => { - const option = document.createElement('option'); - option.value = measure.value; - option.innerHTML = measure.name; - measureDropdown.append(option); - }); + const { + country, + language, + currency, + measure, + } = dropdownValues; + populateDropdown(countryDropdown, country.data); + populateDropdown(languageDropdown, language.data); + populateDropdown(currencyDropdown, currency.data); + populateDropdown(measureDropdown, measure.data); } function setupPasswordReset(block) { @@ -115,7 +95,7 @@ function populateForm(block) { form.email.value = profile.email || ''; form.mobilePhone.value = profile.mobilePhone || ''; form.homePhone.value = profile.homePhone || ''; - form.country.value = profile.country || ''; + form.country.value = profile.country || 'US'; form.address1.value = profile.address1 || ''; form.address2.value = profile.address2 || ''; form.city.value = profile.city || ''; @@ -179,8 +159,8 @@ export default async function decorate(block) { `; + populateDropdowns(block); prepareTabs(block); setupPasswordReset(block); - populateDropdowns(block); populateForm(block); } From 7d1a59f96b1c85c38fd1146894dd9d0e6ea20f2e Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Fri, 19 Jan 2024 15:59:54 -0600 Subject: [PATCH 06/21] More styling work and visual support for required fields --- blocks/profile/profile.css | 34 ++++++++++++++ blocks/profile/profile.js | 93 ++++++++++++++++---------------------- 2 files changed, 73 insertions(+), 54 deletions(-) diff --git a/blocks/profile/profile.css b/blocks/profile/profile.css index d636ab28..b51d67db 100644 --- a/blocks/profile/profile.css +++ b/blocks/profile/profile.css @@ -14,6 +14,7 @@ line-height: var(--line-height-m); font-size: var(--body-font-size-m); margin: 2em 2em 2em 0; + padding: 0; background-color: unset; text-transform: uppercase; } @@ -35,6 +36,17 @@ margin-top: 1em; } +.profile .tab button { + background-color : var(--primary-color); + color: var(--white); + border: none; + padding: 0.7em 2em; + margin-top: 2em; + text-transform: uppercase; + font-size: var(--body-font-size-s); + display: block; +} + .profile .help { font-size: var(--body-font-size-xs); line-height: var(--line-height-m); @@ -50,6 +62,15 @@ margin-right: 0.5em; } +.profile input:required:placeholder-shown { + border: 2px solid var(--error); + background-color: var(--error-highlight); +} + +.profile input:required::placeholder { + color: var(--error); +} + .profile select { height: var(--line-height-xxl); } @@ -58,6 +79,19 @@ margin-right: 0; } +.profile select[name='country'], .profile input[name='address1'], .profile input[name='address2'], .profile input[name='city'] { + width: 27em; + display: block; +} + +.profile input[name='stateOrProvince'] { + width: 18em; +} + +.profile input[name='postalCode'] { + width: 8.25em; +} + .profile input[name='email'] { width: calc(100% - 0.25em); } \ No newline at end of file diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index 051ea197..840237ee 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -40,26 +40,15 @@ function prepareTabs(block) { } function populateDropdown(select, data) { - const options = data.map((d) => ``); - select.innerHTML += options.join(''); + select.innerHTML += data.map((d) => ``).join(''); } async function populateDropdowns(block) { const dropdownValues = await getDropdownValues(); - const countryDropdown = block.querySelector('select[name="country"]'); - const languageDropdown = block.querySelector('select[name="language"]'); - const currencyDropdown = block.querySelector('select[name="currency"]'); - const measureDropdown = block.querySelector('select[name="measure"]'); - const { - country, - language, - currency, - measure, - } = dropdownValues; - populateDropdown(countryDropdown, country.data); - populateDropdown(languageDropdown, language.data); - populateDropdown(currencyDropdown, currency.data); - populateDropdown(measureDropdown, measure.data); + populateDropdown(block.querySelector('select[name="country"]'), dropdownValues.country.data); + populateDropdown(block.querySelector('select[name="language"]'), dropdownValues.language.data); + populateDropdown(block.querySelector('select[name="currency"]'), dropdownValues.currency.data); + populateDropdown(block.querySelector('select[name="measure"]'), dropdownValues.measure.data); } function setupPasswordReset(block) { @@ -72,38 +61,31 @@ function setupPasswordReset(block) { function populateForm(block) { const { profile } = getUserDetails() || { profile: {} }; - if (profile) { - form = { - firstName: block.querySelector('input[name="firstName"]'), - lastName: block.querySelector('input[name="lastName"]'), - email: block.querySelector('input[name="email"]'), - mobilePhone: block.querySelector('input[name="mobilePhone"]'), - homePhone: block.querySelector('input[name="homePhone"]'), - country: block.querySelector('select[name="country"]'), - address1: block.querySelector('input[name="address1"]'), - address2: block.querySelector('input[name="address2"]'), - city: block.querySelector('input[name="city"]'), - stateOrProvince: block.querySelector('input[name="stateOrProvince"]'), - postalCode: block.querySelector('input[name="postalCode"]'), - language: block.querySelector('select[name="language"]'), - currency: block.querySelector('select[name="currency"]'), - measure: block.querySelector('select[name="measure"]'), - }; + form = { + firstName: block.querySelector('input[name="firstName"]'), + lastName: block.querySelector('input[name="lastName"]'), + email: block.querySelector('input[name="email"]'), + mobilePhone: block.querySelector('input[name="mobilePhone"]'), + homePhone: block.querySelector('input[name="homePhone"]'), + country: block.querySelector('select[name="country"]'), + address1: block.querySelector('input[name="address1"]'), + address2: block.querySelector('input[name="address2"]'), + city: block.querySelector('input[name="city"]'), + stateOrProvince: block.querySelector('input[name="stateOrProvince"]'), + postalCode: block.querySelector('input[name="postalCode"]'), + language: block.querySelector('select[name="language"]'), + currency: block.querySelector('select[name="currency"]'), + measure: block.querySelector('select[name="measure"]'), + }; - form.firstName.value = profile.firstName || ''; - form.lastName.value = profile.lastName || ''; - form.email.value = profile.email || ''; - form.mobilePhone.value = profile.mobilePhone || ''; - form.homePhone.value = profile.homePhone || ''; - form.country.value = profile.country || 'US'; - form.address1.value = profile.address1 || ''; - form.address2.value = profile.address2 || ''; - form.city.value = profile.city || ''; - form.stateOrProvince.value = profile.stateOrProvince || ''; - form.postalCode.value = profile.postalCode || ''; - form.language.value = profile.language || ''; - form.currency.value = profile.currency || ''; - form.measure.value = profile.measure || ''; + if (profile) { + Object.keys(form).forEach((key) => { + form[key].value = profile[key] || ''; + // If field is required, append asterisk to placeholder + if (form[key].required) { + form[key].placeholder += '*'; + } + }); } } @@ -125,13 +107,14 @@ export default async function decorate(block) { use your contact information, please review our Terms of Use and Privacy Policy.

+

Click reset, and we will send you a email containing a reset password link.

- @@ -139,28 +122,30 @@ export default async function decorate(block) { +

Set the language for emails and this site.

Language

- -

Set the currency and unit of measurement for this site. +

Set the currency and unit of measurement for this site.

Currency

-

Unit of Measurement

- +
`; - populateDropdowns(block); prepareTabs(block); - setupPasswordReset(block); + populateDropdowns(block); populateForm(block); + setupPasswordReset(block); } From 37a78807a86f5f28079ac38f66623f20ceade1db Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Fri, 19 Jan 2024 16:04:04 -0600 Subject: [PATCH 07/21] Updated form placeholders to match site --- blocks/profile/profile.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index 840237ee..331bbf5e 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -95,7 +95,7 @@ export default async function decorate(block) { - +

Please manage your email preferences by using "Unsubscribe" option at the bottom of emails you receive.

@@ -117,11 +117,11 @@ export default async function decorate(block) { - - + + - +
From d6beb0b8c27ea7ab6b2db5c44a3fce1c396000e1 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Fri, 19 Jan 2024 16:11:58 -0600 Subject: [PATCH 08/21] Remove empty selections for regional preferences --- blocks/profile/profile.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index 331bbf5e..fdacc1d2 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -127,18 +127,12 @@ export default async function decorate(block) {

Set the language for emails and this site.

Language

- +

Set the currency and unit of measurement for this site.

Currency

- +

Unit of Measurement

- +
From 694422817542dc4b8af33641211c87f58f62007f Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Fri, 19 Jan 2024 16:14:15 -0600 Subject: [PATCH 09/21] Skip missing profile check --- blocks/profile/profile.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index fdacc1d2..0ecc6e15 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -78,15 +78,13 @@ function populateForm(block) { measure: block.querySelector('select[name="measure"]'), }; - if (profile) { - Object.keys(form).forEach((key) => { - form[key].value = profile[key] || ''; - // If field is required, append asterisk to placeholder - if (form[key].required) { - form[key].placeholder += '*'; - } - }); - } + Object.keys(form).forEach((key) => { + form[key].value = profile[key] || ''; + // If field is required, append asterisk to placeholder + if (form[key].required) { + form[key].placeholder += '*'; + } + }); } export default async function decorate(block) { From 65524d6dce96512c97e40fc7a36f92fbe7730a2b Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Mon, 22 Jan 2024 19:04:52 -0600 Subject: [PATCH 10/21] Improvements to error reporting and form submission. --- blocks/profile/profile.css | 41 ++++++++++++++- blocks/profile/profile.js | 103 ++++++++++++++++++++++++++++++++++++- scripts/apis/user.js | 23 +++++---- 3 files changed, 154 insertions(+), 13 deletions(-) diff --git a/blocks/profile/profile.css b/blocks/profile/profile.css index b51d67db..7ff21e00 100644 --- a/blocks/profile/profile.css +++ b/blocks/profile/profile.css @@ -32,7 +32,7 @@ display: block; } -.profile p { +.profile .tab > p { margin-top: 1em; } @@ -94,4 +94,41 @@ .profile input[name='email'] { width: calc(100% - 0.25em); -} \ No newline at end of file +} + +.profile .error { + width: 500px; + border: 2px solid var(--error); + padding: 0; + margin: 0 1em 1em 0; + color: var(--error); +} + +.profile .error > p, .profile .error > ul { + padding: 1em 1em 0; + margin-bottom: 1em; +} + +.profile .error > p:first-of-type { + background-color: var(--error-highlight); + border-bottom: 1px solid var(--error); + margin: 0; + padding: 0.25em 1em; + line-height: var(--line-height-xl); + font-weight: bold; +} + +.profile .error .info-circle { + width: 24px; + height: 24px; + float: left; + margin: 0.5em 0.25em 0 0; + filter: brightness(4) brightness(0.5) sepia(1) saturate(2) hue-rotate(310deg) +} + +.profile .error ul { + list-style: unset; + margin-left: 2em; + margin-top: 1em; + padding: 0; +} diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index 0ecc6e15..9380e370 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -1,10 +1,15 @@ -import { getUserDetails, requestPasswordReset, updateProfile, isLoggedIn } from '../../scripts/apis/user.js'; +import { + getUserDetails, + requestPasswordReset, + updateProfile, + isLoggedIn, +} from '../../scripts/apis/user.js'; let form = {}; function asHtml(string) { const div = document.createElement('div'); - div.innerHTML = string; + div.innerHTML = string.trim(); return div.firstChild; } @@ -87,6 +92,99 @@ function populateForm(block) { }); } +function clearError() { + const error = document.querySelector('.profile-wrapper .error'); + if (error) { + error.parentNode.removeChild(error); + } +} + +function showError(err) { + clearError(); + + // Place an error div just after the nav container + const errDiv = asHtml(` +
+

+ + There was a problem processing your request. +

+ ${err.startsWith('<') ? '' : '

'} + ${err} + ${err.startsWith('<') ? '' : '

'} +
+ `); + const nav = document.querySelector('.profile-wrapper nav'); + nav.parentNode.insertBefore(errDiv, nav.nextSibling); +} + +function validateForm() { + if (!isLoggedIn()) { + throw new Error('You must be logged in to update your profile.'); + } + + const errors = []; + Object.keys(form).forEach((key) => { + if (form[key].required && !form[key].value) { + let fieldName = form[key].placeholder || key; + if (fieldName.endsWith('*')) { + fieldName = fieldName.slice(0, -1); + } + errors.push(`${fieldName} is required.`); + } + }); + if (errors.length > 0) { + throw new Error(`
  • ${errors.join('
  • ')}
`); + } +} + +async function performSave() { + validateForm(); + const data = {}; + Object.keys(form).forEach((key) => { + data[key] = form[key].value; + }); + const response = await updateProfile(data); + if (response.status === 200) { + return response; + } + const message = await response.text() || 'There was an error saving changes.'; + throw new Error(message); +} + +function setupSaveHandlers(block) { + const saveButtons = block.querySelectorAll('button.save'); + saveButtons.forEach((button) => { + button.addEventListener('click', async () => { + try { + // disable all save buttons + saveButtons.forEach((b) => { + b.disabled = true; + }); + + validateForm(); + await performSave(); + + clearError(); + } catch (errResponse) { + debugger; + let errResponseText = ''; + if (typeof errResponse.message !== 'string') { + errResponseText = errResponse.message && errResponse.message.text ? await errResponse.message.text() : 'We\'re sorry, but something went wrong.'; + } else { + errResponseText = errResponse.message; + } + showError(errResponseText); + } finally { + // Re-enable all save buttons + saveButtons.forEach((b) => { + b.disabled = false; + }); + } + }); + }); +} + export default async function decorate(block) { block.innerHTML = ` @@ -140,4 +238,5 @@ export default async function decorate(block) { populateDropdowns(block); populateForm(block); setupPasswordReset(block); + setupSaveHandlers(block); } diff --git a/scripts/apis/user.js b/scripts/apis/user.js index 588df47e..28dadced 100644 --- a/scripts/apis/user.js +++ b/scripts/apis/user.js @@ -66,26 +66,31 @@ export async function updateProfile(profile) { const postBody = { FirstName: profile.firstName, LastName: profile.lastName, - MobilePhone: profile.mobilePhone, - HomePhone: profile.homePhone, + MobilePhone: profile.mobilePhone || '', + HomePhone: profile.homePhone || '', Email: profile.email, EmailNotifications: profile.emailNotifications || existingProfile.emailNotifications || false, ContactKey: existingProfile.contactKey, signInScheme: profile.signInScheme || existingProfile.signInScheme || 'default', - HomeAddress1: profile.homeAddress1, - HomeAddress2: profile.homeAddress2, - HomeCity: profile.homeCity, - HomeStateOrProvince: profile.homeStateOrProvince, - HomePostalCode: profile.homePostalCode, + HomeAddress1: profile.homeAddress1 || '', + HomeAddress2: profile.homeAddress2 || '', + HomeCity: profile.homeCity || '', + HomeStateOrProvince: profile.homeStateOrProvince || '', + HomePostalCode: profile.homePostalCode || '', Language: profile.language, Currency: profile.currency, UnitOfMeasure: profile.measure, }; - const response = fetch(url, { + // Post as multi-part form + const formData = new FormData(); + Object.keys(postBody).forEach((key) => { + formData.append(key, postBody[key]); + }); + const response = await fetch(url, { method: 'PUT', credentials: 'include', mode: 'cors', - body: new URLSearchParams(postBody).toString(), + body: formData, }); if (response.ok) { From 993daba6a0ba986eb993bdd2fa116a026d5687a4 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Mon, 22 Jan 2024 21:48:00 -0600 Subject: [PATCH 11/21] Profile saves working! --- scripts/apis/user.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/scripts/apis/user.js b/scripts/apis/user.js index 28dadced..95580a20 100644 --- a/scripts/apis/user.js +++ b/scripts/apis/user.js @@ -61,8 +61,7 @@ export async function updateProfile(profile) { const existingProfile = userDetails.profile; // Update profile in backend, post object as name/value pairs - const time = new Date().getTime(); - const url = `${API_URL}/cregUserProfile?Email=${encodeURIComponent(userDetails.username)}&_=${time}`; + const url = `${API_URL}/cregUserProfile`; const postBody = { FirstName: profile.firstName, LastName: profile.lastName, @@ -81,16 +80,15 @@ export async function updateProfile(profile) { Currency: profile.currency, UnitOfMeasure: profile.measure, }; - // Post as multi-part form - const formData = new FormData(); - Object.keys(postBody).forEach((key) => { - formData.append(key, postBody[key]); - }); const response = await fetch(url, { method: 'PUT', credentials: 'include', mode: 'cors', - body: formData, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'x-requested-with': 'XMLHttpRequest', + }, + body: new URLSearchParams(postBody).toString(), }); if (response.ok) { From 279f7ed95fe3522007a4ce7365adcb84a4ec76cb Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Tue, 23 Jan 2024 12:48:32 -0600 Subject: [PATCH 12/21] Password reset working --- blocks/profile/profile.css | 40 ++++++++++++++----- blocks/profile/profile.js | 81 ++++++++++++++++++++++++-------------- scripts/apis/user.js | 10 ++++- 3 files changed, 90 insertions(+), 41 deletions(-) diff --git a/blocks/profile/profile.css b/blocks/profile/profile.css index 7ff21e00..807f75bc 100644 --- a/blocks/profile/profile.css +++ b/blocks/profile/profile.css @@ -96,29 +96,42 @@ width: calc(100% - 0.25em); } -.profile .error { +.profile .error.notification { + --background-color: var(--error-highlight); + --foreground-color: var(--error); +} + +.profile .success.notification { + --background-color: var(--white); + --foreground-color: var(--success); +} + +.profile .notification { width: 500px; - border: 2px solid var(--error); + border: 2px solid var(--foreground-color); padding: 0; margin: 0 1em 1em 0; - color: var(--error); + color: var(--foreground-color); } -.profile .error > p, .profile .error > ul { +.profile .notification > p, .profile .error > ul { padding: 1em 1em 0; margin-bottom: 1em; } -.profile .error > p:first-of-type { - background-color: var(--error-highlight); - border-bottom: 1px solid var(--error); +.profile .notification > p:first-of-type { + background-color: var(--background-color); margin: 0; padding: 0.25em 1em; line-height: var(--line-height-xl); font-weight: bold; } -.profile .error .info-circle { +.profile .notification > p:first-of-type:not(:only-child) { + border-bottom: 1px solid var(--foreground-color); +} + +.profile .notification .info-circle { width: 24px; height: 24px; float: left; @@ -126,9 +139,18 @@ filter: brightness(4) brightness(0.5) sepia(1) saturate(2) hue-rotate(310deg) } -.profile .error ul { +.profile .notification ul { list-style: unset; margin-left: 2em; margin-top: 1em; padding: 0; } + +.profile .notification .success-circle { + width: 24px; + height: 24px; + float: left; + margin: 0.5em 0.25em 0 0; + color: var(--success); +} + diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index 9380e370..a1c3e588 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -56,13 +56,6 @@ async function populateDropdowns(block) { populateDropdown(block.querySelector('select[name="measure"]'), dropdownValues.measure.data); } -function setupPasswordReset(block) { - const resetPassword = block.querySelector('.reset-password'); - resetPassword.addEventListener('click', () => { - requestPasswordReset(); - }); -} - function populateForm(block) { const { profile } = getUserDetails() || { profile: {} }; @@ -92,32 +85,67 @@ function populateForm(block) { }); } -function clearError() { - const error = document.querySelector('.profile-wrapper .error'); - if (error) { - error.parentNode.removeChild(error); +function clearNotification() { + const note = document.querySelector('.profile-wrapper .notification'); + if (note) { + note.parentNode.removeChild(note); } } -function showError(err) { - clearError(); - - // Place an error div just after the nav container +function showNotification(type, iconHtml, message, message2) { + clearNotification(); + let secondPart = ''; + if (message2) { + secondPart = ` + ${message2.startsWith('<') ? '' : '

'} + ${message2} + ${message2.startsWith('<') ? '' : '

'} + `; + } const errDiv = asHtml(` -
+

- - There was a problem processing your request. + ${iconHtml} + ${message}

- ${err.startsWith('<') ? '' : '

'} - ${err} - ${err.startsWith('<') ? '' : '

'} + ${secondPart}
`); const nav = document.querySelector('.profile-wrapper nav'); nav.parentNode.insertBefore(errDiv, nav.nextSibling); } +function showError(err) { + showNotification('error', '', 'There was a problem processing your request.', err); +} + +function showSuccess(message) { + showNotification('success', '', message); +} + +async function getErrorResponseText(errResponse) { + if (typeof errResponse.message !== 'string') { + return errResponse.message && errResponse.message.text ? errResponse.message.text() : 'We\'re sorry, but something went wrong.'; + } + return errResponse.message; +} + +function setupPasswordReset(block) { + const resetPassword = block.querySelector('.reset-password'); + resetPassword.addEventListener('click', async () => { + try { + const response = await requestPasswordReset(); + if (response.status === 200) { + showSuccess('Your password has been reset.
Check your email for a link to create a new password.'); + } else { + throw new Error(await response.text()); + } + } catch (errResponse) { + showError(await getErrorResponseText(errResponse)); + } + }); +} + function validateForm() { if (!isLoggedIn()) { throw new Error('You must be logged in to update your profile.'); @@ -165,16 +193,9 @@ function setupSaveHandlers(block) { validateForm(); await performSave(); - clearError(); + showSuccess('You have successfully saved your profile.'); } catch (errResponse) { - debugger; - let errResponseText = ''; - if (typeof errResponse.message !== 'string') { - errResponseText = errResponse.message && errResponse.message.text ? await errResponse.message.text() : 'We\'re sorry, but something went wrong.'; - } else { - errResponseText = errResponse.message; - } - showError(errResponseText); + showError(await getErrorResponseText(errResponse)); } finally { // Re-enable all save buttons saveButtons.forEach((b) => { diff --git a/scripts/apis/user.js b/scripts/apis/user.js index 95580a20..8e29552b 100644 --- a/scripts/apis/user.js +++ b/scripts/apis/user.js @@ -110,12 +110,18 @@ export async function requestPasswordReset() { return null; } - const time = new Date().getTime(); - const url = `${API_URL}/cregPasswordReset?Email=${encodeURIComponent(userDetails.username)}&_=${time}`; + const url = `${API_URL}/cregForgotPasswordtServlet`; + const postBody = { + Email: userDetails.username, + }; const response = fetch(url, { method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, credentials: 'include', mode: 'cors', + body: new URLSearchParams(postBody).toString(), }); return response; From 3b3374d468224576c721f94fe563f3c1a4369d93 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Tue, 23 Jan 2024 15:08:46 -0600 Subject: [PATCH 13/21] Navigation and login/logout starting to work. Needs more testing --- blocks/header/header.css | 39 +++++++++++++++++++++++++++++++++++ blocks/header/header.js | 25 ++++++++++------------ blocks/login/login-delayed.js | 3 +++ 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/blocks/header/header.css b/blocks/header/header.css index 398a5dd1..a57e1261 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -127,6 +127,45 @@ body.light-nav { display: none; } +.header.block nav .nav-profile .level-2 { + filter: drop-shadow(0 0 6px rgba(0 0 0 / 25%)); + background-color: var(--white); + padding: 0.25em 0; +} + +.header.block nav .nav-profile .level-1:has(.level-2)>a::after { + content: '\f0d7'; + font-family: var(--font-family-fontawesome); + height: var(--body-font-size-s); + width: var(--body-font-size-s); + margin-left: 10px; + color: var(--body-color); + transition: transform 0.2s ease-in-out; +} + +.header.block nav .nav-profile .level-1 .level-2 { + display:none; +} + +.header.block nav .nav-profile .level-1:hover .level-2 { + display: block; +} + +.header.block nav .nav-profile .level-2::before { + content: ''; + position: absolute; + top: -0.5em; + right: 1em; + height: 1em; + width: 1em; + background-color: var(--white); + transform: rotate(45deg); +} + +.header.block nav .nav-profile .level-2 li { + padding: 0.4em 0; +} + .header.block nav .nav-sections { display: none; grid-area: sections; diff --git a/blocks/header/header.js b/blocks/header/header.js index 19415107..19846ddc 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -119,9 +119,9 @@ function buildLogo() { } function doLogout() { - const userDetailsLink = document.body.querySelector('.username a'); - userDetailsLink.textContent = 'Sign In'; logout(); + document.body.querySelector('.nav-profile .login').style.display = 'block'; + document.body.querySelector('.nav-profile .username').style.display = 'none'; } /** @@ -133,24 +133,21 @@ function addProfileLogin(nav) { const profileMenu = document.createElement('ul'); profileMenu.innerHTML = ` - -
  • +
  • +
  • {Username} -
  • - -
  • - Back -
      -
    • Profile
    • + + `; - profileList.prepend(...profileMenu.childNodes); + profileList.append(...profileMenu.childNodes); profileList.querySelector('.login a').addEventListener('click', openSignIn); - profileList.querySelector('.user-menu .logout a').addEventListener('click', doLogout); + profileList.querySelector('.username .logout a').addEventListener('click', doLogout); } /** diff --git a/blocks/login/login-delayed.js b/blocks/login/login-delayed.js index f9933533..b0e50350 100644 --- a/blocks/login/login-delayed.js +++ b/blocks/login/login-delayed.js @@ -42,6 +42,9 @@ function checkForLoggedInUser() { document.body.querySelector('.nav-profile .username').style.display = 'block'; const userDetails = getUserDetails(); userDetailsLink.textContent = userDetails?.profile?.firstName || 'Valued Customer'; + } else { + document.body.querySelector('.nav-profile .login').style.display = 'block'; + document.body.querySelector('.nav-profile .username').style.display = 'none'; } } From 354520ffd4ab980f70047d13972a4444c33541bd Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Tue, 23 Jan 2024 15:13:54 -0600 Subject: [PATCH 14/21] Small cleanup --- blocks/login/login-delayed.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/blocks/login/login-delayed.js b/blocks/login/login-delayed.js index b0e50350..4615bcb1 100644 --- a/blocks/login/login-delayed.js +++ b/blocks/login/login-delayed.js @@ -32,19 +32,26 @@ function loginError(response) { } } +function setVisible(selector, visible = true) { + const elem = document.querySelector(selector); + if (elem) { + elem.style.display = visible ? 'block' : 'none'; + } +} + /** * Checks if the user is logged in and updates the header accordingly. */ function checkForLoggedInUser() { if (isLoggedIn()) { - const userDetailsLink = document.body.querySelector('.nav-profile .username a'); - document.body.querySelector('.nav-profile .login').style.display = 'none'; - document.body.querySelector('.nav-profile .username').style.display = 'block'; + setVisible('.nav-profile .login', false); + setVisible('.nav-profile .username'); const userDetails = getUserDetails(); + const userDetailsLink = document.body.querySelector('.nav-profile .username a'); userDetailsLink.textContent = userDetails?.profile?.firstName || 'Valued Customer'; } else { - document.body.querySelector('.nav-profile .login').style.display = 'block'; - document.body.querySelector('.nav-profile .username').style.display = 'none'; + setVisible('.nav-profile .login'); + setVisible('.nav-profile .username', false); } } From 90406a30d11dafe6f0e34f92d40597592d26fce1 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Tue, 23 Jan 2024 15:21:37 -0600 Subject: [PATCH 15/21] Fix linting gripes --- blocks/header/header.css | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/blocks/header/header.css b/blocks/header/header.css index a57e1261..cbeefda1 100644 --- a/blocks/header/header.css +++ b/blocks/header/header.css @@ -133,6 +133,13 @@ body.light-nav { padding: 0.25em 0; } +.header.block nav .nav-sections > ul > li.nav-drop > a::after { + content: ''; + height: 32px; + width: 32px; + background: url('/icons/chevron-right-white.svg') center center no-repeat; +} + .header.block nav .nav-profile .level-1:has(.level-2)>a::after { content: '\f0d7'; font-family: var(--font-family-fontawesome); @@ -162,6 +169,11 @@ body.light-nav { transform: rotate(45deg); } + +.header.block nav .nav-sections > ul > li { + border-bottom: 1px solid var(--black); +} + .header.block nav .nav-profile .level-2 li { padding: 0.4em 0; } @@ -182,10 +194,6 @@ body.light-nav { margin: 10px 0; } -.header.block nav .nav-sections > ul > li { - border-bottom: 1px solid var(--black); -} - /* stylelint-disable-next-line no-descending-specificity */ .header.block nav .nav-sections > ul > li > a { display: flex; @@ -201,13 +209,6 @@ body.light-nav { justify-content: space-between; } -.header.block nav .nav-sections > ul > li.nav-drop > a::after { - content: ''; - height: 32px; - width: 32px; - background: url('/icons/chevron-right-white.svg') center center no-repeat; -} - .header.block nav .nav-sections > ul > li > ul { padding-bottom: 15px; } From 5272eebecefd714ddba131e987d13f0fbb2dd73b Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Wed, 24 Jan 2024 11:42:13 -0600 Subject: [PATCH 16/21] Better session handling and also moved header visibility changes into header block (and out of login-delayed) --- blocks/header/header.js | 27 +++++++- blocks/login/login-delayed.js | 40 +++--------- blocks/profile/profile.js | 8 ++- scripts/apis/user.js | 119 +++++++++++++++++++++++----------- 4 files changed, 121 insertions(+), 73 deletions(-) diff --git a/blocks/header/header.js b/blocks/header/header.js index 19846ddc..2937eb73 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -1,7 +1,12 @@ import { BREAKPOINTS } from '../../scripts/scripts.js'; import { getMetadata, decorateIcons, decorateSections } from '../../scripts/aem.js'; import { open as openSignIn, close as closeSignIn } from '../login/login.js'; -import { logout } from '../../scripts/apis/user.js'; +import { + logout, + isLoggedIn, + onProfileUpdate, + getUserDetails, +} from '../../scripts/apis/user.js'; // media query match that indicates mobile/tablet width const isDesktop = BREAKPOINTS.large; @@ -124,6 +129,23 @@ function doLogout() { document.body.querySelector('.nav-profile .username').style.display = 'none'; } +function showHideNavProfile() { + const profileList = document.querySelector('.nav-profile ul'); + if (!profileList) { + return; + } + if (isLoggedIn()) { + profileList.querySelector('.login').style.display = 'none'; + profileList.querySelector('.username').style.display = 'block'; + const userDetails = getUserDetails(); + const userDetailsLink = document.body.querySelector('.nav-profile .username a'); + userDetailsLink.textContent = userDetails?.profile?.firstName || 'Valued Customer'; + } else { + profileList.querySelector('.login').style.display = 'block'; + profileList.querySelector('.username').style.display = 'none'; + } +} + /** * Adds the Profile submenu to the Nav. * @param {HTMLDivElement} nav @@ -148,6 +170,7 @@ function addProfileLogin(nav) { profileList.append(...profileMenu.childNodes); profileList.querySelector('.login a').addEventListener('click', openSignIn); profileList.querySelector('.username .logout a').addEventListener('click', doLogout); + onProfileUpdate(showHideNavProfile); } /** @@ -250,5 +273,7 @@ export default async function decorate(block) { navWrapper.className = 'nav-wrapper'; navWrapper.append(nav); block.querySelector(':scope > div').replaceWith(navWrapper); + + showHideNavProfile(); } } diff --git a/blocks/login/login-delayed.js b/blocks/login/login-delayed.js index 4615bcb1..c9fca743 100644 --- a/blocks/login/login-delayed.js +++ b/blocks/login/login-delayed.js @@ -1,5 +1,5 @@ import { close, displayError, reset } from './login.js'; -import { login, isLoggedIn, getUserDetails } from '../../scripts/apis/user.js'; +import { login } from '../../scripts/apis/user.js'; const block = document.querySelector('.login.block'); @@ -24,34 +24,15 @@ function isValid(form) { return true; } -function loginError(response) { - if (response.status === 401) { - displayError(['Invalid username or password.']); - } else { - displayError([`There was an error logging in (${response.body})`]); - } -} - -function setVisible(selector, visible = true) { - const elem = document.querySelector(selector); - if (elem) { - elem.style.display = visible ? 'block' : 'none'; - } -} - -/** - * Checks if the user is logged in and updates the header accordingly. - */ -function checkForLoggedInUser() { - if (isLoggedIn()) { - setVisible('.nav-profile .login', false); - setVisible('.nav-profile .username'); - const userDetails = getUserDetails(); - const userDetailsLink = document.body.querySelector('.nav-profile .username a'); - userDetailsLink.textContent = userDetails?.profile?.firstName || 'Valued Customer'; +async function loginError(response) { + if (response.status) { + if (response.status === 401) { + displayError(['Invalid username or password.']); + } else { + displayError([`There was an error logging in: (${await response.text()})`]); + } } else { - setVisible('.nav-profile .login'); - setVisible('.nav-profile .username', false); + displayError([`There was an error logging in: ${response}`]); } } @@ -71,7 +52,6 @@ async function submit(form) { const userDetails = await login(credentials, loginError); if (userDetails) { close(); - checkForLoggedInUser(); } } } @@ -129,5 +109,3 @@ block.querySelector('.cta a.cancel').addEventListener('click', (e) => { e.stopPropagation(); close(); }); - -checkForLoggedInUser(); diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index a1c3e588..c4ce4151 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -1,8 +1,9 @@ import { getUserDetails, requestPasswordReset, - updateProfile, + saveProfile, isLoggedIn, + onProfileUpdate, } from '../../scripts/apis/user.js'; let form = {}; @@ -79,7 +80,7 @@ function populateForm(block) { Object.keys(form).forEach((key) => { form[key].value = profile[key] || ''; // If field is required, append asterisk to placeholder - if (form[key].required) { + if (form[key].required && !form[key].placeholder.endsWith('*')) { form[key].placeholder += '*'; } }); @@ -172,7 +173,7 @@ async function performSave() { Object.keys(form).forEach((key) => { data[key] = form[key].value; }); - const response = await updateProfile(data); + const response = await saveProfile(data); if (response.status === 200) { return response; } @@ -260,4 +261,5 @@ export default async function decorate(block) { populateForm(block); setupPasswordReset(block); setupSaveHandlers(block); + onProfileUpdate(() => populateForm(block)); } diff --git a/scripts/apis/user.js b/scripts/apis/user.js index 8e29552b..550d13a7 100644 --- a/scripts/apis/user.js +++ b/scripts/apis/user.js @@ -32,11 +32,12 @@ export function isLoggedIn() { * @returns {object} user details */ export function getUserDetails() { - if (!isLoggedIn()) { - return null; - } - const userDetails = sessionStorage.getItem('userDetails'); + if (!userDetails) { + return { + profile: {}, + }; + } return JSON.parse(userDetails); } @@ -47,13 +48,42 @@ async function fetchUserProfile(username) { return json; } +const profileListeners = []; + +/** + * Register a callback handler that is fired any time a profile is modified + * by either login, logout, updateProfile, or saveProfile + * + * @param {Function} listener + */ +export function onProfileUpdate(listener) { + profileListeners.push(listener); +} + +/** Make changes to the user profile in session (does not save to the servlet) + * This also triggers any listeners that are registered for profile updates + * + * @param {Object} Updated user profile +*/ +export function updateProfile(profile) { + const userDetails = getUserDetails(); + + // Update profile in session storage using a merge + const existingProfile = userDetails.profile; + userDetails.profile = { ...existingProfile, ...profile }; + sessionStorage.setItem('userDetails', JSON.stringify(userDetails)); + profileListeners.forEach((listener) => { + listener(userDetails.profile); + }); +} + /** * Attempt to update the user profile. If successful, also update session copy. * Caller must look at response to see if it was successful, etc. * @param {Object} Updated user profile * @returns response object with status, null if user not logged in */ -export async function updateProfile(profile) { +export async function saveProfile(profile) { const userDetails = getUserDetails(); if (userDetails === null) { return null; @@ -92,9 +122,8 @@ export async function updateProfile(profile) { }); if (response.ok) { - // Update profile in session storage using a merge - userDetails.profile = { ...existingProfile, ...profile }; - sessionStorage.setItem('userDetails', JSON.stringify(userDetails)); + // Update profile in session + updateProfile(profile); } return response; @@ -142,6 +171,9 @@ export function logout() { document.cookie = `${cookie}; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; } }); + profileListeners.forEach((listener) => { + listener({}); + }); } /** @@ -155,39 +187,50 @@ export function logout() { */ export async function login(credentials, failureCallback = null) { const url = `${API_URL}/cregLoginServlet`; - const resp = await fetch(url, { - method: 'POST', - credentials: 'include', - mode: 'cors', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - }, - body: new URLSearchParams({ - Username: credentials.username, - Password: credentials.password, - }).toString(), - }); - if (resp.ok) { - // Extract contactKey and externalID from response JSON. Store in session - const responseJson = await resp.json(); - const { contactKey } = responseJson; - // const { hsfconsumerid } = JSON.parse(externalID); - - const profile = await fetchUserProfile(credentials.username); - - const sessionData = { - contactKey, - // externalID, - // hsfconsumerid, - profile, - username: credentials.username, - }; - sessionStorage.setItem('userDetails', JSON.stringify(sessionData)); - return sessionData; + let error; + try { + const resp = await fetch(url, { + method: 'POST', + credentials: 'include', + mode: 'cors', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: new URLSearchParams({ + Username: credentials.username, + Password: credentials.password, + }).toString(), + }); + if (resp.ok) { + // Extract contactKey and externalID from response JSON. Store in session + const responseJson = await resp.json(); + const { contactKey } = responseJson; + // const { hsfconsumerid } = JSON.parse(externalID); + + const profile = await fetchUserProfile(credentials.username); + + const sessionData = { + contactKey, + // externalID, + // hsfconsumerid, + profile, + username: credentials.username, + }; + sessionStorage.setItem('userDetails', JSON.stringify(sessionData)); + profileListeners.forEach((listener) => { + listener(profile); + }); + return sessionData; + } + error = resp; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e.message); + error = e.message; } logout(); if (failureCallback) { - failureCallback(resp); + await failureCallback(error); } return null; } From 2b69194ce6eda3037cea0c0c9d47f9833f6ef3d5 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Wed, 24 Jan 2024 15:32:51 -0600 Subject: [PATCH 17/21] Refactored i18n to be in utils --- blocks/profile/profile.js | 73 ++++++++++++++++++++------------------- scripts/util.js | 34 ++++++++++++++++++ 2 files changed, 72 insertions(+), 35 deletions(-) diff --git a/blocks/profile/profile.js b/blocks/profile/profile.js index c4ce4151..087c423f 100644 --- a/blocks/profile/profile.js +++ b/blocks/profile/profile.js @@ -5,8 +5,10 @@ import { isLoggedIn, onProfileUpdate, } from '../../scripts/apis/user.js'; +import { i18nLookup } from '../../scripts/util.js'; let form = {}; +let i18n; function asHtml(string) { const div = document.createElement('div'); @@ -117,11 +119,11 @@ function showNotification(type, iconHtml, message, message2) { } function showError(err) { - showNotification('error', '', 'There was a problem processing your request.', err); + showNotification('error', '', i18n('There was a problem processing your request.'), i18n(err)); } function showSuccess(message) { - showNotification('success', '', message); + showNotification('success', '', i18n(message)); } async function getErrorResponseText(errResponse) { @@ -137,7 +139,7 @@ function setupPasswordReset(block) { try { const response = await requestPasswordReset(); if (response.status === 200) { - showSuccess('Your password has been reset.
      Check your email for a link to create a new password.'); + showSuccess(`${i18n('Your password has been reset.')}
      ${i18n('Check your email for a link to create a new password.')}`); } else { throw new Error(await response.text()); } @@ -149,7 +151,7 @@ function setupPasswordReset(block) { function validateForm() { if (!isLoggedIn()) { - throw new Error('You must be logged in to update your profile.'); + throw new Error(i18n('You must be logged in to update your profile.')); } const errors = []; @@ -159,7 +161,7 @@ function validateForm() { if (fieldName.endsWith('*')) { fieldName = fieldName.slice(0, -1); } - errors.push(`${fieldName} is required.`); + errors.push(`${fieldName} ${i18n('is required')}.`); } }); if (errors.length > 0) { @@ -208,50 +210,51 @@ function setupSaveHandlers(block) { } export default async function decorate(block) { + i18n = await i18nLookup(); block.innerHTML = ` - - - - -

      Please manage your email preferences by using "Unsubscribe" option at the bottom of emails you receive.

      - - + + + + +

      ${i18n('Please manage your email preferences by using "Unsubscribe" option at the bottom of emails you receive.')}

      + +

      - By providing your telephone number, you are giving permission to Berkshire Hathaway HomeServices and a franchisee + ${i18n(`By providing your telephone number, you are giving permission to Berkshire Hathaway HomeServices and a franchisee member of the Berkshire Hathaway HomeServices real estate network to communicate with you by phone or text, including automated means, even if your telephone number appears on any "Do Not Call" list. A phone number is not - required in order to receive real estate brokerage services. Message/data rates may apply. For more about how we will - use your contact information, please review our Terms of Use - and Privacy Policy. + required in order to receive real estate brokerage services. Message/data rates may apply.`)} + ${i18n('For more about how we will use your contact information, please review our')} + ${i18n('Terms of Use')} ${i18n('and')} ${i18n('Privacy Policy')}.

      - +
      - -

      Click reset, and we will send you a email containing a reset password link.

      - + +

      ${i18n('Click reset, and we will send you a email containing a reset password link.')}

      +
      - + - - - - - - + + + + + + - -

      Set the language for emails and this site.

      -

      Language

      + +

      ${i18n('Set the language for emails and this site.')}

      +

      ${i18n('Language')}

      -

      Set the currency and unit of measurement for this site.

      -

      Currency

      +

      ${i18n('Set the currency and unit of measurement for this site.')}

      +

      ${i18n('Currency')}

      -

      Unit of Measurement

      +

      ${i18n('Unit of Measurement')}

      - +
      `; diff --git a/scripts/util.js b/scripts/util.js index ffd1b819..f89c8c4b 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -1,3 +1,5 @@ +import { fetchPlaceholders } from './aem.js'; + /** * Creates the standard Spinner Div. * @@ -47,9 +49,41 @@ export function showModal(content) { document.body.append(modal); } +function createTextKey(text) { + // create a key that can be used to look up the text in the placeholders + const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/); + if (words.length > 5) { + words.splice(5); + } + words.forEach((word, i) => { + if (i > 0) { + words[i] = word.charAt(0).toUpperCase() + word.slice(1); + } + }); + return words.join(''); +} + +export async function i18nLookup(prefix) { + const placeholders = await fetchPlaceholders(prefix); + return (msg) => { + if (placeholders[msg]) { + return placeholders[msg]; + } + if (placeholders[msg.toLowerCase()]) { + return placeholders[msg.toLowerCase()]; + } + const key = createTextKey(msg); + if (placeholders[key]) { + return placeholders[key]; + } + return msg; + }; +} + const Util = { getSpinner, showModal, + i18nLookup, }; export default Util; From 624cd18bd909d59a1b70b686ebe6aeb9d5e543e9 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Wed, 24 Jan 2024 16:00:11 -0600 Subject: [PATCH 18/21] Add i18n support to header nav --- blocks/header/header.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/blocks/header/header.js b/blocks/header/header.js index 2937eb73..73430699 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -7,9 +7,11 @@ import { onProfileUpdate, getUserDetails, } from '../../scripts/apis/user.js'; +import { i18nLookup } from '../../scripts/util.js'; // media query match that indicates mobile/tablet width const isDesktop = BREAKPOINTS.large; +let i18n; function closeOnEscape(e) { if (e.code === 'Escape') { @@ -139,7 +141,7 @@ function showHideNavProfile() { profileList.querySelector('.username').style.display = 'block'; const userDetails = getUserDetails(); const userDetailsLink = document.body.querySelector('.nav-profile .username a'); - userDetailsLink.textContent = userDetails?.profile?.firstName || 'Valued Customer'; + userDetailsLink.textContent = userDetails?.profile?.firstName || i18n('Valued Customer'); } else { profileList.querySelector('.login').style.display = 'block'; profileList.querySelector('.username').style.display = 'none'; @@ -155,15 +157,12 @@ function addProfileLogin(nav) { const profileMenu = document.createElement('ul'); profileMenu.innerHTML = ` - +
    • {Username} -
    • `; @@ -183,10 +182,10 @@ function buildHamburger() { const icon = document.createElement('div'); icon.classList.add('nav-hamburger-icon'); icon.innerHTML = ` -
  • - - + +
    - +
    - I forgot my password + ${i18n('I forgot my password')}
    -
    OR
    +
    ${i18n('OR')}
    - By clicking 'SIGN IN' or registering using any of the above third-party logins, I agree to the - Terms of Use and Privacy Policy for this website. + ${i18n('By clicking \'SIGN IN\' or registering using any of the above third-party logins, I agree to the')} + ${i18n('Terms of Use')} ${i18n('and')} ${i18n('Privacy Policy')} ${i18n('for this website.')}
    From 307bd7571843c2e1bd991bdc11acc11ca7e58fdb Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Wed, 24 Jan 2024 16:11:11 -0600 Subject: [PATCH 20/21] Add i18n to login form --- blocks/login/login-delayed.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/blocks/login/login-delayed.js b/blocks/login/login-delayed.js index c9fca743..e9379979 100644 --- a/blocks/login/login-delayed.js +++ b/blocks/login/login-delayed.js @@ -1,6 +1,8 @@ import { close, displayError, reset } from './login.js'; import { login } from '../../scripts/apis/user.js'; +import { i18nLookup } from '../../scripts/util.js'; +const i18n = await i18nLookup(); const block = document.querySelector('.login.block'); function isValid(form) { @@ -27,12 +29,12 @@ function isValid(form) { async function loginError(response) { if (response.status) { if (response.status === 401) { - displayError(['Invalid username or password.']); + displayError([i18n('Invalid username or password.')]); } else { - displayError([`There was an error logging in: (${await response.text()})`]); + displayError([`${i18n('There was an error logging in')}: (${i18n(await response.text())})`]); } } else { - displayError([`There was an error logging in: ${response}`]); + displayError([`${i18n('There was an error logging in')}: ${i18n(response)}`]); } } From 15ac38d8a7d6cfc8a44688f5a4529f9be0b97623 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Wed, 24 Jan 2024 16:12:08 -0600 Subject: [PATCH 21/21] Missing two text strings in i18n for login --- blocks/login/login-delayed.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blocks/login/login-delayed.js b/blocks/login/login-delayed.js index e9379979..504253e3 100644 --- a/blocks/login/login-delayed.js +++ b/blocks/login/login-delayed.js @@ -9,14 +9,14 @@ function isValid(form) { const errors = []; const user = form.querySelector('input[name="username"]'); if (!user.value || user.value.trim().length === 0) { - errors.push('Email address is required.'); + errors.push(i18n('Email address is required.')); block.querySelector('input[name="username"]').classList.add('error'); } const password = form.querySelector('input[name="password"]'); if (!password.value || password.value.trim().length === 0) { block.querySelector('input[name="password"]').classList.add('error'); - errors.push('Password is required.'); + errors.push(i18n('Password is required.')); } if (errors.length > 0) {