Skip to content

Commit

Permalink
Feat/profile (#194)
Browse files Browse the repository at this point in the history
* Update profile code implemetation

* Start of profile UI implementation and saving routine

* Tabs are working, needs more form styling and validation though

* Style improvements, also adjusted selectors to make linter happy

* Dropdown values implemented

* More styling work and visual support for required fields

* Updated form placeholders to match site

* Remove empty selections for regional preferences

* Skip missing profile check

* Improvements to error reporting and form submission.

* Profile saves working!

* Password reset working

* Navigation and login/logout starting to work.  Needs more testing

* Small cleanup

* Fix linting gripes

* Better session handling and also moved header visibility changes into header block (and out of login-delayed)

* Refactored i18n to be in utils

* Add i18n support to header nav

* Added i18n to login form

* Add i18n to login form

* Missing two text strings in i18n for login

---------

Co-authored-by: Brendan Robert <[email protected]>
  • Loading branch information
2 people authored and bstopp committed Mar 6, 2024
1 parent faa31ae commit 4f9936f
Show file tree
Hide file tree
Showing 8 changed files with 744 additions and 104 deletions.
62 changes: 51 additions & 11 deletions blocks/header/header.css
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,57 @@ 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-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);
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-sections > ul > li {
border-bottom: 1px solid var(--black);
}

.header.block nav .nav-profile .level-2 li {
padding: 0.4em 0;
}

.header.block nav .nav-sections {
display: none;
grid-area: sections;
Expand All @@ -143,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;
Expand All @@ -162,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;
}
Expand Down
59 changes: 41 additions & 18 deletions blocks/header/header.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
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';
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') {
Expand Down Expand Up @@ -119,9 +126,26 @@ 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';
}

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 || i18n('Valued Customer');
} else {
profileList.querySelector('.login').style.display = 'block';
profileList.querySelector('.username').style.display = 'none';
}
}

/**
Expand All @@ -133,24 +157,19 @@ function addProfileLogin(nav) {

const profileMenu = document.createElement('ul');
profileMenu.innerHTML = `
<li class="login">
<a href="#">Sign In</a>
</li>
<li class="username">
<li class="level-1 login"><a href="#">${i18n('Sign In')}</a></li>
<li class="level-1 username">
<a href="#">{Username}</a>
</li>
<li class="user-menu">
<a href="#">Back</a>
<ul>
<li class="profile"><a href="#">Profile</a></li>
<li class="logout"><a href="#">Sign out</a></li>
<ul class="level-2">
<li class="profile"><a href="/account/profile">${i18n('Profile')}</a></li>
<li class="logout"><a href="#">${i18n('Sign out')}</a></li>
</ul>
</li>
`;
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);
onProfileUpdate(showHideNavProfile);
}

/**
Expand All @@ -163,10 +182,10 @@ function buildHamburger() {
const icon = document.createElement('div');
icon.classList.add('nav-hamburger-icon');
icon.innerHTML = `
<svg class="open" role="img" aria-hidden="true" tabindex="-1" aria-label="Open Navigation">
<svg class="open" role="img" aria-hidden="true" tabindex="-1" aria-label="${i18n('Open Navigation')}">
<use id="hamburger-icon" xlink:href="/icons/icons.svg#hamburger-white"></use>
</svg>
<svg class="close" role="img" aria-hidden="true" tabindex="-1" aria-label="Close Navigation">
<svg class="close" role="img" aria-hidden="true" tabindex="-1" aria-label="${i18n('Close Navigation')}">
<use id="close-hamburger-icon" xlink:href="/icons/icons.svg#close-x"></use>
</svg>
`;
Expand All @@ -179,6 +198,8 @@ function buildHamburger() {
* @param {Element} block The header block element
*/
export default async function decorate(block) {
i18n = await i18nLookup();

// fetch nav content
const navMeta = getMetadata('nav');
const navPath = navMeta ? new URL(navMeta).pathname : '/nav';
Expand Down Expand Up @@ -253,5 +274,7 @@ export default async function decorate(block) {
navWrapper.className = 'nav-wrapper';
navWrapper.append(nav);
block.querySelector(':scope > div').replaceWith(navWrapper);

showHideNavProfile();
}
}
36 changes: 13 additions & 23 deletions blocks/login/login-delayed.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { close, displayError, reset } from './login.js';
import { login, isLoggedIn, getUserDetails } from '../../scripts/apis/user.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) {
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) {
Expand All @@ -24,24 +26,15 @@ function isValid(form) {
return true;
}

function loginError(response) {
if (response.status === 401) {
displayError(['Invalid username or password.']);
async function loginError(response) {
if (response.status) {
if (response.status === 401) {
displayError([i18n('Invalid username or password.')]);
} else {
displayError([`${i18n('There was an error logging in')}: (${i18n(await response.text())})`]);
}
} else {
displayError([`There was an error logging in (${response.body})`]);
}
}

/**
* 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';
const userDetails = getUserDetails();
userDetailsLink.textContent = userDetails?.profile?.firstName || 'Valued Customer';
displayError([`${i18n('There was an error logging in')}: ${i18n(response)}`]);
}
}

Expand All @@ -61,7 +54,6 @@ async function submit(form) {
const userDetails = await login(credentials, loginError);
if (userDetails) {
close();
checkForLoggedInUser();
}
}
}
Expand Down Expand Up @@ -119,5 +111,3 @@ block.querySelector('.cta a.cancel').addEventListener('click', (e) => {
e.stopPropagation();
close();
});

checkForLoggedInUser();
42 changes: 24 additions & 18 deletions blocks/login/login.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { i18nLookup } from '../../scripts/util.js';

let i18n;

const LOGIN_ERROR = 'There was a problem processing your request.';

export function reset() {
Expand Down Expand Up @@ -66,7 +70,9 @@ function initLogin() {
document.head.append(script);
}

export default function decorate(block) {
export default async function decorate(block) {
i18n = await i18nLookup();

block.innerHTML = `
<div class="login-overlay"></div>
<div class="login-form">
Expand All @@ -80,55 +86,55 @@ export default function decorate(block) {
</div>
</div>
<div class="inputs">
<input name="username" aria-label="email address" aria-required="true"
type="text" placeholder="Email Address*" autocomplete="email" >
<input name="password" aria-label="password" aria-required="true"
type="password" placeholder="Password*" autocomplete="current-password">
<input name="username" aria-label="${i18n('email address')}" aria-required="true"
type="text" placeholder="${i18n('Email Address')}*" autocomplete="email" >
<input name="password" aria-label="${i18n('password')}" aria-required="true"
type="password" placeholder="${i18n('Password')}*" autocomplete="current-password">
</div>
<div class="help">
<div class="remember">
<input type="checkbox" name="rememberMe" aria-label="remember me" id="rememberMe">
<div class="checkbox"></div>
<label for="rememberMe">Remember me</label>
<label for="rememberMe">${i18n('Remember me')}</label>
</div>
<a href="#" class="forgot-password" role="button">I forgot my password</a>
<a href="#" class="forgot-password" role="button">${i18n('I forgot my password')}</a>
<div class="warning" role="alert">
Don’t check this box if you are using a public computer or shared device.
${i18n('Don’t check this box if you are using a public computer or shared device.')}
</div>
</div>
<div class="cta">
<div class="button-container">
<a href="" class="button primary submit" role="button">Sign In</a>
<a href="" class="button primary submit" role="button">${i18n('Sign In')}</a>
</div>
<div class="button-container">
<a href="" class="button secondary cancel" role="button">Cancel</a>
<a href="" class="button secondary cancel" role="button">${i18n('Cancel')}</a>
</div>
</div>
<div class="divider">OR</div>
<div class="divider">${i18n('OR')}</div>
<div class="social-sign-in">
<a class="fb button" role="button">
<img src="https://facebookbrand.com/wp-content/uploads/2019/10/Copy-of-facebook-app.svg" alt="facebook icon #1" class="facebook">
<span>Continue with Facebook</span>
<span>${i18n('Continue with Facebook')}</span>
</a>
<a class="google button" role="button">
<img src="data:image/png;base64,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" alt="Google icon #1" loading="lazy" class="google_logo">
<span>Continue with Google</span>
<span>${i18n('Continue with Google')}</span>
</a>
<a class="apple button" role="button">
<span>Continue with Apple</span>
<span>${i18n('Continue with Apple')}</span>
</a>
</div>
<div class="terms">
By clicking 'SIGN IN' or registering using any of the above third-party logins, I agree to the
<a href="/terms-of-use">Terms of Use</a> and <a href="/privacy-policy">Privacy Policy </a> for this website.
${i18n('By clicking \'SIGN IN\' or registering using any of the above third-party logins, I agree to the')}
<a href="/terms-of-use">${i18n('Terms of Use')}</a> ${i18n('and')} <a href="/privacy-policy">${i18n('Privacy Policy')} </a> ${i18n('for this website.')}
</div>
<button type="submit" aria-label="Submit" title="Submit" class="sr-only"></button>
</form>
<div class="create-account">
<div class="container">
Not a member yet?
${i18n('Not a member yet?')}
<br>
<div class="create-button" role="button" tabindex="0">Create an account</div>
<div class="create-button" role="button" tabindex="0">${i18n('Create an account')}</div>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit 4f9936f

Please sign in to comment.