Move the digest email opt-in to the follow tags onboarding step (#20133)
* Removed intro slide and updated sequence * Revert db change * Initial save * Design changes * Mobile designs * Added functionality * Added tests * Gradient effect added * Added API call test
This commit is contained in:
parent
35c6c4bca7
commit
54131cd94c
5 changed files with 210 additions and 23 deletions
|
|
@ -92,6 +92,46 @@
|
|||
}
|
||||
}
|
||||
|
||||
.onboarding-content-separator {
|
||||
position: absolute;
|
||||
bottom: 160px;
|
||||
left: var(--su-4);
|
||||
width: calc(100% - 2 * var(--su-4));
|
||||
height: 96px;
|
||||
background-image: linear-gradient(
|
||||
rgba(255, 255, 255, 0) 25%,
|
||||
rgba(255, 255, 255, 1) 75%
|
||||
);
|
||||
pointer-events: none;
|
||||
|
||||
@media screen and (min-width: $breakpoint-s) {
|
||||
left: var(--su-8);
|
||||
width: calc(100% - 2 * var(--su-8));
|
||||
bottom: 168px;
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-email-digest {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
margin: 0px var(--su-4) var(--su-4) var(--su-4);
|
||||
align-items: center;
|
||||
border-radius: 0px 6px 6px 0px;
|
||||
border: 1px solid rgba(var(--tag-onboarding-border), 1);
|
||||
background-color: var(--card-secondary-bg);
|
||||
|
||||
@media screen and (min-width: $breakpoint-s) {
|
||||
margin: 0px var(--su-8) 40px var(--su-8);
|
||||
}
|
||||
|
||||
&__rectangle {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
background: rgba(var(--accent-brand-rgb), 1);
|
||||
}
|
||||
}
|
||||
|
||||
// modal navigation
|
||||
.onboarding-navigation {
|
||||
align-self: flex-end;
|
||||
|
|
@ -217,6 +257,11 @@
|
|||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-gap: var(--su-2);
|
||||
width: 100%;
|
||||
padding-bottom: 48px;
|
||||
|
||||
@media screen and (min-width: $breakpoint-s) {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export class EmailPreferencesForm extends Component {
|
|||
|
||||
this.state = {
|
||||
email_newsletter: false,
|
||||
email_digest_periodic: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +48,7 @@ export class EmailPreferencesForm extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { email_newsletter, email_digest_periodic } = this.state;
|
||||
const { email_newsletter } = this.state;
|
||||
const { prev, slidesCount, currentSlideIndex } = this.props;
|
||||
return (
|
||||
<div
|
||||
|
|
@ -88,19 +87,6 @@ export class EmailPreferencesForm extends Component {
|
|||
I want to receive weekly newsletter emails.
|
||||
</label>
|
||||
</li>
|
||||
<li className="checkbox-item">
|
||||
<label htmlFor="email_digest_periodic">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="email_digest_periodic"
|
||||
name="email_digest_periodic"
|
||||
checked={email_digest_periodic}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
I want to receive a periodic digest of top posts from my
|
||||
tags.
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ export class FollowTags extends Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleComplete = this.handleComplete.bind(this);
|
||||
|
||||
this.state = {
|
||||
allTags: [],
|
||||
selectedTags: [],
|
||||
email_digest_periodic: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -38,6 +40,31 @@ export class FollowTags extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
handleContainerClick = () => {
|
||||
const checkbox = document.getElementById('email_digest_periodic');
|
||||
checkbox.checked = !checkbox.checked;
|
||||
|
||||
const event = new Event('change', { bubbles: true });
|
||||
checkbox.dispatchEvent(event);
|
||||
|
||||
this.setState({ email_digest_periodic: checkbox.checked });
|
||||
};
|
||||
|
||||
handleContainerKeyDown = (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
this.handleContainerClick();
|
||||
}
|
||||
};
|
||||
|
||||
handleCheckboxClick = (event) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
handleChange = (event) => {
|
||||
const { name, checked } = event.target;
|
||||
this.setState({ [name]: checked });
|
||||
};
|
||||
|
||||
handleClick(tag) {
|
||||
let { selectedTags } = this.state;
|
||||
if (!selectedTags.includes(tag)) {
|
||||
|
|
@ -56,7 +83,7 @@ export class FollowTags extends Component {
|
|||
|
||||
handleComplete() {
|
||||
const csrfToken = getContentOfToken('csrf-token');
|
||||
const { selectedTags } = this.state;
|
||||
const { selectedTags, email_digest_periodic } = this.state;
|
||||
|
||||
Promise.all(
|
||||
selectedTags.map((tag) =>
|
||||
|
|
@ -74,10 +101,31 @@ export class FollowTags extends Component {
|
|||
credentials: 'same-origin',
|
||||
}),
|
||||
),
|
||||
).then((_) => {
|
||||
const { next } = this.props;
|
||||
next();
|
||||
});
|
||||
)
|
||||
.then(() => {
|
||||
if (email_digest_periodic) {
|
||||
return fetch('/onboarding/notifications', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'X-CSRF-Token': csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
notifications: {
|
||||
email_digest_periodic: this.state.email_digest_periodic,
|
||||
},
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then((response) => {
|
||||
if (!email_digest_periodic || response.ok) {
|
||||
const { next } = this.props;
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderFollowCount() {
|
||||
|
|
@ -94,7 +142,7 @@ export class FollowTags extends Component {
|
|||
|
||||
render() {
|
||||
const { prev, currentSlideIndex, slidesCount } = this.props;
|
||||
const { selectedTags, allTags } = this.state;
|
||||
const { selectedTags, allTags, email_digest_periodic } = this.state;
|
||||
const canSkip = selectedTags.length === 0;
|
||||
|
||||
return (
|
||||
|
|
@ -162,6 +210,46 @@ export class FollowTags extends Component {
|
|||
})}
|
||||
</div>
|
||||
</div>
|
||||
<span class="onboarding-content-separator" />
|
||||
<div
|
||||
class="onboarding-email-digest"
|
||||
onClick={this.handleContainerClick}
|
||||
onKeyDown={this.handleContainerKeyDown}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<span class="onboarding-email-digest__rectangle" />
|
||||
<div class="flex items-start my-4 ml-1 mr-4">
|
||||
<form>
|
||||
<fieldset>
|
||||
<ul>
|
||||
<li className="checkbox-item">
|
||||
<label htmlFor="email_digest_periodic">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="email_digest_periodic"
|
||||
name="email_digest_periodic"
|
||||
checked={email_digest_periodic}
|
||||
onChange={this.handleChange}
|
||||
onClick={this.handleCheckboxClick}
|
||||
tabIndex="-1"
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div class="flex flex-col items-start">
|
||||
<p class="crayons-subtitle-3 fw-medium">
|
||||
Get a Periodic Digest of Top Posts
|
||||
</p>
|
||||
<p class="fs-s fw-normal lh-base color-secondary">
|
||||
We'll email you with a curated selection of top posts based on
|
||||
the tags you follow.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Navigation
|
||||
prev={prev}
|
||||
next={this.handleComplete}
|
||||
|
|
|
|||
|
|
@ -56,11 +56,10 @@ describe('EmailPreferencesForm', () => {
|
|||
expect(queryByText('Email preferences')).toExist();
|
||||
});
|
||||
|
||||
it('should show the two checkboxes unchecked', () => {
|
||||
it('should show the checkbox unchecked', () => {
|
||||
const { queryByLabelText } = renderEmailPreferencesForm();
|
||||
|
||||
expect(queryByLabelText(/receive weekly newsletter/i).checked).toBe(false);
|
||||
expect(queryByLabelText(/receive a periodic digest/i).checked).toBe(false);
|
||||
});
|
||||
|
||||
it('should render a stepper', () => {
|
||||
|
|
|
|||
|
|
@ -162,4 +162,73 @@ describe('FollowTags', () => {
|
|||
expect(getByText('0 tags selected')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should toggle the checkbox when container is clicked', async () => {
|
||||
const { container } = renderFollowTags();
|
||||
const checkbox = container.querySelector('#email_digest_periodic');
|
||||
|
||||
expect(checkbox.checked).toBeFalsy();
|
||||
|
||||
fireEvent.click(container.querySelector('.onboarding-email-digest'));
|
||||
|
||||
expect(checkbox.checked).toBeTruthy();
|
||||
|
||||
fireEvent.click(container.querySelector('.onboarding-email-digest'));
|
||||
|
||||
expect(checkbox.checked).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should toggle the checkbox when Enter or Space key is pressed', async () => {
|
||||
const { container } = renderFollowTags();
|
||||
const checkbox = container.querySelector('#email_digest_periodic');
|
||||
|
||||
expect(checkbox.checked).toBeFalsy();
|
||||
|
||||
fireEvent.keyDown(container.querySelector('.onboarding-email-digest'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
charCode: 13,
|
||||
});
|
||||
|
||||
expect(checkbox.checked).toBeTruthy();
|
||||
|
||||
fireEvent.keyDown(container.querySelector('.onboarding-email-digest'), {
|
||||
key: ' ',
|
||||
code: 'Space',
|
||||
keyCode: 32,
|
||||
charCode: 32,
|
||||
});
|
||||
|
||||
expect(checkbox.checked).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should prevent checkbox click event from propagating', async () => {
|
||||
const { container } = renderFollowTags();
|
||||
const checkbox = container.querySelector('#email_digest_periodic');
|
||||
|
||||
let clicked = false;
|
||||
|
||||
checkbox.addEventListener('click', () => {
|
||||
clicked = true;
|
||||
});
|
||||
|
||||
fireEvent.click(container.querySelector('.onboarding-email-digest'));
|
||||
|
||||
expect(clicked).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should call /onboarding/notifications API when email_digest_periodic is true', async () => {
|
||||
const { getByText, container } = renderFollowTags();
|
||||
|
||||
fireEvent.click(container.querySelector('.onboarding-email-digest'));
|
||||
|
||||
const skipButton = getByText(/Skip for now/i);
|
||||
fireEvent.click(skipButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const [lastFetchUri] = fetch.mock.calls[fetch.mock.calls.length - 1];
|
||||
expect(lastFetchUri).toEqual('/onboarding/notifications');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue