Improve onboarding UI and UX (P1) (#6901)

* Improve onboarding UI and UX (P1)

- Introduce Crayons variables
- Remove unnecessary CSS files to have just one CSS file

* Add new onboarding tests

* Update onboarding yarn test to reflect updated class names
This commit is contained in:
Lisa Sy 2020-03-30 15:38:15 -07:00 committed by GitHub
parent ec371b4624
commit b6fd02b70f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1117 additions and 876 deletions

View file

@ -1,45 +0,0 @@
@import 'variables';
#onboarding-container {
min-height: calc(100vh - 350px);
}
.onboarding-container {
max-width: 800px;
margin: 0 auto;
.next-button {
float: right;
}
.next-button,
.back-button {
padding: 4px 20px;
border-radius: 100px;
margin-right: 5px;
font-size: 14px;
margin-top: 6px;
display: inline-block;
color: #0a0a0a;
}
.next-button {
background: #66e2d5;
border: 2px solid transparent;
}
.back-button {
background: white;
border: 2px solid black;
}
label,
textarea,
input {
display: block;
}
input[type='checkbox'] {
display: inline;
}
}

View file

@ -1,440 +1,548 @@
@import 'variables';
@import 'mixins';
@import 'components/buttons';
@import 'components/modals';
@import 'components/forms';
@import 'config/import';
#onboarding-container {
min-height: calc(100vh - 350px);
}
.pages-onboarding {
min-height: initial;
}
.onboarding-body {
background-color: var(--body-bg);
position: fixed;
margin-top: 50px;
bottom: 0;
height: 100vh;
width: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
line-height: 200%;
z-index: 11;
margin: 0;
background-image: url('/assets/onboarding-background-white.png');
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
/* Center the image */
background-position: center;
/* Do not repeat the image */
background-repeat: no-repeat;
// modal box card
.onboarding-main {
@extend .crayons-modal;
@extend .crayons-modal--l;
@extend .crayons-modal--padding-0;
border: none;
z-index: 1;
@media screen and (min-width: 600px) {
border-width: 2px;
min-height: auto;
}
a {
color: var(--link-brand-color);
}
}
.sticker-logo {
height: 80px;
width: 80px;
transform: rotate(-5deg);
padding: 10px;
vertical-align: middle;
top: -10px;
padding-top: 5px;
}
// modal inner content spacing
.onboarding-content {
box-sizing: border-box;
flex: 1 0 auto;
padding: $su-4;
width: 100%;
.onboarding-main {
display: flex;
flex-direction: column;
width: 800px;
height: 466px;
@include themeable(background-color, theme-container-background, $white);
border: 8px solid #6b55c9;
@include themeable(border-color, theme-container-border, #6b55c9);
padding: 30px;
border-radius: 3px;
z-index: 1;
font-size: 20px;
box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.3), 8px 0px 13px rgba(0, 0, 0, 0.21);
align-items: center;
@media screen and (min-width: $breakpoint-s) {
justify-content: center;
padding: $su-7 $su-8 $su-8;
}
}
.onboarding-content {
flex: 1 0 auto;
width: 100%;
margin-top: 5px;
// window background
.onboarding-body {
-moz-background-size: cover;
-o-background-size: cover;
-webkit-background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
display: flex;
height: 100vh;
justify-content: start;
line-height: 200%;
margin: 0;
overflow-y: scroll;
position: fixed;
top: 0;
width: 100%;
h1 {
margin-top: 15px;
line-height: 1.1em;
}
@media screen and (min-width: 800px) {
height: 440px;
}
}
.onboarding-navigation {
align-self: flex-end;
flex-shrink: 0;
width: 100%;
}
.background-image {
z-index: -1;
position: absolute;
top: 60px;
left: 0px;
}
h1 {
text-align: center;
}
h2 {
margin-top: 20px;
font-size: 15px;
@media screen and (min-width: 600px) {
font-size: 26px;
}
}
p {
line-height: 1.25em;
}
button {
width: 130px;
padding: 6px 0px;
height: auto;
margin: 11px 20px;
text-align: center;
font-weight: bold;
border-radius: 3px;
border: 2px solid #0a0a0a;
@include themeable(border-color, theme-color, #0a0a0a);
@include themeable(color, theme-color, #0a0a0a);
@include themeable(
background-color,
theme-container-accent-background,
$light-gray
);
font-family: 'HelveticaNeue-CondensedBold', 'HelveticaNeueBoldCondensed',
'HelveticaNeue-Bold-Condensed', 'Helvetica Neue Bold Condensed',
'HelveticaNeueBold', 'HelveticaNeue-Bold', 'Helvetica Neue Bold',
'HelveticaNeue', 'Helvetica Neue', 'TeXGyreHerosCnBold', 'Helvetica',
'Tahoma', 'Geneva', 'Arial Narrow', 'Arial', sans-serif;
-webkit-appearance: none;
font-stretch: condensed;
font-size: 26px;
}
.next-button {
color: var(--button-primary-color);
background: var(--button-primary-bg);
float: right;
}
.about {
width: 100%;
label,
input,
textarea {
display: block;
width: 95%;
margin: 0 auto;
}
input,
textarea {
padding: 12px;
font-size: 19px;
text-emphasis: bold;
border: 1px solid #dee6e9;
border-radius: 3px;
}
textarea {
height: 120px;
}
}
.checkbox-slide {
label {
display: block;
cursor: pointer;
}
input[type='checkbox'] {
-webkit-appearance: none;
width: 19px;
height: 19px;
background: transparent;
border-radius: 5px;
border: 2px solid #555;
vertical-align: middle;
margin: 8px;
cursor: pointer;
}
input[type='checkbox']:checked {
background: var(--link-brand-color);
}
}
.warning-message {
background-color: red;
opacity: 0.7;
padding: 10px;
width: 100%;
border-radius: 3px;
color: white;
}
.scroll {
height: 365px;
overflow: auto;
display: flex;
flex-direction: row;
flex-wrap: wrap;
border-bottom: 1px solid $light-medium-gray;
}
.tag {
padding: 44px 6px 46px;
font-size: 20px;
margin: 5px;
border-radius: 3px;
font-weight: bold;
border: none;
outline: none;
@media screen and (min-width: 600px) {
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
text-overflow: ellipsis;
width: 100%;
opacity: 0.8;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
@media screen and (min-width: 600px) {
width: 255px;
}
&.tag-selected {
opacity: 1;
}
&:hover {
opacity: 1;
}
.onboarding-tag-follow-indicator {
position: absolute;
bottom: 22px;
left: 0px;
right: 0px;
text-align: center;
font-size: 0.66em;
}
background-image: url('/assets/onboarding-background-white.png');
}
.select-all-button-wrapper {
width: 100%;
button {
width: 300px;
line-height: 1em;
text-align: left;
padding-left: 15px;
}
}
.user {
width: calc(100% - 40px);
overflow: hidden;
text-overflow: ellipsis;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
font-size: 15px;
padding: 5px;
border-radius: 3px;
text-align: center;
margin-right: 5px;
position: relative;
color: $black;
.onboarding-user-follow-status {
position: absolute;
color: #32a99c;
right: 8px;
top: 5px;
font-size: 0.88em;
}
span {
display: block;
width: 100%;
}
p {
font-weight: 400;
height: 55px;
}
@media screen and (min-width: 600px) {
width: 365px;
}
img {
height: 80px;
width: 80px;
border-radius: 50%;
padding: 5px;
margin: 10px auto;
display: block;
}
}
.user:active {
background-color: red;
}
.badge-image {
&:before {
display: block;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.65);
height: 100%;
width: 100%;
}
}
.onboarding-what-next {
// modal navigation
.onboarding-navigation {
align-self: flex-end;
flex-shrink: 0;
width: 100%;
border-bottom: 2px solid var(--box-darker);
.navigation-content {
display: flex;
flex-direction: column;
height: 250px;
overflow: auto;
@media screen and (min-width: 600px) {
flex-direction: row;
}
a {
background: #d9e7ff;
padding: 15px 4px 0px 12px;
height: 140px;
margin: 4px auto;
border-radius: 3px;
width: 350px;
display: block;
line-height: 1.2em;
font-weight: bold;
position: relative;
text-align: center;
@media screen and (min-width: 600px) {
height: 220px;
margin: 4px 4px;
text-align: left;
padding: 20px 4px 0px 12px;
}
.whatnext-emoji {
display: none;
text-align: center;
position: absolute;
bottom: 22px;
left: 0;
right: 0;
font-size: 50px;
@media screen and (min-width: 600px) {
display: block;
}
}
}
}
.onboarding-previous-location {
text-align: center;
margin-top: 10px;
font-weight: bold;
display: none;
@media screen and (min-width: 600px) {
display: block;
}
code {
font-size: 0.6em;
margin-top: -10px;
display: block;
font-weight: 400;
}
}
@media screen and (max-width: 800px) {
.onboarding-main {
width: 100%;
height: 100%;
background-image: url('/assets/mobile-onboarding-background.png');
background-size: cover;
/* Center the image */
background-position: center;
/* Do not repeat the image */
background-repeat: no-repeat;
border: none;
color: $black;
}
.onboarding-content {
flex: 0 0 auto;
h1,
h2 {
margin-top: initial;
}
}
span {
display: block;
}
}
@media screen and (max-width: 650px) {
.onboarding-main {
font-size: 18px;
}
justify-content: space-between;
padding: $su-4;
button {
width: 100px;
font-size: 20px;
}
}
@media screen and (max-width: 450px) {
.onboarding-main {
font-size: 16px;
}
button {
width: 100px;
font-size: 18px;
}
}
@media screen and (max-width: 330px) {
button {
width: 80px;
font-size: 14px;
}
.sticker-logo {
height: 40px;
width: 40px;
}
h1 {
font-size: 20px;
@extend .crayons-btn;
}
}
}
// modal header
.onboarding-content-header {
margin-bottom: $su-4;
@media screen and (min-width: $breakpoint-s) {
margin-bottom: $su-6;
}
.title,
.subtitle {
margin: 0;
}
.title {
font-weight: $fw-heavy;
margin-bottom: $su-1;
text-align: left;
}
.subtitle {
color: var(--container-color-secondary);
font-size: $fs-xl;
font-weight: $fw-medium;
letter-spacing: normal;
line-height: $lh-base;
}
}
// modal selection status
.onboarding-selection-status {
align-self: flex-end;
flex-shrink: 0;
width: 100%;
border-top: 2px solid var(--box-darker);
.selection-status-content {
display: block;
padding: $su-4;
text-align: center;
}
}
// override crayons notice
.onboarding-body {
.crayons-notice {
display: block;
margin-bottom: $su-2;
}
}
// modal overflow scrolling
.onboarding-modal-scroll-container {
height: auto;
@media screen and (min-width: $breakpoint-s) {
max-height: 500px;
overflow-y: scroll;
position: relative;
}
}
// tag styles
.onboarding-tags {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: $su-4;
width: 100%;
@media screen and (min-width: 800px) {
grid-template-columns: repeat(3, 1fr);
}
&__button {
@extend .crayons-btn;
border: 0;
display: block;
position: absolute;
left: $su-4;
right: $su-4;
bottom: $su-4;
width: calc(100% - 32px);
transform: translateY(0px);
font-family: $ff-default;
$button: &;
@media screen and (min-width: 800px) {
transform: translateY(60px);
}
&-default {
display: inline-block;
}
&-alt {
display: none;
}
}
&__item {
border-radius: $br-default;
position: relative;
overflow: hidden;
height: 150px;
$item: &;
&__inner {
background: var(--base-10);
bottom: 0;
font-family: $ff-accent;
hyphens: auto;
left: 0;
padding: $su-4;
position: absolute;
right: 0;
top: 0;
transition: all ease-in-out 0.1s;
z-index: 2;
}
&:hover {
#{$item}__inner {
background: transparent;
}
.onboarding-tags__button {
transform: translateY(0);
&:hover {
transform: translateY(-2px);
}
}
}
&--selected {
#{$item}__inner {
background: transparent;
}
input[type='checkbox']:checked {
background: var(--link-brand-color);
}
}
}
}
// Onboarding Intro
.onboarding-main.introduction {
width: 578px;
.onboarding-content {
background: var(--base-100);
color: white;
display: flex;
flex-direction: column;
justify-content: start;
padding: $su-8;
@media screen and (min-width: $breakpoint-s) {
justify-content: center;
}
}
figure {
margin: 0 0 $su-2 0;
.sticker-logo {
height: $su-8;
width: $su-8;
transform: rotate(-30deg);
padding: $su-3;
vertical-align: middle;
}
}
.introduction-title,
.introduction-subtitle {
margin: 0;
}
.introduction-title {
font-size: $fs-5xl;
font-weight: $fw-heavy;
line-height: $lh-tight;
}
.introduction-subtitle {
font-size: $fs-2xl;
font-weight: $fw-normal;
line-height: $lh-base;
}
.navigation-content {
padding: $su-7;
justify-content: center;
button {
@extend .crayons-btn;
}
}
}
// follow users slide
$onboarding-user-unselected-hover: rgba(71, 85, 235, 0.05);
$onboarding-user-selected: rgba(71, 85, 235, 0.1);
$onboarding-user-selected-hover: rgba(71, 85, 235, 0.2);
.onboarding-body {
.content-row {
align-items: center;
border: none;
box-sizing: border-box;
color: var(--body-color);
display: flex;
padding: $su-2 $su-4;
text-align: left;
width: 100%;
&.unselected {
&:hover {
background-color: $onboarding-user-unselected-hover;
cursor: pointer;
}
button {
@extend .crayons-btn;
@extend .crayons-btn--outlined;
}
}
&.selected {
background-color: $onboarding-user-selected;
&:hover {
background-color: $onboarding-user-selected-hover;
cursor: pointer;
}
button {
@extend .crayons-btn;
}
}
}
.user-avatar-container {
align-self: baseline;
margin: 0 $su-4 0 0;
@media screen and (min-width: $breakpoint-s) {
align-self: center;
}
}
.user-avatar {
border-radius: 100%;
height: $su-8;
width: $su-8;
object-fit: cover;
border: 1px solid var(--base-10);
}
.user-name,
.user-summary {
margin: 0;
line-height: $lh-tight;
}
.user-name {
font-size: $fs-base;
}
.user-summary {
font-size: $fs-s;
}
.user-following-status {
margin-left: $su-7;
}
}
// closing slide
.onboarding-what-next {
display: flex;
flex-direction: column;
overflow: auto;
@media screen and (min-width: $breakpoint-s) {
flex-direction: column;
}
p {
align-items: center;
display: inline-flex;
}
a {
background: #d9e7ff;
margin: 4px auto;
border-radius: 3px;
display: block;
font-weight: 800;
width: 100%;
padding: 0.25em;
position: relative;
text-align: center;
.whatnext-emoji {
text-align: center;
font-size: 32px;
margin-right: 8px;
}
}
}
.onboarding-previous-location {
text-align: center;
margin-top: 10px;
font-weight: bold;
display: none;
@media screen and (min-width: 600px) {
display: block;
}
code {
font-size: 0.6em;
margin-top: -10px;
display: block;
font-weight: 400;
}
}
// Follow user
.onboarding-body {
.content-row {
align-items: center;
color: #202428;
display: flex;
padding: 8px 16px;
text-align: left;
width: 100%;
&:hover {
background-color: #eef0f1 !important;
}
}
.user-avatar-container {
margin: 0 16px 0 0;
}
.user-avatar {
border-radius: 100%;
height: 48px;
width: 48px;
object-fit: cover;
border: 1px solid #eef0f1;
}
.user-name,
.user-summary {
margin: 0;
}
.user-following-status {
margin-left: 32px;
}
}
// Forms
.onboarding-main {
fieldset {
padding: 0;
border: none;
margin-bottom: $su-6;
&:last-child {
margin-bottom: 0;
}
ul {
list-style-type: none;
margin: 0;
padding-left: 0;
}
}
legend {
color: var(--body-color);
font-weight: $fw-medium;
}
input[type='text'],
textarea {
@extend .crayons-textfield;
box-sizing: border-box;
}
textarea {
min-height: 120px;
}
label {
@extend .crayons-field;
@extend .crayons-field__label;
margin-bottom: $su-4;
&:last-child {
margin-bottom: 0;
}
}
// Checkboxes
.checkbox-item {
label {
@extend .crayons-field__label;
align-items: baseline;
border-radius: $br-default;
display: inline-block;
flex-direction: row;
padding: 0 $su-1;
width: 100%;
@media screen and (min-width: $breakpoint-s) {
align-items: center;
}
&:hover {
background: var(--body-bg);
cursor: pointer;
}
&:last-child {
margin-bottom: 0;
}
a {
display: contents;
}
input[type='checkbox'] {
@extend .crayons-checkbox;
margin-right: $su-2 !important;
}
}
}
}

View file

@ -13,7 +13,7 @@ import FollowUsers from '../components/FollowUsers';
global.fetch = fetch;
function flushPromises() {
return new Promise(resolve => setImmediate(resolve));
return new Promise((resolve) => setImmediate(resolve));
}
function initializeSlides(currentSlide, dataUser = null, mockData = null) {
@ -284,18 +284,19 @@ describe('<Onboarding />', () => {
});
test('should render three tags', async () => {
expect(onboardingSlides.find('.tag').length).toBe(3);
expect(onboardingSlides.find('.onboarding-tags__item').length).toBe(3);
});
test('should allow a user to add a tag', async () => {
fetch.once({});
const followTags = onboardingSlides.find(<FollowTags />);
const firstButton = onboardingSlides
.find('.onboarding-tags__button')
.first();
onboardingSlides
.find('.tag')
.first()
.simulate('click');
firstButton.simulate('click');
expect(followTags.state('selectedTags').length).toBe(1);
onboardingSlides.find('.next-button').simulate('click');
fetch.once(fakeUsersResponse);
await flushPromises();
@ -328,10 +329,7 @@ describe('<Onboarding />', () => {
fetch.once({});
const followUsers = onboardingSlides.find(<FollowUsers />);
onboardingSlides
.find('.user')
.first()
.simulate('click');
onboardingSlides.find('.user').first().simulate('click');
expect(followUsers.state('selectedUsers').length).toBe(2);
onboardingSlides.find('.next-button').simulate('click');
await flushPromises();

View file

@ -5,13 +5,37 @@ preact-render-spy (1 nodes)
-------
<main class="onboarding-body">
<div class="onboarding-main">
<nav class="onboarding-navigation">
<div class="navigation-content">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
Back
</button>
<button
onClick={[Function bound onSubmit]}
class="next-button"
type="button"
>
Continue
</button>
</div>
</nav>
<div class="onboarding-content about">
<h2>About You!</h2>
<header class="onboarding-content-header">
<h1 class="title">Introduce yourself</h1>
<h2 class="subtitle">
Tell the community about yourself. Write a quick bio about what you do, what you're interested in, or anything else.
</h2>
</header>
<form>
<label htmlFor="summary">
Tell the community about yourself! Write a quick bio about what you do, what you're interested in, or anything else!
Bio
<textarea
name="summary"
placeholder="Tell us about yourself"
onChange={[Function bound handleChange]}
maxLength="120"
>
@ -19,22 +43,6 @@ preact-render-spy (1 nodes)
</label>
</form>
</div>
<nav class="onboarding-navigation">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
BACK
</button>
<button
onClick={[Function bound onSubmit]}
class="next-button"
type="button"
>
Continue
</button>
</nav>
</div>
</main>
@ -46,62 +54,68 @@ preact-render-spy (1 nodes)
<main class="onboarding-body">
<div class="onboarding-main">
<div class="onboarding-content">
<h1>
Youre part of the community!
<span
role="img"
aria-label="tada"
>
🎉
</span>
</h1>
<h2 style="text-align: center;">What next?</h2>
<header class="onboarding-content-header">
<h1 class="title">
Youre a part of the community!
<span
role="img"
aria-label="tada"
>
🎉
</span>
</h1>
<h2 class="subtitle">What next?</h2>
</header>
<div class="onboarding-what-next">
<a
href="/welcome"
data-no-instant={true}
>
Join the Welcome Thread
<p class="whatnext-emoji">
<p>
<span
class="whatnext-emoji"
role="img"
aria-label="tada"
>
😊
</span>
Join the Welcome Thread
</p>
</a>
<a href="/new">
Write your first DEV post
<p class="whatnext-emoji">
<p>
<span
class="whatnext-emoji"
role="img"
aria-label="tada"
>
✍️
</span>
Write your first DEV post
</p>
</a>
<a href="/top/infinity">
Read all-time top posts
<p class="whatnext-emoji">
<p>
<span
class="whatnext-emoji"
role="img"
aria-label="tada"
>
🤓
</span>
Read all-time top posts
</p>
</a>
<a href="/settings">
Customize your profile
<p class="whatnext-emoji">
<p>
<span
class="whatnext-emoji"
role="img"
aria-label="tada"
>
💅
</span>
Customize your profile
</p>
</a>
</div>
@ -116,82 +130,104 @@ preact-render-spy (1 nodes)
-------
<main class="onboarding-body">
<div class="onboarding-main">
<nav class="onboarding-navigation">
<div class="navigation-content">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
Back
</button>
<button
onClick={[Function bound onSubmit]}
class="next-button"
type="button"
>
Continue
</button>
</div>
</nav>
<div class="onboarding-content checkbox-slide">
<h2>Some things to check off!</h2>
<header class="onboarding-content-header">
<h1 class="title">Getting started</h1>
<h2 class="subtitle">Let's review a few things first</h2>
</header>
<form>
<label htmlFor="checked_code_of_conduct">
<input
type="checkbox"
id="checked_code_of_conduct"
name="checked_code_of_conduct"
checked={false}
onChange={[Function bound handleChange]}
/>
You agree to uphold our
<a
href="/code-of-conduct"
data-no-instant={true}
onClick={[Function onClick]}
>
Code of Conduct
</a>
</label>
<label htmlFor="checked_terms_and_conditions">
<input
type="checkbox"
id="checked_terms_and_conditions"
name="checked_terms_and_conditions"
checked={false}
onChange={[Function bound handleChange]}
/>
You agree to our
<a
href="/terms"
data-no-instant={true}
onClick={[Function onClick]}
>
Terms and Conditions
</a>
</label>
<h3>Email Preferences</h3>
<label htmlFor="email_newsletter">
<input
type="checkbox"
id="email_newsletter"
name="email_newsletter"
checked={true}
onChange={[Function bound handleChange]}
/>
Do you want to receive our weekly newsletter emails?
</label>
<label htmlFor="email_digest_periodic">
<input
type="checkbox"
id="email_digest_periodic"
name="email_digest_periodic"
checked={true}
onChange={[Function bound handleChange]}
/>
Do you want to receive a periodic digest with some of the top posts from your tags?
</label>
<fieldset>
<legend>Some things to check off</legend>
<ul>
<li class="checkbox-item">
<label htmlFor="checked_code_of_conduct">
<input
type="checkbox"
id="checked_code_of_conduct"
name="checked_code_of_conduct"
checked={false}
onChange={[Function bound handleChange]}
/>
I agree to uphold the
<a
href="/code-of-conduct"
data-no-instant={true}
onClick={[Function onClick]}
>
Code of Conduct
</a>
</label>
</li>
<li class="checkbox-item">
<label htmlFor="checked_terms_and_conditions">
<input
type="checkbox"
id="checked_terms_and_conditions"
name="checked_terms_and_conditions"
checked={false}
onChange={[Function bound handleChange]}
/>
I agree to our
<a
href="/terms"
data-no-instant={true}
onClick={[Function onClick]}
>
Terms and Conditions
</a>
</label>
</li>
</ul>
</fieldset>
<fieldset>
<legend>Email preferences</legend>
<ul>
<li class="checkbox-item">
<label htmlFor="email_newsletter">
<input
type="checkbox"
id="email_newsletter"
name="email_newsletter"
checked={true}
onChange={[Function bound handleChange]}
/>
I want to receive weekly newsletter emails.
</label>
</li>
<li class="checkbox-item">
<label htmlFor="email_digest_periodic">
<input
type="checkbox"
id="email_digest_periodic"
name="email_digest_periodic"
checked={true}
onChange={[Function bound handleChange]}
/>
I want to receive a periodic digest with some of the top posts from your tags.
</label>
</li>
</ul>
</fieldset>
</form>
</div>
<nav class="onboarding-navigation">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
BACK
</button>
<button
onClick={[Function bound onSubmit]}
class="next-button"
type="button"
>
Continue
</button>
</nav>
</div>
</main>
@ -202,54 +238,82 @@ preact-render-spy (1 nodes)
-------
<main class="onboarding-body">
<div class="onboarding-main">
<div class="onboarding-content">
<h2>Follow tags to customize your feed</h2>
<div class="scroll">
<nav class="onboarding-navigation">
<div class="navigation-content">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
onClick={[Function onClick]}
style="background-color: #000000; color: #ffffff;"
class="tag"
>
#discuss
<div class="onboarding-tag-follow-indicator"></div>
Back
</button>
<button
onClick={[Function bound handleComplete]}
class="next-button"
type="button"
onClick={[Function onClick]}
style="background-color: #f7df1e; color: #000000;"
class="tag"
>
#javascript
<div class="onboarding-tag-follow-indicator"></div>
</button>
<button
type="button"
onClick={[Function onClick]}
style="background-color: #2a2566; color: #ffffff;"
class="tag"
>
#career
<div class="onboarding-tag-follow-indicator"></div>
Continue
</button>
</div>
</div>
<nav class="onboarding-navigation">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
BACK
</button>
<button
onClick={[Function bound handleComplete]}
class="next-button"
type="button"
>
Continue
</button>
</nav>
<div class="onboarding-content">
<header class="onboarding-content-header">
<h1 class="title">What are you interested in?</h1>
<h2 class="subtitle">Follow tags to customize your feed</h2>
</header>
<div class="onboarding-modal-scroll-container">
<div class="onboarding-tags">
<div
class="onboarding-tags__item false"
style="box-shadow: inset 0 0 0 2px #000000; color: ;"
>
<div class="onboarding-tags__item__inner">
#discuss
<button
type="button"
onClick={[Function onClick]}
class="onboarding-tags__button false"
style="background-color: #000000; color: #ffffff;"
>
+ Follow
</button>
</div>
</div>
<div
class="onboarding-tags__item false"
style="box-shadow: inset 0 0 0 2px #f7df1e; color: ;"
>
<div class="onboarding-tags__item__inner">
#javascript
<button
type="button"
onClick={[Function onClick]}
class="onboarding-tags__button false"
style="background-color: #f7df1e; color: #000000;"
>
+ Follow
</button>
</div>
</div>
<div
class="onboarding-tags__item false"
style="box-shadow: inset 0 0 0 2px #2a2566; color: ;"
>
<div class="onboarding-tags__item__inner">
#career
<button
type="button"
onClick={[Function onClick]}
class="onboarding-tags__button false"
style="background-color: #2a2566; color: #ffffff;"
>
+ Follow
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
@ -260,79 +324,114 @@ preact-render-spy (1 nodes)
-------
<main class="onboarding-body">
<div class="onboarding-main">
<nav class="onboarding-navigation">
<div class="navigation-content">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
Back
</button>
<button
onClick={[Function bound handleComplete]}
class="next-button"
type="button"
>
Continue
</button>
</div>
</nav>
<div class="onboarding-content">
<h2>
Ok, here are some people we picked for you
</h2>
<div class="scroll">
<div class="select-all-button-wrapper">
<header class="onboarding-content-header">
<h1 class="title">Suggested people to follow</h1>
<h2 class="subtitle">Let's review a few things first</h2>
</header>
<div class="onboarding-modal-scroll-container">
<button
type="button"
onClick={[Function onClick]}
onKeyDown={[Function onKeyDown]}
class="user content-row selected"
>
<figure class="user-avatar-container">
<img
class="user-avatar"
src="ben.jpg"
alt="profile"
/>
</figure>
<div class="user-info">
<h4 class="user-name">Ben Halpern</h4>
<p class="user-summary"></p>
</div>
<button
type="button"
onClick={[Function onClick]}
class="user-following-status"
>
Select All ✅
Following
</button>
</div>
<button
type="button"
style="background-color: #c7ffe8;"
onClick={[Function onClick]}
class="user"
>
<div class="onboarding-user-follow-status">selected</div>
<img
src="ben.jpg"
alt=""
/>
<span>Ben Halpern</span>
<p></p>
</button>
<button
type="button"
style="background-color: #c7ffe8;"
onClick={[Function onClick]}
class="user"
onKeyDown={[Function onKeyDown]}
class="user content-row selected"
>
<div class="onboarding-user-follow-status">selected</div>
<img
src="clown.jpg"
alt=""
/>
<span>Krusty the Clown</span>
<p></p>
<figure class="user-avatar-container">
<img
class="user-avatar"
src="clown.jpg"
alt="profile"
/>
</figure>
<div class="user-info">
<h4 class="user-name">Krusty the Clown</h4>
<p class="user-summary"></p>
</div>
<button
type="button"
class="user-following-status"
>
Following
</button>
</button>
<button
type="button"
style="background-color: #c7ffe8;"
onClick={[Function onClick]}
class="user"
onKeyDown={[Function onKeyDown]}
class="user content-row selected"
>
<div class="onboarding-user-follow-status">selected</div>
<img
src="dev.jpg"
alt=""
/>
<span>dev.to staff</span>
<p></p>
<figure class="user-avatar-container">
<img
class="user-avatar"
src="dev.jpg"
alt="profile"
/>
</figure>
<div class="user-info">
<h4 class="user-name">dev.to staff</h4>
<p class="user-summary"></p>
</div>
<button
type="button"
class="user-following-status"
>
Following
</button>
</button>
</div>
</div>
<div class="onboarding-selection-status">
<div class="selection-status-content">
<button
type="button"
onClick={[Function onClick]}
>
Select All ✅
</button>
</div>
</div>
<nav class="onboarding-navigation">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
BACK
</button>
<button
onClick={[Function bound handleComplete]}
class="next-button"
type="button"
>
Continue
</button>
</nav>
</div>
</main>
@ -342,51 +441,30 @@ exports[`<Onboarding /> IntroSlide renders properly 1`] = `
preact-render-spy (1 nodes)
-------
<main class="onboarding-body">
<div class="onboarding-main">
<div class="onboarding-main introduction">
<div class="onboarding-content">
<h1>
<span>Welcome to the </span>
<figure>
<img
src="/assets/purple-dev-logo.png"
class="sticker-logo"
alt="DEV"
/>
<span>community!</span>
</h1>
<div>
<p>
DEV is where programmers share ideas and help each other grow.
<span
role="img"
aria-label="Nerd Face"
>
🤓
</span>
</p>
<p>
Ask questions, leave helpful comments, encourage others, and have fun!
<span
role="img"
aria-label="Raising Hands"
>
🙌
</span>
</p>
<p>
A few
<strong>quick questions</strong>
for you before you get started...
</p>
</div>
</figure>
<h1 class="introduction-title">Welcome to DEV!</h1>
<h2 class="introduction-subtitle">
DEV is where programmers share ideas and help each other grow.
</h2>
</div>
<nav class="onboarding-navigation">
<button
onClick={[Function bound onSubmit]}
class="next-button"
type="button"
>
Continue
</button>
<div class="navigation-content">
<button
onClick={[Function bound onSubmit]}
class="next-button"
type="button"
>
Continue
</button>
</div>
</nav>
</div>
</main>
@ -398,8 +476,28 @@ preact-render-spy (1 nodes)
-------
<main class="onboarding-body">
<div class="onboarding-main">
<div class="onboarding-content about">
<h2>About You!</h2>
<nav class="onboarding-navigation">
<div class="navigation-content">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
Back
</button>
<button
onClick={[Function bound onSubmit]}
class="next-button"
type="button"
>
Continue
</button>
</div>
</nav>
<div class="onboarding-content">
<header class="onboarding-content-header">
<h1 class="title">About You</h1>
</header>
<form>
<label htmlFor="location">
Where are you located?
@ -407,6 +505,7 @@ preact-render-spy (1 nodes)
type="text"
name="location"
id="location"
placeholder="e.g. New York, NY"
onChange={[Function bound handleChange]}
maxLength="60"
/>
@ -417,6 +516,7 @@ preact-render-spy (1 nodes)
type="text"
name="employment_title"
id="employment_title"
placeholder="e.g. Software Engineer"
onChange={[Function bound handleChange]}
maxLength="60"
/>
@ -427,28 +527,13 @@ preact-render-spy (1 nodes)
type="text"
name="employer_name"
id="employer_name"
placeholder="e.g. Company name, self-employed, etc."
onChange={[Function bound handleChange]}
maxLength="60"
/>
</label>
</form>
</div>
<nav class="onboarding-navigation">
<button
onClick={[Function bound prevSlide]}
class="back-button"
type="button"
>
BACK
</button>
<button
onClick={[Function bound onSubmit]}
class="next-button"
type="button"
>
Continue
</button>
</nav>
</div>
</main>

View file

@ -51,21 +51,27 @@ class BioForm extends Component {
const { prev } = this.props;
return (
<div className="onboarding-main">
<Navigation prev={prev} next={this.onSubmit} />
<div className="onboarding-content about">
<h2>About You!</h2>
<header className="onboarding-content-header">
<h1 className="title">Introduce yourself</h1>
<h2 className="subtitle">
Tell the community about yourself. Write a quick bio about what
you do, what you&apos;re interested in, or anything else.
</h2>
</header>
<form>
<label htmlFor="summary">
Tell the community about yourself! Write a quick bio about what
you do, what you&apos;re interested in, or anything else!
Bio
<textarea
name="summary"
placeholder="Tell us about yourself"
onChange={this.handleChange}
maxLength="120"
/>
</label>
</form>
</div>
<Navigation prev={prev} next={this.onSubmit} />
</div>
);
}

View file

@ -27,7 +27,11 @@ class ClosingSlide extends Component {
</div>
);
}
if (previousLocation !== 'none' && previousLocation !== null && !previousLocation.startsWith('javascript')) {
if (
previousLocation !== 'none' &&
previousLocation !== null &&
!previousLocation.startsWith('javascript')
) {
return (
<a className="onboarding-previous-location" href={previousLocation}>
<div>Or go back to the page you were on before you signed up</div>
@ -119,35 +123,35 @@ class ClosingSlide extends Component {
return (
<div className="onboarding-what-next">
<a href="/welcome" data-no-instant>
Join the Welcome Thread
<p className="whatnext-emoji">
<span role="img" aria-label="tada">
<p>
<span className="whatnext-emoji" role="img" aria-label="tada">
😊
</span>
Join the Welcome Thread
</p>
</a>
<a href="/new">
Write your first DEV post
<p className="whatnext-emoji">
<span role="img" aria-label="tada">
<p>
<span className="whatnext-emoji" role="img" aria-label="tada">
</span>
Write your first DEV post
</p>
</a>
<a href="/top/infinity">
Read all-time top posts
<p className="whatnext-emoji">
<span role="img" aria-label="tada">
<p>
<span className="whatnext-emoji" role="img" aria-label="tada">
🤓
</span>
Read all-time top posts
</p>
</a>
<a href="/settings">
Customize your profile
<p className="whatnext-emoji">
<span role="img" aria-label="tada">
<p>
<span className="whatnext-emoji" role="img" aria-label="tada">
💅
</span>
Customize your profile
</p>
</a>
</div>
@ -157,14 +161,17 @@ class ClosingSlide extends Component {
return (
<div className="onboarding-main">
<div className="onboarding-content">
<h1>
You&lsquo;re part of the community!
<span role="img" aria-label="tada">
{' '}
🎉
</span>
</h1>
<h2 style={{ textAlign: 'center' }}>What next?</h2>
<header className="onboarding-content-header">
<h1 className="title">
You&lsquo;re a part of the community!
<span role="img" aria-label="tada">
{' '}
🎉
</span>
</h1>
<h2 className="subtitle">What next?</h2>
</header>
{nextStepLinks()}
{previousLocationListElement()}
</div>

View file

@ -58,14 +58,14 @@ class EmailTermsConditionsForm extends Component {
} = this.state;
if (!checked_code_of_conduct) {
this.setState({
message: 'You must agree to our Code of Conduct before continuing!',
message: 'You must agree to our Code of Conduct before continuing.',
});
return false;
}
if (!checked_terms_and_conditions) {
this.setState({
message:
'You must agree to our Terms and Conditions before continuing!',
'You must agree to our Terms and Conditions before continuing.',
});
return false;
}
@ -103,7 +103,7 @@ class EmailTermsConditionsForm extends Component {
<div className="onboarding-main">
<div className="onboarding-content checkbox-slide">
<button type="button" onClick={() => this.backToSlide()}>
BACK
Back
</button>
<div
/* eslint-disable react/no-danger */
@ -117,72 +117,99 @@ class EmailTermsConditionsForm extends Component {
}
return (
<div className="onboarding-main">
<Navigation prev={prev} next={this.onSubmit} />
<div className="onboarding-content checkbox-slide">
<h2>Some things to check off!</h2>
{message && <span className="warning-message">{message}</span>}
<form>
<label htmlFor="checked_code_of_conduct">
<input
type="checkbox"
id="checked_code_of_conduct"
name="checked_code_of_conduct"
checked={checked_code_of_conduct}
onChange={this.handleChange}
/>
You agree to uphold our
{' '}
<a
href="/code-of-conduct"
data-no-instant
onClick={e => this.handleShowText(e, 'coc')}
>
Code of Conduct
</a>
</label>
<label htmlFor="checked_terms_and_conditions">
<input
type="checkbox"
id="checked_terms_and_conditions"
name="checked_terms_and_conditions"
checked={checked_terms_and_conditions}
onChange={this.handleChange}
/>
You agree to our
{' '}
<a
href="/terms"
data-no-instant
onClick={e => this.handleShowText(e, 'terms')}
>
Terms and Conditions
</a>
</label>
<h3>Email Preferences</h3>
<label htmlFor="email_newsletter">
<input
type="checkbox"
id="email_newsletter"
name="email_newsletter"
checked={email_newsletter}
onChange={this.handleChange}
/>
Do you want to receive our weekly newsletter emails?
</label>
<header className="onboarding-content-header">
<h1 className="title">Getting started</h1>
<h2 className="subtitle">Let&apos;s review a few things first</h2>
</header>
<label htmlFor="email_digest_periodic">
<input
type="checkbox"
id="email_digest_periodic"
name="email_digest_periodic"
checked={email_digest_periodic}
onChange={this.handleChange}
/>
Do you want to receive a periodic digest with some of the top
posts from your tags?
</label>
{message && (
<span className="crayons-notice crayons-notice--danger">
{message}
</span>
)}
<form>
<fieldset>
<legend>Some things to check off</legend>
<ul>
<li className="checkbox-item">
<label htmlFor="checked_code_of_conduct">
<input
type="checkbox"
id="checked_code_of_conduct"
name="checked_code_of_conduct"
checked={checked_code_of_conduct}
onChange={this.handleChange}
/>
I agree to uphold the
{' '}
<a
href="/code-of-conduct"
data-no-instant
onClick={e => this.handleShowText(e, 'coc')}
>
Code of Conduct
</a>
</label>
</li>
<li className="checkbox-item">
<label htmlFor="checked_terms_and_conditions">
<input
type="checkbox"
id="checked_terms_and_conditions"
name="checked_terms_and_conditions"
checked={checked_terms_and_conditions}
onChange={this.handleChange}
/>
I agree to our
{' '}
<a
href="/terms"
data-no-instant
onClick={e => this.handleShowText(e, 'terms')}
>
Terms and Conditions
</a>
</label>
</li>
</ul>
</fieldset>
<fieldset>
<legend>Email preferences</legend>
<ul>
<li className="checkbox-item">
<label htmlFor="email_newsletter">
<input
type="checkbox"
id="email_newsletter"
name="email_newsletter"
checked={email_newsletter}
onChange={this.handleChange}
/>
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 with some of the top
posts from your tags.
</label>
</li>
</ul>
</fieldset>
</form>
</div>
<Navigation prev={prev} next={this.onSubmit} />
</div>
);
}

View file

@ -85,31 +85,63 @@ class FollowTags extends Component {
const { selectedTags, allTags } = this.state;
return (
<div className="onboarding-main">
<Navigation prev={prev} next={this.handleComplete} />
<div className="onboarding-content">
<h2>Follow tags to customize your feed</h2>
<div className="scroll">
{allTags.map(tag => (
<button
type="button"
onClick={() => this.handleClick(tag)}
style={{
backgroundColor: tag.bg_color_hex,
color: tag.text_color_hex,
}}
className={
selectedTags.includes(tag) ? 'tag tag-selected' : 'tag'
}
>
#
{tag.name}
<div className="onboarding-tag-follow-indicator">
{selectedTags.includes(tag) ? '✓ following ' : ''}
<header className="onboarding-content-header">
<h1 className="title">What are you interested in?</h1>
<h2 className="subtitle">Follow tags to customize your feed</h2>
</header>
<div className="onboarding-modal-scroll-container">
<div className="onboarding-tags">
{allTags.map(tag => (
<div
className={`onboarding-tags__item ${selectedTags.includes(
tag,
) && 'onboarding-tags__item--selected'}`}
style={{
boxShadow: selectedTags.includes(tag)
? `inset 0 0 0 100px ${tag.bg_color_hex}`
: `inset 0 0 0 2px ${tag.bg_color_hex}`,
color: selectedTags.includes(tag) ? tag.text_color_hex : '',
}}
>
<div className="onboarding-tags__item__inner">
#
{tag.name}
<button
type="button"
onClick={() => this.handleClick(tag)}
className={`onboarding-tags__button ${selectedTags.includes(
tag,
) && 'onboarding-tags__button--selected'}`}
style={{
backgroundColor: selectedTags.includes(tag)
? tag.text_color_hex
: tag.bg_color_hex,
color: selectedTags.includes(tag)
? tag.bg_color_hex
: tag.text_color_hex,
}}
>
{selectedTags.includes(tag) ? (
<span>
<span className="onboarding-tags__button-default">
Following
</span>
<span className="onboarding-tags__button-alt">
Unfollow
</span>
</span>
) : (
'+ Follow'
)}
</button>
</div>
</div>
</button>
))}
))}
</div>
</div>
</div>
<Navigation prev={prev} next={this.handleComplete} />
</div>
);
}

View file

@ -97,38 +97,52 @@ class FollowUsers extends Component {
const { prev } = this.props;
return (
<div className="onboarding-main">
<Navigation prev={prev} next={this.handleComplete} />
<div className="onboarding-content">
<h2>Ok, here are some people we picked for you</h2>
<div className="scroll">
<div className="select-all-button-wrapper">
<button type="button" onClick={() => this.handleSelectAll()}>
Select All
{' '}
{selectedUsers.length === users.length ? '✅' : ''}
</button>
</div>
<header className="onboarding-content-header">
<h1 className="title">Suggested people to follow</h1>
<h2 className="subtitle">Let&apos;s review a few things first</h2>
</header>
<div className="onboarding-modal-scroll-container">
{users.map(user => (
<button
type="button"
style={{
backgroundColor: selectedUsers.includes(user)
? '#c7ffe8'
: 'white',
}}
onClick={() => this.handleClick(user)}
className="user"
onKeyDown={() => this.handleKeyDown(user)}
className={
selectedUsers.includes(user)
? 'user content-row selected'
: 'user content-row unselected'
}
>
<div className="onboarding-user-follow-status">
{selectedUsers.includes(user) ? 'selected' : ''}
<figure className="user-avatar-container">
<img
className="user-avatar"
src={user.profile_image_url}
alt="profile"
/>
</figure>
<div className="user-info">
<h4 className="user-name">{user.name}</h4>
<p className="user-summary">{user.summary}</p>
</div>
<img src={user.profile_image_url} alt="" />
<span>{user.name}</span>
<p>{user.summary}</p>
<button type="button" className="user-following-status">
{selectedUsers.includes(user) ? 'Following' : 'Follow'}
</button>
</button>
))}
</div>
</div>
<Navigation prev={prev} next={this.handleComplete} />
<div className="onboarding-selection-status">
<div className="selection-status-content">
<button type="button" onClick={() => this.handleSelectAll()}>
Select All
{' '}
{selectedUsers.length === users.length ? '✅' : ''}
</button>
</div>
</div>
</div>
);
}

View file

@ -78,22 +78,22 @@ class IntroSlide extends Component {
}
render() {
const { prev, variant } = this.props;
const onboardingBody = this.selectVariant(variant);
const { prev } = this.props;
return (
<div className="onboarding-main">
<div className="onboarding-main introduction">
<div className="onboarding-content">
<h1>
<span>Welcome to the </span>
<figure>
<img
src="/assets/purple-dev-logo.png"
className="sticker-logo"
alt="DEV"
/>
<span>community!</span>
</h1>
{onboardingBody}
</figure>
<h1 className="introduction-title">Welcome to DEV!</h1>
<h2 className="introduction-subtitle">
DEV is where programmers share ideas and help each other grow.
</h2>
</div>
<Navigation prev={prev} next={this.onSubmit} hidePrev />
</div>
@ -104,7 +104,6 @@ class IntroSlide extends Component {
IntroSlide.propTypes = {
prev: PropTypes.func.isRequired,
next: PropTypes.string.isRequired,
variant: PropTypes.string.isRequired,
};
export default IntroSlide;

View file

@ -3,16 +3,18 @@ import PropTypes from 'prop-types';
const Navigation = ({ next, prev, hideNext, hidePrev }) => (
<nav className="onboarding-navigation">
{!hidePrev && (
<button onClick={prev} className="back-button" type="button">
BACK
</button>
)}
{!hideNext && (
<button onClick={next} className="next-button" type="button">
Continue
</button>
)}
<div className="navigation-content">
{!hidePrev && (
<button onClick={prev} className="back-button" type="button">
Back
</button>
)}
{!hideNext && (
<button onClick={next} className="next-button" type="button">
Continue
</button>
)}
</div>
</nav>
);

View file

@ -9,6 +9,7 @@ import { getContentOfToken } from '../utilities';
const setupFormTextField = ({
labelText = '',
entityName = '',
placeholderText = '',
onChangeCallback,
}) => {
return (
@ -18,6 +19,7 @@ const setupFormTextField = ({
type="text"
name={entityName}
id={entityName}
placeholder={placeholderText}
onChange={onChangeCallback}
maxLength="60"
/>
@ -99,27 +101,32 @@ class PersonalInfoForm extends Component {
const { prev } = this.props;
return (
<div className="onboarding-main">
<div className="onboarding-content about">
<h2>About You!</h2>
<Navigation prev={prev} next={this.onSubmit} />
<div className="onboarding-content">
<header className="onboarding-content-header">
<h1 className="title">About You</h1>
</header>
<form>
{setupFormTextField({
labelText: 'Where are you located?',
entityName: 'location',
placeholderText: 'e.g. New York, NY',
onChangeCallback: this.handleChange,
})}
{setupFormTextField({
labelText: 'What is your title?',
entityName: 'employment_title',
placeholderText: 'e.g. Software Engineer',
onChangeCallback: this.handleChange,
})}
{setupFormTextField({
labelText: 'Where do you work?',
entityName: 'employer_name',
placeholderText: 'e.g. Company name, self-employed, etc.',
onChangeCallback: this.handleChange,
})}
</form>
</div>
<Navigation prev={prev} next={this.onSubmit} />
</div>
);
}
@ -128,6 +135,7 @@ class PersonalInfoForm extends Component {
setupFormTextField.propTypes = {
labelText: PropTypes.string.isRequired,
entityName: PropTypes.string.isRequired,
placeholderText: PropTypes.string.isRequired,
onChangeCallback: PropTypes.func.isRequired,
};