Feature/refactored onboarding (#3333)

* set up refactored onboarding

* create onboarding page

* add in first slide and change slide functionality

* fix test suite

* profile refactor

* profile refactor

* refactor to api

* add checkbox fields

* add checkbox fields

* remove puts

* add basic css

* add styling

* add redirect

* hide back and next at first and last slides

* test refactored onboarding

* test refactored onboarding

* remove article edits

* Fix schema

* Add deleted file back in

* Add default value for checked_t&c column

* Adjust HTML structure to keep nav buttons in place

* Fix ESLint issues on Onboarding.jsx file

* Handling for undefined or empty followedTags on getUserTags

* Fix codeclimate issues

* Fix codeclimate issues

* Fix more codeclimate issues

* Fix more codeclimate issues

* Update Onboarding snapshots

* Uncheck the CoC and T&C checkboxes on render

* Update snapshots

* Return false instead of raising error

* Update spec to use new onboarding

* Redirect to onboarding if haven't seen it yet

* Prevent redirect to onboarding from /signout_confirm

* Use assign_attributes instead of saving twice

* Move COC and T&C checkbox page to second slide

* Add 'go back to original page' functionality

* Reuse ready prototype logic

* Keep track of the last visited onboarding page

* Fix email subscription bug

* Fix overflow issue for tags page

* Remove height to prevent page container scrolling

* Check for CoC and T&C for displaying onboarding

* Add InstantClick redirect and preserve referrer in client

* Fix async update + check by using localStorage

* Turn off onboarding for tests

* Finalize design for onboarding

* Finalize design for onboarding

* Make bulk follows during onboarding

* Fix bulk follow test
This commit is contained in:
Ali Spittel 2019-07-26 15:53:32 -04:00 committed by Ben Halpern
parent 6cc0d125b5
commit 52c60ce37e
78 changed files with 3050 additions and 1326 deletions

View file

@ -8,9 +8,11 @@ about: Create a report to help us improve
<!-- If you're having trouble updating your profile, it is likely because you logged in separately with GitHub & Twitter. Please check if this is the case before creating a bug report, and email yo@dev.to so we can merge your accounts. -->
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
<!-- 1. Go to '...' -->
@ -19,9 +21,11 @@ about: Create a report to help us improve
<!-- 4. See error -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Desktop (please complete the following information):**
@ -38,4 +42,5 @@ about: Create a report to help us improve
- Version:
**Additional context**
<!-- Add any other context about the problem or helpful links here. -->

View file

@ -4,13 +4,17 @@ about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -1,5 +1,5 @@
var bigDev =
' \n \
' \n \
\n \
-oooooooo/- .+ooooooooo: +ooo+ oooo/ \n \
+MMMMMMMMMMm+ -NMMMMMMMMMMs +MMMM: /MMMM/ \n \
@ -19,5 +19,5 @@ var bigDev =
console.log(
bigDev,
"Hey there! Interested in the code behind dev.to? Well you're in luck - we're open source! Come say hi, tell us what you're debugging, or even lend a hand in our repo - https://github.com/thepracticaldev/dev.to/ \n",
"Did you find a bug or vulnerability? Check out our bug bounty info here: https://dev.to/security"
'Did you find a bug or vulnerability? Check out our bug bounty info here: https://dev.to/security',
);

View file

@ -1,6 +1,6 @@
function initializeAllFollowButts() {
var followButts = document.getElementsByClassName('follow-action-button');
for(var i = 0; i < followButts.length; i++) {
for (var i = 0; i < followButts.length; i++) {
initializeFollowButt(followButts[i]);
}
}
@ -8,10 +8,12 @@ function initializeAllFollowButts() {
//private
function initializeFollowButt(butt) {
var user = userData()
var deviceWidth = (window.innerWidth > 0) ? window.innerWidth : screen.width;
var user = userData();
var deviceWidth = window.innerWidth > 0 ? window.innerWidth : screen.width;
var buttInfo = JSON.parse(butt.dataset.info);
var userStatus = document.getElementsByTagName('body')[0].getAttribute('data-user-status');
var userStatus = document
.getElementsByTagName('body')[0]
.getAttribute('data-user-status');
if (userStatus === 'logged-out') {
addModalEventListener(butt);
return;
@ -19,9 +21,8 @@ function initializeFollowButt(butt) {
if (buttInfo.className === 'Tag' && user) {
handleTagButtAssignment(user, butt, buttInfo);
return;
}
else {
if(butt.dataset.fetched === 'fetched') {
} else {
if (butt.dataset.fetched === 'fetched') {
return;
}
fetchButt(butt, buttInfo);
@ -30,27 +31,34 @@ function initializeFollowButt(butt) {
function addModalEventListener(butt) {
assignState(butt, 'login');
butt.onclick = function (e) {
butt.onclick = function(e) {
e.preventDefault();
showModal('follow-button');
return;
}
};
}
function fetchButt(butt, buttInfo) {
butt.dataset.fetched = 'fetched';
var dataRequester;
if (window.XMLHttpRequest) {
dataRequester = new XMLHttpRequest();
dataRequester = new XMLHttpRequest();
} else {
dataRequester = new ActiveXObject('Microsoft.XMLHTTP');
dataRequester = new ActiveXObject('Microsoft.XMLHTTP');
}
dataRequester.onreadystatechange = function() {
if (dataRequester.readyState === XMLHttpRequest.DONE && dataRequester.status === 200) {
if (
dataRequester.readyState === XMLHttpRequest.DONE &&
dataRequester.status === 200
) {
addButtClickHandle(dataRequester.response, butt);
}
}
dataRequester.open('GET', '/follows/' + buttInfo.id + '?followable_type=' + buttInfo.className, true);
};
dataRequester.open(
'GET',
'/follows/' + buttInfo.id + '?followable_type=' + buttInfo.className,
true,
);
dataRequester.send();
}
@ -61,11 +69,16 @@ function addButtClickHandle(response, butt) {
butt.onclick = function(e) {
e.preventDefault();
handleOptimisticButtRender(butt);
}
};
}
function handleTagButtAssignment(user, butt, buttInfo) {
var buttAssignmentBoolean = JSON.parse(user.followed_tags).map(function (a) { return a.id; }).indexOf(buttInfo.id) !== -1;
var buttAssignmentBoolean =
JSON.parse(user.followed_tags)
.map(function(a) {
return a.id;
})
.indexOf(buttInfo.id) !== -1;
var buttAssignmentBoolText = buttAssignmentBoolean ? 'true' : 'false';
addButtClickHandle(buttAssignmentBoolText, butt);
shouldNotFetch = true;
@ -75,17 +88,13 @@ function assignInitialButtResponse(response, butt) {
butt.classList.add('showing');
if (response === 'true' || response === 'mutual') {
assignState(butt, 'unfollow');
}
else if (response === 'follow-back') {
assignState(butt, 'follow-back')
}
else if (response === 'false') {
} else if (response === 'follow-back') {
assignState(butt, 'follow-back');
} else if (response === 'false') {
assignState(butt, 'follow');
}
else if (response === 'self') {
} else if (response === 'self') {
assignState(butt, 'self');
}
else {
} else {
assignState(butt, 'login');
}
}
@ -98,25 +107,29 @@ function handleOptimisticButtRender(butt) {
} else {
// Handles actual following of tags/users
try {
//lets try grab the event buttons info data attribute user id
var evFabUserId = JSON.parse(butt.dataset.info).id;
var requestVerb = butt.dataset.verb;
//now for all follow action buttons
document.querySelectorAll('.follow-action-button').forEach(function(fab){
try{
//lets check they have info data attributes
if( fab.dataset.info ){
//and attempt to parse those, to grab that buttons info user id
var fabUserId = JSON.parse(fab.dataset.info).id;
//now does that user id match our event buttons user id?
if ( fabUserId && fabUserId === evFabUserId ) {
//yes - time to assign the same state!
assignState(fab, requestVerb);
}
}
}catch (err) {return}
});
}catch (err) {return}
//lets try grab the event buttons info data attribute user id
var evFabUserId = JSON.parse(butt.dataset.info).id;
var requestVerb = butt.dataset.verb;
//now for all follow action buttons
document.querySelectorAll('.follow-action-button').forEach(function(fab) {
try {
//lets check they have info data attributes
if (fab.dataset.info) {
//and attempt to parse those, to grab that buttons info user id
var fabUserId = JSON.parse(fab.dataset.info).id;
//now does that user id match our event buttons user id?
if (fabUserId && fabUserId === evFabUserId) {
//yes - time to assign the same state!
assignState(fab, requestVerb);
}
}
} catch (err) {
return;
}
});
} catch (err) {
return;
}
handleFollowButtPress(butt);
}

View file

@ -1,75 +1,84 @@
// Set reaction count to correct number
function setReactionCount(reactionName, newCount) {
var reactionClassList = document.getElementById("reaction-butt-" + reactionName).classList;
var reactionNumber = document.getElementById("reaction-number-" + reactionName);
var reactionClassList = document.getElementById(
'reaction-butt-' + reactionName,
).classList;
var reactionNumber = document.getElementById(
'reaction-number-' + reactionName,
);
if (newCount > 0) {
reactionClassList.add("activated");
reactionClassList.add('activated');
reactionNumber.textContent = newCount;
}
else {
reactionClassList.remove("activated");
reactionNumber.textContent = "0";
} else {
reactionClassList.remove('activated');
reactionNumber.textContent = '0';
}
}
function showUserReaction(reactionName, animatedClass) {
document.getElementById("reaction-butt-" + reactionName).classList.add("user-activated", animatedClass);
document
.getElementById('reaction-butt-' + reactionName)
.classList.add('user-activated', animatedClass);
}
function hideUserReaction(reactionName) {
document.getElementById("reaction-butt-" + reactionName).classList.remove("user-activated", "user-animated");
document
.getElementById('reaction-butt-' + reactionName)
.classList.remove('user-activated', 'user-animated');
}
function hasUserReacted(reactionName) {
return document.getElementById("reaction-butt-" + reactionName)
.classList.contains("user-activated");
return document
.getElementById('reaction-butt-' + reactionName)
.classList.contains('user-activated');
}
function getNumReactions(reactionName) {
var num = document.getElementById("reaction-number-" + reactionName).textContent;
if (num == "") {
var num = document.getElementById('reaction-number-' + reactionName)
.textContent;
if (num == '') {
return 0;
}
return parseInt(num);
}
function initializeArticleReactions() {
setTimeout(function () {
if (document.getElementById("article-body")) {
var articleId = document.getElementById("article-body").dataset.articleId;
if (document.getElementById("article-reaction-actions")) {
setTimeout(function() {
if (document.getElementById('article-body')) {
var articleId = document.getElementById('article-body').dataset.articleId;
if (document.getElementById('article-reaction-actions')) {
var ajaxReq;
var thisButt = this;
if (window.XMLHttpRequest) {
ajaxReq = new XMLHttpRequest();
} else {
ajaxReq = new ActiveXObject("Microsoft.XMLHTTP");
ajaxReq = new ActiveXObject('Microsoft.XMLHTTP');
}
ajaxReq.onreadystatechange = function () {
ajaxReq.onreadystatechange = function() {
if (ajaxReq.readyState == XMLHttpRequest.DONE) {
var json = JSON.parse(ajaxReq.response);
json.article_reaction_counts.forEach(function (reaction) {
setReactionCount(reaction.category, reaction.count)
})
json.reactions.forEach(function (reaction) {
if (document.getElementById("reaction-butt-" + reaction.category)) {
showUserReaction(reaction.category, "not-user-animated");
json.article_reaction_counts.forEach(function(reaction) {
setReactionCount(reaction.category, reaction.count);
});
json.reactions.forEach(function(reaction) {
if (
document.getElementById('reaction-butt-' + reaction.category)
) {
showUserReaction(reaction.category, 'not-user-animated');
}
})
});
}
}
ajaxReq.open("GET", "/reactions?article_id=" + articleId, true);
};
ajaxReq.open('GET', '/reactions?article_id=' + articleId, true);
ajaxReq.send();
}
}
var reactionButts = document.getElementsByClassName("article-reaction-butt")
var reactionButts = document.getElementsByClassName(
'article-reaction-butt',
);
for (var i = 0; i < reactionButts.length; i++) {
reactionButts[i].onclick = function (e) {
reactToArticle(articleId, this.dataset.category)
reactionButts[i].onclick = function(e) {
reactToArticle(articleId, this.dataset.category);
};
}
if (document.getElementById('jump-to-comments')) {
@ -81,7 +90,7 @@ function initializeArticleReactions() {
});
};
}
}, 3)
}, 3);
}
function reactToArticle(articleId, reaction) {
@ -92,18 +101,20 @@ function reactToArticle(articleId, reaction) {
hideUserReaction(reaction);
setReactionCount(reaction, currentNum - 1);
} else {
showUserReaction(reaction, "user-animated");
showUserReaction(reaction, 'user-animated');
setReactionCount(reaction, currentNum + 1);
}
}
var userStatus = document.getElementsByTagName('body')[0].getAttribute('data-user-status');
var userStatus = document
.getElementsByTagName('body')[0]
.getAttribute('data-user-status');
sendHapticMessage('medium');
if (userStatus == "logged-out") {
showModal("react-to-article");
if (userStatus == 'logged-out') {
showModal('react-to-article');
return;
} else {
toggleReaction();
document.getElementById("reaction-butt-" + reaction).disabled = true;
document.getElementById('reaction-butt-' + reaction).disabled = true;
}
function createFormdata() {
@ -112,26 +123,26 @@ function reactToArticle(articleId, reaction) {
* The logic can be seen in sendFetch.js.
*/
var formData = new FormData();
formData.append("reactable_type", "Article");
formData.append("reactable_id", articleId);
formData.append("category", reaction);
formData.append('reactable_type', 'Article');
formData.append('reactable_id', articleId);
formData.append('category', reaction);
return formData;
}
getCsrfToken()
.then(sendFetch("reaction-creation", createFormdata()))
.then(function (response) {
.then(sendFetch('reaction-creation', createFormdata()))
.then(function(response) {
if (response.status === 200) {
return response.json().then(() => {
document.getElementById("reaction-butt-" + reaction).disabled = false;
document.getElementById('reaction-butt-' + reaction).disabled = false;
});
} else {
toggleReaction();
document.getElementById("reaction-butt-" + reaction).disabled = false;
document.getElementById('reaction-butt-' + reaction).disabled = false;
}
})
.catch(function (error) {
.catch(function(error) {
toggleReaction();
document.getElementById("reaction-butt-" + reaction).disabled = false;
})
document.getElementById('reaction-butt-' + reaction).disabled = false;
});
}

View file

@ -1,7 +1,10 @@
function initializeFooterMod() {
var footerContainer = document.getElementById("footer-container");
var footerContainer = document.getElementById('footer-container');
if (
footerContainer && document.getElementById('page-content').className.indexOf('stories-show') > -1 && !document.getElementById('IS_CENTERED_PAGE')
footerContainer &&
document.getElementById('page-content').className.indexOf('stories-show') >
-1 &&
!document.getElementById('IS_CENTERED_PAGE')
) {
document
.getElementById('footer-container')

View file

@ -15,21 +15,21 @@ function initializePWAFunctionality() {
e.preventDefault();
window.location.reload();
};
var isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|DEV-Native-ios/i.test(navigator.userAgent);
var isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|DEV-Native-ios/i.test(
navigator.userAgent,
);
if (!isTouchDevice) {
var domain = window.location.protocol + '//' + window.location.host
var links = document.getElementsByTagName("a");
for(var i=0, max=links.length; i<max; i++) {
var domain = window.location.protocol + '//' + window.location.host;
var links = document.getElementsByTagName('a');
for (var i = 0, max = links.length; i < max; i++) {
var a = links[i];
if (a.href.indexOf(domain + '/') === 0
|| a.href.indexOf('/') === 0
) {
// Is internal link. Do nothing right now.
if (a.href.indexOf(domain + '/') === 0 || a.href.indexOf('/') === 0) {
// Is internal link. Do nothing right now.
} else {
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
}
}
}
}
}
}

View file

@ -1,50 +1,65 @@
function initializeTouchDevice() {
var isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|DEV-Native-ios/i.test(navigator.userAgent);
var isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|DEV-Native-ios/i.test(
navigator.userAgent,
);
if (navigator.userAgent === 'DEV-Native-ios') {
document.getElementsByTagName("body")[0].classList.add("dev-ios-native-body");
document
.getElementsByTagName('body')[0]
.classList.add('dev-ios-native-body');
}
setTimeout(function(){
setTimeout(function() {
removeShowingMenu();
if (isTouchDevice) {
document.getElementById("navigation-butt").onclick = function(e){
document.getElementById("navbar-menu-wrapper").classList.toggle('showing');
}
document.getElementById('navigation-butt').onclick = function(e) {
document
.getElementById('navbar-menu-wrapper')
.classList.toggle('showing');
};
} else {
document.getElementById("navbar-menu-wrapper").classList.add('desktop')
document.getElementById("navigation-butt").onfocus = function(e){
document.getElementById("navbar-menu-wrapper").classList.add('showing');
}
document.getElementById("last-nav-link").onblur = function(e) {
setTimeout(function(){
console.log(document.activeElement)
if (document.activeElement != document.getElementById("second-last-nav-link")) {
document.getElementById("navbar-menu-wrapper").classList.remove('showing');
document.getElementById('navbar-menu-wrapper').classList.add('desktop');
document.getElementById('navigation-butt').onfocus = function(e) {
document.getElementById('navbar-menu-wrapper').classList.add('showing');
};
document.getElementById('last-nav-link').onblur = function(e) {
setTimeout(function() {
console.log(document.activeElement);
if (
document.activeElement !=
document.getElementById('second-last-nav-link')
) {
document
.getElementById('navbar-menu-wrapper')
.classList.remove('showing');
}
}, 10)
}
document.getElementById("navigation-butt").onblur = function(e){
setTimeout(function(){
console.log(document.activeElement)
if (document.activeElement != document.getElementById("first-nav-link")) {
document.getElementById("navbar-menu-wrapper").classList.remove('showing');
}, 10);
};
document.getElementById('navigation-butt').onblur = function(e) {
setTimeout(function() {
console.log(document.activeElement);
if (
document.activeElement != document.getElementById('first-nav-link')
) {
document
.getElementById('navbar-menu-wrapper')
.classList.remove('showing');
}
}, 10)
}
}, 10);
};
}
document.getElementById("menubg").onclick = function(e) {
document.getElementById("navbar-menu-wrapper").classList.remove('showing');
}
},10)
document.getElementById('menubg').onclick = function(e) {
document
.getElementById('navbar-menu-wrapper')
.classList.remove('showing');
};
}, 10);
}
function removeShowingMenu() {
document.getElementById("navbar-menu-wrapper").classList.remove('showing')
setTimeout(function(){
document.getElementById("navbar-menu-wrapper").classList.remove('showing')
},5)
setTimeout(function(){
document.getElementById("navbar-menu-wrapper").classList.remove('showing')
},150)
function removeShowingMenu() {
document.getElementById('navbar-menu-wrapper').classList.remove('showing');
setTimeout(function() {
document.getElementById('navbar-menu-wrapper').classList.remove('showing');
}, 5);
setTimeout(function() {
document.getElementById('navbar-menu-wrapper').classList.remove('showing');
}, 150);
}

View file

@ -1,17 +1,18 @@
function showModal(context) {
document.getElementById("global-signup-modal").style.display = "block";
document.getElementById("global-signup-modal").classList.add("showing");
document.getElementsByTagName("body")[0].classList.add("modal-open");
document.getElementById('global-signup-modal').style.display = 'block';
document.getElementById('global-signup-modal').classList.add('showing');
document.getElementsByTagName('body')[0].classList.add('modal-open');
initSignupModal();
}
function initSignupModal() {
if (document.getElementById("global-signup-modal")) {
document.getElementById("global-signup-modal-bg").onclick = function() {
document.getElementById("global-signup-modal").style.display = "none";
document.getElementById("global-signup-modal").classList.remove("showing");
document.getElementsByTagName("body")[0].classList.remove("modal-open");
}
if (document.getElementById('global-signup-modal')) {
document.getElementById('global-signup-modal-bg').onclick = function() {
document.getElementById('global-signup-modal').style.display = 'none';
document
.getElementById('global-signup-modal')
.classList.remove('showing');
document.getElementsByTagName('body')[0].classList.remove('modal-open');
};
}
}
}

View file

@ -15,14 +15,12 @@
@import 'user-profile-header';
@import 'comments';
@import 'leaderboard';
@import 'onboarding';
@import 'notifications';
@import 'syntax';
@import 'signup-modal';
@import 'tags';
@import 'live';
@import 'delete-confirm';
@import 'preact/onboarding-modal';
@import 'preact/sidebar-widget';
@import 'preact/article-form';
@import 'tag-edit';

View file

@ -1,41 +1,43 @@
@import 'variables';
.onboarding-container{
padding-top:calc(5% + 50px);
text-align:center;
min-height:550px;
font-family: $helvetica;
h2{
font-weight:300;
#onboarding-container {
min-height: calc(100vh - 350px);
}
.onboarding-container {
max-width: 800px;
margin: 0 auto;
.next-button {
float: right;
}
form{
width:98%;
max-width:500px;
margin:auto;
input, textarea{
width:80%;
padding:10px;
border:1px solid $light-medium-gray;
display:block;
margin:5px auto;
border-radius:2px;
font-size:20px;
&[type=submit]{
background:$blue;
border:1px solid darken($blue,4%);
color:white;
width:calc(80% + 22px);
cursor:pointer;
&:hover{
opacity:0.92;
}
.next-button,
.back-button {
padding: 4px 20px;
border-radius: 100px;
margin-right: 5px;
font-size: 14px;
margin-top: 6px;
display: inline-block;
color: #0a0a0a;
}
}
}
textarea{
resize:none;
font-size:17px;
height:80px;
}
.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,557 +1,384 @@
@import 'variables';
.global-modal {
color: blue;
display:block;
position:fixed;
top:calc(15% - 33px);
left:calc(15% - 33px);
right:calc(15% - 33px);
bottom:calc(15% - 33px);
z-index:899900;
animation: fadein 0.20s ease ;
display: block !important;
@keyframes fadein{
0% { opacity:0;}
100% { opacity:1;}
.pages-onboarding {
min-height: initial;
}
.onboarding-body {
background-color: white;
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;
background-position: center; /* Center the image */
background-repeat: no-repeat; /* Do not repeat the image */
a {
color: #2e3b9e;
}
.close-button {
position:fixed;
text-align: right;
color:black;
border:0;
font-size: 1em;
background:transparent;
top:2%;
left:1%;
cursor:pointer;
img{
width:calc(0.7vw + 18px);
.sticker-logo {
height: 80px;
width: 80px;
transform: rotate(-5deg);
padding: 10px;
vertical-align: middle;
top: -10px;
padding-top: 5px;
}
.onboarding-main {
display: flex;
flex-direction: column;
width: 800px;
height: 466px;
background-color: white;
border: 8px solid #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);
// box-shadow: var(--theme-container-box-shadow, 1px 1px 0px #c2c2c2);
align-items: center;
justify-content: center;
}
.onboarding-content {
flex: 1 0 auto;
width: 100%;
height: 440px;
margin-top: 5px;
h1 {
margin-top: 15px;
line-height: 1.1em;
}
}
.global-modal-bg {
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
background-color: #bac5fa;
z-index:899901;
opacity: 0.97;
.onboarding-navigation {
align-self: flex-end;
flex-shrink: 0;
width: 100%;
}
.yellow{
background: $yellow;
}
.purple{
background: $purple;
}
.green{
background: $green;
.background-image {
z-index: -1;
position: absolute;
top: 60px;
left: 0px;
}
.modal-header {
height: 10%;
display:block;
font-weight: 500;
font-size: 1.1em;
max-width: 450px;
font-family: $helvetica-condensed;
font-stretch:condensed;
.triangle-isosceles {
margin-left: 5px;
position:relative;
padding:10px 20px;
color:white;
background:#6091fb;
-webkit-border-radius:10px;
-moz-border-radius:10px;
border-radius:10px;
}
.triangle-isosceles:after {
content:"";
position:absolute;
bottom:-0.5em;
left:5%; /* controls horizontal position */
border-width:.75em .75em 0;
border-style:solid;
border-color:#6091fb transparent;
display:block;
width:0;
h1 {
text-align: center;
}
h2 {
font-size: 15px;
@media screen and (min-width: 600px) {
font-size: 26px;
}
}
.modal-body {
height:80%;
display:block;
overflow: hidden;
p {
text-align: left;
color: black;
margin: 0.5em 0px;
}
.onboarding-initial-welcome {
font-weight: bold;
font-size: calc( 1.9vw + 2.1vh + 10px);
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
p{
margin: 0;
}
@media screen and (min-width: 800px) {
font-size: 48px;
}
}
.sloan-bar {
width:15%;
float:left;
height: 100%;
text-align: center;
display: none;
@media screen and (min-width: 700px) {
display: block;
}
.sloan-img {
width:96%;
}
}
.body-message {
height:98%;
width:95%;
padding-left: calc(3% + 2px);
max-width: 1000px;
float:left;
font-size: calc(0.8em + 6px);
@media screen and (min-width: 700px) {
width:80%;
}
}
.tags-slide {
height: 100%;
}
.tags-col-container {
overflow-x: hidden;
overflow-y: auto;
text-align:left;
font-size: calc(0.77em + 6px);
height: calc(100vh - 280px);
-webkit-overflow-scrolling: touch;
@media screen and (min-width: 900px) {
font-size: 27px;
}
}
.onboarding-tag-container {
background: lighten($light-medium-gray, 1%);
color: $black;
display:inline-block;
font-family: "Lucida Console", Monaco, monospace, sans-serif;
position:relative;
width: 47%;
margin-right: 2%;
border-radius: 3px;
margin-top: calc(0.2vw + 3px);
font-size: .75em;
margin-bottom: 6px;
&:last-child{
margin-bottom: 150px;
}
@media screen and (max-width: 900px) {
width: 92%;
margin-right: 0%;
font-size: .66em;
}
&:hover{
background: darken($light-medium-gray, 3%);
}
&.followed-tag{
opacity:1;
a.onboarding-tag-link-follow{
background: rgba(255,255,255, 0.1);
}
}
a.onboarding-tag-link{
color: $black;
padding:14px 20px;
display:inline-block;
width:calc(100% - 40px);
@media screen and (max-width: 900px) {
text-align: left;
}
}
a.onboarding-tag-link-follow {
position:absolute;
right:0px;
top: 0px;
bottom: 0px;
width:60px;
text-align:center;
color:$black;
font-weight:900;
line-height: calc(4vw + 20px);
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
text-align: center;
font-size:1.1em;
border-left: 3px solid white;
@media screen and (min-width: 900px) {
line-height: 55px;
}
&.following-butt {
font-family: $helvetica-condensed;
top:8px;
right:6px;
border: 2px solid white;
color:white;
line-height:33px;
}
}
}
}
.onboarding-user-cta{
font-size: 1.1em;
color: $black;
margin: 20px auto 15px;
text-align: left;
}
.onboarding-user-container{
margin-top: calc(0.5vw + 8px);
text-align: left;
.onboarding-article-container{
color: $black;
.onboarding-article-header{
padding: 5px 10px 5px 5px;
background: $purple;
font-size: 1.1em;
font-weight: bold;
border: 1px solid darken($purple, 20%);
box-shadow: 4px 5px 0px darken($purple, 20%);
}
.onboarding-article-header-checkbox{
width: 80%;
display: inline-block;
text-align: center;
.onboarding-article-save-all-btn{
border: 2px solid transparent;
border-radius: 3px;
text-align: center;
float: right;
font-size: 0.75em;
background: inherit;
color: $black;
&:hover {
background: darken($purple, 10%);
}
&.saved {
background: darken($purple, 26%);
color: $white;
&:hover {
background: darken($purple, 30%);
}
}
}
}
}
.onboarding-user-list{
color: $black;
font-size: 0.7em;
text-align: center;
width: 97%;
// border: 1px solid darken($purple, 20%);
// box-shadow: 4px 5px 0px darken($purple, 20%);
.onboarding-user-list-body{
height: calc(65vh - 20px);
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
overflow-x: hidden;
.onboarding-user-loading {
display: flex;
align-items: center;
justify-content: center;
margin-top: 15%;
color: $medium-gray;
}
}
.onboarding-user-list-header{
background: $purple;
font-size:1.1em;
font-weight: bold;
width: calc(100% - 40px);
padding: 13px 20px;
border-radius: 3px;
position: relative;
text-align: left;
button {
position: absolute;
top: 0;
right: 0;
bottom: 0;
border: 0;
font-size: 1.5em;
width: 80px;
background: rgba(255,255,255,0.3);
color: $bold-blue;
text-align: center;
}
}
.onboarding-user-list-row{
padding: 20px 10px 10px;
width: 95%;
display: inline-block;
text-align: left;
position: relative;
@media screen and (min-width: 1000px) {
width: 45%;
}
&:last-child{
margin-bottom: 110px;
}
img{
height: 70px;
width: 70px;
border-radius: 50%;
display: inline-block;
vertical-align: -0.5em;
margin-bottom: 10px;
}
.onboarding-user-list-row__summary{
color: $medium-gray;
font-size: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.onboarding-article-engagement {
font-size: 16px;
font-weight: bold;
font-family: $helvetica-condensed;
color: $medium-gray;
img {
height: 20px;
min-width: 27px;
width: initial;
vertical-align: -5px;
margin-right: 3px;
border-radius: initial;
padding-left: 7px;
}
}
.onboarding-user-list-key{
width: calc(100% - 80px);
display: inline-block;
padding-left: 10px;
h3 {
-webkit-margin-before: 0.2em;
-webkit-margin-after: 0.3em;
font-size: 30px;
}
&.article {
width: calc(100% - 100px);
span {
// article author's name
color: $medium-gray;
}
p {
// article description
-webkit-margin-before: 0.5em;
-webkit-margin-start: 10px;
font-size: 0.85em;
}
}
}
.onboarding-user-list-checkbox{
width: 70px;
display: inline-block;
text-align: center;
button{
border: 0px;
border-radius: 3px;
background: transparent;
font-size:1.5em;
position: absolute;
top:25px;
right: 8px;
padding: 5px 12px;
&.checked{
background: $purple;
color: $black;
}
&:hover {
background: darken($purple, 10%);
}
&:active {
background: darken($purple, 20%);
}
&.article{
&:hover{
background: darken($purple, 10%);
}
&:active {
background: darken($purple, 20%);
}
&.save-single{
background: darken($purple, 2%);
font-size: 0.8em;
font-family: $helvetica-condensed;
width: inherit;
height: 35px;
&:hover {
background: darken($purple, 10%);
}
&:active {
background: darken($purple, 20%);
}
}
&.saved {
background: darken($purple, 26%);
color: white;
&:hover {
background: darken($purple, 35%);
}
&:active {
background: darken($purple, 50%);
}
}
}
}
}
}
}
}
.modal-footer {
height:8%;
display:block;
padding-top: 12px;
user-select: none;
.pageindicators{
text-align: center;
position: absolute;
right: 130px;
left: 130px;
bottom: 23px;
.pageindicator {
height: 11px;
width: 11px;
background: $light-medium-gray;
border-radius: 25px;
margin: 20px calc(3px + 0.5vw);
display: inline-block;
&.pageindicator--active{
background: $green;
}
}
}
.modal-footer-left {
float: left;
text-align: left;
width: 50%;
height: 100%;
.button {
background: $light-medium-gray;
}
}
.modal-footer-right {
float: left;
width:50%;
height:100%;
text-align: right;
.button {
background: $bold-blue;
color: white;
}
}
.button {
border-radius: 100px;
text-align: center;
margin: 1% auto 0;
cursor: pointer;
display: inline-block;
font-size: calc(0.1vw + 20px);
padding: 7px 30px;
border: 0px;
a {
color: black;
}
&:hover {
opacity:0.9;
}
&:active {
opacity:0.8;
}
}
}
.global-modal-inner {
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
position:fixed;
top:calc(12% - 36px);
left:calc(9% - 23px);
right:calc(9% - 23px);
bottom:calc(5% - 33px);
background:white;
z-index:899902;
text-align:center;
font-size:calc(1.1vw + 15px);
box-shadow:0px 0px 100px 1px rgba(0,0,0,0.3);
border-radius:5px;
padding:calc(1vw + 6px);
h1{
font-size:1.4em;
font-weight:400;
}
@media screen and (min-width: 900px) {
top:calc(7% - 33px);
bottom:calc(7% - 33px);
}
}
.onboarding-profile-page{
text-align: left;
color: $black;
height: 70vh;
padding-bottom: 60px;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
.onboarding-profile-question{
font-weight: bold;
font-size: 18px;
padding: 12px 0px 8px;
}
input {
width: 90%;
padding: 7px;
border-radius: 3px;
font-size: 18px;
border: 2px solid $light-medium-gray;
&:last-child{
margin-bottom: 110px;
}
}
}
.onboarding-final-message{
font-size: calc( 1.9vw + 2.1vh + 10px);
button {
width: 130px;
padding: 6px 0px;
height: auto;
margin: 11px 20px;
text-align: center;
font-weight: bold;
p{
margin: 1.2em 0px;
border-radius: 3px;
border: 2px solid #0a0a0a;
border: 2px solid var(--theme-top-bar-write-color, #0a0a0a);
color: #0a0a0a;
color: var(--theme-top-bar-write-color, #0a0a0a);
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 {
background: #66e2d5;
background: var(--theme-top-bar-write-background, #66e2d5);
float: right;
}
.about {
width: 100%;
label,
input,
textarea {
display: block;
width: 95%;
margin: 0 auto;
}
@media screen and (min-width: 800px) {
font-size: 48px;
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: white;
border-radius: 5px;
border: 2px solid #555;
vertical-align: middle;
margin: 8px;
cursor: pointer;
}
input[type='checkbox']:checked {
background: var(--theme-top-bar-write-background, #66e2d5);
}
}
.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;
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;
}
}
.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;
.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 {
position: absolute;
}
.onboarding-what-next {
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;
background-position: center; /* Center the image */
background-repeat: no-repeat; /* Do not repeat the image */
border: none;
}
.onboarding-content {
flex: 0 0 auto;
h1, h2 {
margin-top: initial;
}
}
span {
display: block;
}
.back-button {
background-color: white;
}
}
@media screen and (max-width: 400px) {
.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;
}
}
}

View file

@ -3,8 +3,8 @@ module Api
class FollowsController < ApplicationController
def create
return unless user_signed_in?
user_ids = JSON.parse(params[:users]).map { |h| h["id"] }
user_ids = params[:users].map { |h| h["id"] }
users = User.where(id: user_ids)
users.each do |user|
current_user.delay.follow(user)

View file

@ -36,6 +36,8 @@ class ApplicationController < ActionController::Base
end
def after_sign_in_path_for(resource)
return "/onboarding?referrer=#{request.env['omniauth.origin'] || 'none'}" unless current_user.saw_onboarding
request.env["omniauth.origin"] || stored_location_for(resource) || "/dashboard"
end

View file

@ -45,6 +45,7 @@ class AsyncInfoController < ApplicationController
reading_list_ids: ReadingList.new(@user).cached_ids_of_articles,
saw_onboarding: @user.saw_onboarding,
checked_code_of_conduct: @user.checked_code_of_conduct,
checked_terms_and_conditions: @user.checked_terms_and_conditions,
number_of_comments: @user.comments.count,
display_sponsors: @user.display_sponsors,
trusted: @user.trusted,

View file

@ -39,6 +39,10 @@ class PagesController < ApplicationController
set_surrogate_key_header "badge_page"
end
def onboarding
set_surrogate_key_header "onboarding_page"
end
def report_abuse
reported_url = params[:reported_url] || params[:url] || request.referer
@feedback_message = FeedbackMessage.new(

View file

@ -96,16 +96,31 @@ class UsersController < ApplicationController
end
def onboarding_update
current_user.update(JSON.parse(params[:user]).to_h) if params[:user]
current_user.assign_attributes(params[:user].permit(:summary, :location, :employment_title, :employer_name, :last_onboarding_page)) if params[:user]
current_user.saw_onboarding = true
authorize User
if current_user.save!
if current_user.save
respond_to do |format|
format.json { render json: { outcome: "onboarding closed" } }
format.json { render json: { outcome: "updated successfully" } }
end
else
respond_to do |format|
format.json { render json: { outcome: "onboarding opened" } }
format.json { render json: { outcome: "update failed" } }
end
end
end
def onboarding_checkbox_update
current_user.assign_attributes(params[:user].permit(:checked_code_of_conduct, :checked_terms_and_conditions, :email_membership_newsletter, :email_digest_periodic)) if params[:user]
current_user.saw_onboarding = true
authorize User
if current_user.save
respond_to do |format|
format.json { render json: { outcome: "updated successfully" } }
end
else
respond_to do |format|
format.json { render json: { outcome: "update failed" } }
end
end
end

View file

@ -29,5 +29,6 @@ module.exports = {
filterXSS: false,
Pusher: false,
algoliasearch: false,
ga: false,
},
};

View file

@ -64,7 +64,7 @@ describe('<ArticleForm />', () => {
const getArticleForm = () => (
<ArticleForm
version='v2'
version="v2"
article={
'{ "id": null, "body_markdown": null, "cached_tag_list": null, "main_image": null, "published": false, "title": null }'
}

View file

@ -432,9 +432,7 @@ export default class ArticleForm extends Component {
onClick={this.toggleImageManagement}
type="button"
>
<img src={ImageUploadIcon} alt="upload images" />
{' '}
IMAGES
<img src={ImageUploadIcon} alt="upload images" /> IMAGES
</button>
{moreConfigBottomButton}
</div>

View file

@ -70,12 +70,8 @@ export default class MoreConfig extends Component {
/>
</div>
<small>
Change meta tag
{' '}
<code>canonical_url</code>
{' '}
if this post was first published elsewhere
(like your own blog)
Change meta tag <code>canonical_url</code> if this post was first
published elsewhere (like your own blog)
</small>
<div>
<label>Series Name</label>

View file

@ -2,25 +2,36 @@ import { h } from 'preact';
import PropTypes from 'prop-types';
const orgOptions = (organizations, organizationId) => {
const orgs = organizations.map((organization) => {
if(organizationId === organization.id) {
return(
<option value={organization.id} selected>{organization.name}</option>
)
const orgs = organizations.map(organization => {
if (organizationId === organization.id) {
return (
<option value={organization.id} selected>
{organization.name}
</option>
);
}
return (
<option value={organization.id}>{organization.name}</option>
)
})
const nullOrgOption = organizationId === null ? <option value="" selected>None</option> : <option value="">None</option>
orgs.unshift(nullOrgOption) // make first option as "None"
return orgs
}
return <option value={organization.id}>{organization.name}</option>;
});
const nullOrgOption =
organizationId === null ? (
<option value="" selected>
None
</option>
) : (
<option value="">None</option>
);
orgs.unshift(nullOrgOption); // make first option as "None"
return orgs;
};
const OrgSettings = ({ organizations, organizationId, onToggle }) => (
<div className="articleform__orgsettings">
Publish under an organization:
<select name="article[organization_id]" id="article_publish_under_org" onBlur={onToggle}>
<select
name="article[organization_id]"
id="article_publish_under_org"
onBlur={onToggle}
>
{orgOptions(organizations, organizationId)}
</select>
</div>

View file

@ -98,16 +98,19 @@ export function getChannels(
) {
successCb(channels, query);
} else {
fetch(`/chat_channel_memberships/find_by_chat_channel_id?chat_channel_id=${retrievalID}`, {
Accept: 'application/json',
'Content-Type': 'application/json',
credentials: 'same-origin',
})
.then(response => response.json())
.then(json => {
channels.unshift(json);
successCb(channels, query);
})
fetch(
`/chat_channel_memberships/find_by_chat_channel_id?chat_channel_id=${retrievalID}`,
{
Accept: 'application/json',
'Content-Type': 'application/json',
credentials: 'same-origin',
},
)
.then(response => response.json())
.then(json => {
channels.unshift(json);
successCb(channels, query);
});
}
});
}

View file

@ -79,11 +79,15 @@ export default class Article extends Component {
.catch(this.handleNewReactionFailure);
};
actionButton = (props) => {
actionButton = props => {
const types = {
"heart": ["heart-reaction-button", "like", heartImage],
"unicorn": ["unicorn-reaction-button", "unicorn", unicornImage],
"readinglist": ["readinglist-reaction-button", "readinglist", bookmarkImage]
heart: ['heart-reaction-button', 'like', heartImage],
unicorn: ['unicorn-reaction-button', 'unicorn', unicornImage],
readinglist: [
'readinglist-reaction-button',
'readinglist',
bookmarkImage,
],
};
const curType = types[props.reaction];
@ -94,12 +98,15 @@ export default class Article extends Component {
onClick={this.handleReactionClick}
data-category={curType[1]}
>
<img src={curType[2]} data-category={curType[1]} alt={`${curType[1]} reaction`} />
<img
src={curType[2]}
data-category={curType[1]}
alt={`${curType[1]} reaction`}
/>
</button>
)
);
};
render() {
const article = this.props.resource;
let heartReactedClass = '';
@ -161,15 +168,10 @@ export default class Article extends Component {
src={article.user.profile_image_90}
alt={article.user.username}
/>
<span>
{article.user.name}
{' '}
</span>
<span>{article.user.name} </span>
<span className="published-at">
{' '}
|
{' '}
{article.readable_publish_date}
| {article.readable_publish_date}
</span>
</a>
</h3>
@ -179,9 +181,18 @@ export default class Article extends Component {
</div>
</div>
<div className="activechatchannel__activeArticleActions">
<this.actionButton reaction="heart" reactedClass={heartReactedClass} />
<this.actionButton reaction="unicorn" reactedClass={unicornReactedClass} />
<this.actionButton reaction="readinglist" reactedClass={bookmarkReactedClass} />
<this.actionButton
reaction="heart"
reactedClass={heartReactedClass}
/>
<this.actionButton
reaction="unicorn"
reactedClass={unicornReactedClass}
/>
<this.actionButton
reaction="readinglist"
reactedClass={bookmarkReactedClass}
/>
</div>
</div>
);

View file

@ -144,24 +144,16 @@ class ChannelDetails extends Component {
if (this.userInList(channel.channel_users, user)) {
invite = (
<span className="channel__member">
is already in
{' '}
<em>{channel.channel_name}</em>
is already in <em>{channel.channel_name}</em>
</span>
);
}
return (
<div className="channeldetails__searchedusers">
<a href={user.path} target="_blank" rel="noopener noreferrer">
<img src={user.user.profile_image_90} alt="profile_image" />
@
{user.user.username}
{' '}
-
{' '}
{user.title}
</a>
{' '}
<img src={user.user.profile_image_90} alt="profile_image" />@
{user.user.username} - {user.title}
</a>{' '}
{invite}
</div>
);
@ -175,12 +167,7 @@ class ChannelDetails extends Component {
rel="noopener noreferrer"
data-content={`users/${user.id}`}
>
@
{user.username}
{' '}
-
{' '}
{user.name}
@{user.username} - {user.name}
</a>
</div>
));
@ -201,8 +188,7 @@ class ChannelDetails extends Component {
<div className="channeldetails__leftchannel">
<h2>Danger Zone</h2>
<h3>
You have left this channel
{' '}
You have left this channel{' '}
<span role="img" aria-label="emoji">
😢😢😢
</span>

View file

@ -18,13 +18,13 @@ const Channels = ({
const isUnopened =
new Date(channel.channel_last_message_at) > new Date(lastOpened) &&
channel.channel_messages_count > 0;
let newMessagesIndicator = isUnopened ? 'new' : 'old';
let newMessagesIndicator = isUnopened ? 'new' : 'old';
if (incomingVideoCallChannelIds.indexOf(channel.chat_channel_id) > -1) {
newMessagesIndicator = 'video';
}
const otherClassname = isActive
? 'chatchanneltab--active'
: 'chatchanneltab--inactive';
? 'chatchanneltab--active'
: 'chatchanneltab--inactive';
return (
<button
type="button"
@ -48,13 +48,17 @@ const Channels = ({
className={`chatchanneltabindicator chatchanneltabindicator--${newMessagesIndicator}`}
data-channel-id={channel.chat_channel_id}
>
<img src={channel.channel_image} alt='pic' className='chatchanneltabindicatordirectimage' />
<img
src={channel.channel_image}
alt="pic"
className="chatchanneltabindicatordirectimage"
/>
</span>
{channel.channel_name}
</span>
</button>
)
})
);
});
let topNotice = '';
if (
expanded &&
@ -66,11 +70,9 @@ const Channels = ({
<div className="chatchannels__channelslistheader">
<span role="img" aria-label="emoji">
👋
</span>
{' '}
</span>{' '}
Welcome to
<b> DEV Connect</b>
! You may message anyone you mutually follow.
<b> DEV Connect</b>! You may message anyone you mutually follow.
</div>
);
}
@ -105,7 +107,7 @@ const Channels = ({
{configFooter}
</div>
);
}
};
Channels.propTypes = {
activeChannelId: PropTypes.number.isRequired,

View file

@ -64,7 +64,7 @@ export default class Chat extends Component {
nonChatView: null,
inviteChannels: [],
soundOn: true,
videoOn: true
videoOn: true,
};
}
@ -198,7 +198,10 @@ export default class Chat extends Component {
scrolled: false,
});
const channel = channels[0];
this.triggerSwitchChannel(channel.chat_channel_id, channel.channel_modified_slug);
this.triggerSwitchChannel(
channel.chat_channel_id,
channel.channel_modified_slug,
);
} else {
this.setState({ channelsLoaded: true });
}
@ -272,13 +275,13 @@ export default class Chat extends Component {
receiveNewMessage = message => {
const receivedChatChannelId = message.chat_channel_id;
let newMessages = []
let newMessages = [];
if (this.state.messages[receivedChatChannelId]) {
newMessages = this.state.messages[receivedChatChannelId].slice();
newMessages.push(message);
if (newMessages.length > 150) {
newMessages.shift();
}
}
}
const newShowAlert =
this.state.activeChannelId === receivedChatChannelId
@ -450,17 +453,17 @@ export default class Chat extends Component {
});
};
handleVideoParticipantChange = (participants) => {
this.setState({videoCallParticipants: participants})
}
handleVideoParticipantChange = participants => {
this.setState({ videoCallParticipants: participants });
};
toggleVideoSound = () => {
this.setState({soundOn: !this.state.soundOn})
}
this.setState({ soundOn: !this.state.soundOn });
};
toggleVideoVideo = () => {
this.setState({videoOn: !this.state.videoOn})
}
this.setState({ videoOn: !this.state.videoOn });
};
triggerSwitchChannel = (id, slug) => {
this.setState({
@ -644,23 +647,15 @@ export default class Chat extends Component {
return (
<div className="chatmessage" style={{ color: 'grey' }}>
<div className="chatmessage__body">
You and
{' '}
You and{' '}
<a href={`/${activeChannel.channel_modified_slug}`}>
{activeChannel.channel_modified_slug}
</a>
{' '}
are
connected because you both follow each other. All interactions
{' '}
</a>{' '}
are connected because you both follow each other. All interactions{' '}
<em>
<b>must</b>
</em>
{' '}
abide by the
{' '}
<a href="/code-of-conduct">code of conduct</a>
.
</em>{' '}
abide by the <a href="/code-of-conduct">code of conduct</a>.
</div>
</div>
);
@ -669,19 +664,11 @@ are
return (
<div className="chatmessage" style={{ color: 'grey' }}>
<div className="chatmessage__body">
You have joined
{' '}
{activeChannel.channel_name}
! All interactions
{' '}
You have joined {activeChannel.channel_name}! All interactions{' '}
<em>
<b>must</b>
</em>
{' '}
abide by the
{' '}
<a href="/code-of-conduct">code of conduct</a>
.
</em>{' '}
abide by the <a href="/code-of-conduct">code of conduct</a>.
</div>
</div>
);
@ -885,7 +872,7 @@ are
: '';
let channelHeader = <div className="activechatchannel__header">&nbsp;</div>;
const currentChannel = this.state.activeChannel;
let channelConfigImage = ''
let channelConfigImage = '';
if (currentChannel) {
let channelHeaderInner = '';
if (currentChannel.channel_type === 'direct') {
@ -898,7 +885,13 @@ are
{currentChannel.channel_modified_slug}
</a>
);
channelConfigImage = <img src={ConfigImage} onClick={this.triggerActiveContent} data-content={`users/by_username?url=${currentChannel.channel_username}`} />;
channelConfigImage = (
<img
src={ConfigImage}
onClick={this.triggerActiveContent}
data-content={`users/by_username?url=${currentChannel.channel_username}`}
/>
);
} else {
channelHeaderInner = (
<a
@ -909,16 +902,23 @@ are
{currentChannel.channel_name}
</a>
);
channelConfigImage = <img src={ConfigImage} onClick={this.triggerActiveContent} data-content={`chat_channels/${this.state.activeChannelId}`} />;
channelConfigImage = (
<img
src={ConfigImage}
onClick={this.triggerActiveContent}
data-content={`chat_channels/${this.state.activeChannelId}`}
/>
);
}
if (this.state.activeContent[this.state.activeChannelId] && this.state.activeContent[this.state.activeChannelId].type_of) {
if (
this.state.activeContent[this.state.activeChannelId] &&
this.state.activeContent[this.state.activeChannelId].type_of
) {
channelConfigImage = '';
}
channelHeader = (
<div className="activechatchannel__header">
{channelHeaderInner}
{' '}
{channelConfigImage}
{channelHeaderInner} {channelConfigImage}
</div>
);
}
@ -946,8 +946,7 @@ are
className="activechatchannel__incomingcall"
onClick={this.answerVideoCall}
>
👋 Incoming Video Call
{' '}
👋 Incoming Video Call{' '}
</div>
);
}

View file

@ -20,16 +20,12 @@ export default class GithubRepo extends Component {
componentDidMount() {
if (this.state.token) {
getJSONContents(
`https://api.github.com/repos/${
this.props.resource.args
}/contents?access_token=${this.state.token}`,
`https://api.github.com/repos/${this.props.resource.args}/contents?access_token=${this.state.token}`,
this.loadContent,
this.loadFailure,
);
getJSONContents(
`https://api.github.com/repos/${
this.props.resource.args
}/readme?access_token=${this.state.token}`,
`https://api.github.com/repos/${this.props.resource.args}/readme?access_token=${this.state.token}`,
this.loadContent,
this.loadFailure,
);
@ -91,9 +87,7 @@ export default class GithubRepo extends Component {
<div className="activecontent__githubrepoheader">
<em>Authentication required</em>
</div>
<p>
This feature is in internal alpha testing mode.
</p>
<p>This feature is in internal alpha testing mode.</p>
</div>
);
}
@ -115,9 +109,7 @@ export default class GithubRepo extends Component {
data-path={item.path}
onClick={this.handleItemClick}
>
📁
{' '}
{item.name}
📁 {item.name}
</a>
</div>
));

View file

@ -13,7 +13,7 @@ function blockChat(activeChannelId) {
export default class UserDetails extends Component {
render() {
const { user} = this.props;
const { user } = this.props;
const channelId = this.props.activeChannelId;
const channel = this.props.activeChannel || {};
const socialIcons = [];
@ -65,24 +65,26 @@ export default class UserDetails extends Component {
}
let blockButton = '';
if (channel.channel_type === 'direct' && window.currentUser.id != user.id) {
blockButton = <button
onClick={() => {
const modal = document.getElementById('userdetails__blockmsg');
const otherModal = document.getElementById(
'userdetails__reportabuse',
);
otherModal.style.display = 'none';
if (modal.style.display === 'none') {
modal.style.display = 'block';
window.location.href = `#userdetails__blockmsg`;
} else {
modal.style.display = 'none';
window.location.href = `#`;
}
}}
>
Block User
</button>
blockButton = (
<button
onClick={() => {
const modal = document.getElementById('userdetails__blockmsg');
const otherModal = document.getElementById(
'userdetails__reportabuse',
);
otherModal.style.display = 'none';
if (modal.style.display === 'none') {
modal.style.display = 'block';
window.location.href = `#userdetails__blockmsg`;
} else {
modal.style.display = 'none';
window.location.href = `#`;
}
}}
>
Block User
</button>
);
}
return (
<div>

View file

@ -8,11 +8,11 @@ export default class Video extends Component {
let leftPx = 40;
let topPx = 70;
if (window.innerWidth > 1500) {
leftPx = window.innerWidth/5 + 200;
topPx = window.innerHeight/10;
leftPx = window.innerWidth / 5 + 200;
topPx = window.innerHeight / 10;
} else if (window.innerWidth > 641) {
leftPx = window.innerWidth/6;
topPx = window.innerHeight/10;
leftPx = window.innerWidth / 6;
topPx = window.innerHeight / 10;
}
this.state = {
leftPx,
@ -55,26 +55,29 @@ export default class Video extends Component {
'videolocalscreen',
);
localMediaContainer.appendChild(track.attach());
});
const roomParticipants = [];
room.participants.forEach(participant => {
component.triggerRemoteJoin(participant);
roomParticipants.push(participant);
});
component.setState({ participants: roomParticipants });
room.on('participantConnected', function(participant) {
});
const roomParticipants = [];
room.participants.forEach(participant => {
component.triggerRemoteJoin(participant);
roomParticipants.push(participant);
});
component.setState({ participants: roomParticipants });
room.on('participantConnected', function(participant) {
component.props.onParticipantChange(room.participants);
component.triggerRemoteJoin(participant);
room.participants.forEach(p => {
roomParticipants.push(p);
});
component.setState({ participants: roomParticipants });
room.on('participantDisconnected', function() {
component.props.onParticipantChange(room.participants)
room.on('participantDisconnected', function() {
component.props.onParticipantChange(room.participants);
});
participant.on('dominantSpeakerChanged', participant => {
console.log('The new dominant speaker in the Room is:', participant);
})
console.log(
'The new dominant speaker in the Room is:',
participant,
);
});
});
},
function(error) {
@ -89,15 +92,17 @@ export default class Video extends Component {
participant.on('trackAdded', track => {
if (!document.getElementById(`${track.kind}-${track.id}`)) {
const trackDiv = document.createElement('div');
trackDiv.className = `chat__videocalltrackdiv--${track.kind}`
trackDiv.appendChild(track.attach())
trackDiv.className = `chat__videocalltrackdiv--${track.kind}`;
trackDiv.appendChild(track.attach());
document.getElementById('videoremotescreen').appendChild(trackDiv);
document.getElementById('videoremotescreen').lastChild.id = `${track.kind}-${track.id}`
document.getElementById(
'videoremotescreen',
).lastChild.id = `${track.kind}-${track.id}`;
}
});
participant.on('trackRemoved', track => {
if (document.getElementById(track.id)) {
document.getElementById(track.id).outerHTML = ''
document.getElementById(track.id).outerHTML = '';
}
});
// participant.on('trackDisabled', track => {
@ -144,7 +149,7 @@ export default class Video extends Component {
};
toggleSound = () => {
const { room } = this.state
const { room } = this.state;
if (room) {
room.localParticipant.audioTracks.forEach(track => {
if (track.isEnabled) {
@ -152,13 +157,13 @@ export default class Video extends Component {
} else {
track.enable();
}
})
});
}
this.props.onToggleSound()
}
this.props.onToggleSound();
};
toggleVideo = () => {
const { room } = this.state
const { room } = this.state;
if (room) {
room.localParticipant.videoTracks.forEach(track => {
if (track.isEnabled) {
@ -166,11 +171,10 @@ export default class Video extends Component {
} else {
track.enable();
}
})
});
}
this.props.onToggleVideo()
}
this.props.onToggleVideo();
};
render() {
return (
@ -183,9 +187,7 @@ export default class Video extends Component {
>
<div
id="videoremotescreen"
className={`chat__remotevideoscreen-${
this.state.participants.length
}`}
className={`chat__remotevideoscreen-${this.state.participants.length}`}
/>
<div className="chat__localvideoscren" id="videolocalscreen" />
<button
@ -194,11 +196,17 @@ export default class Video extends Component {
>
×
</button>
<button className="chat__videocallcontrolbutton" onClick={this.toggleSound}>
{this.props.soundOn ? 'Mute' : 'UnMute'}
<button
className="chat__videocallcontrolbutton"
onClick={this.toggleSound}
>
{this.props.soundOn ? 'Mute' : 'UnMute'}
</button>
<button className="chat__videocallcontrolbutton chat__videocallcontrolbutton--videoonoff" onClick={this.toggleVideo}>
{this.props.videoOn ? 'Turn Off Video' : 'Turn On Video'}
<button
className="chat__videocallcontrolbutton chat__videocallcontrolbutton--videoonoff"
onClick={this.toggleVideo}
>
{this.props.videoOn ? 'Turn Off Video' : 'Turn On Video'}
</button>
</div>
);

View file

@ -4,9 +4,7 @@ import { h } from 'preact';
export const ListingRow = ({ listing }) => {
const tagLinks = listing.tag_list.map(tag => (
<a href={`/listings?t=${tag}`} data-no-instant>
#
{tag}
{' '}
#{tag}{' '}
</a>
));

View file

@ -3,28 +3,41 @@ import { h } from 'preact';
import PropTypes from 'prop-types';
const orgOptions = (organizations, organizationId) => {
const orgs = organizations.map((organization) => {
const orgs = organizations.map(organization => {
if (organizationId === organization.id) {
return (
<option value={organization.id} selected>{organization.name}</option>
)
<option value={organization.id} selected>
{organization.name}
</option>
);
}
return (
<option value={organization.id}>{organization.name}</option>
)
})
const nullOrgOption = organizationId === null ? <option value="" selected>None</option> : <option value="">None</option>
orgs.unshift(nullOrgOption) // make first option as "None"
return orgs
}
return <option value={organization.id}>{organization.name}</option>;
});
const nullOrgOption =
organizationId === null ? (
<option value="" selected>
None
</option>
) : (
<option value="">None</option>
);
orgs.unshift(nullOrgOption); // make first option as "None"
return orgs;
};
const OrgSettings = ({ organizations, organizationId, onToggle }) => (
<div className="field">
<label htmlFor="organizationId">Post under an organization:</label>
<select name="classified_listing[organization_id]" id="listing_organization_id" onBlur={onToggle}>
<select
name="classified_listing[organization_id]"
id="listing_organization_id"
onBlur={onToggle}
>
{orgOptions(organizations, organizationId)}
</select>
<p><em>Posting on behalf of org spends org credits.</em></p>
<p>
<em>Posting on behalf of org spends org credits.</em>
</p>
</div>
);

View file

@ -69,15 +69,10 @@ export class ListingDashboard extends Component {
const listingLength = (selected, userListings, organizationListings) => {
return selected === 'user' ? (
<h4>
Listings Made:
{' '}
{userListings.length}
</h4>
<h4>Listings Made: {userListings.length}</h4>
) : (
<h4>
Listings Made:
{' '}
Listings Made:{' '}
{
organizationListings.filter(
listing => listing.organization_id === selected,
@ -89,15 +84,10 @@ export class ListingDashboard extends Component {
const creditCount = (selected, userCreds, organizations) => {
return selected === 'user' ? (
<h4>
Credits Available:
{' '}
{userCredits}
</h4>
<h4>Credits Available: {userCredits}</h4>
) : (
<h4>
Credits Available:
{' '}
Credits Available:{' '}
{organizations.find(org => org.id === selected).unspent_credits_count}
</h4>
);

View file

@ -28,13 +28,13 @@ export default class ListingForm extends Component {
categoriesForDetails: this.categoriesForDetails,
organizations,
organizationId: null, // change this for /edit later
}
};
}
handleOrgIdChange = e => {
const organizationId = e.target.selectedOptions[0].value;
this.setState({ organizationId })
}
this.setState({ organizationId });
};
render() {
const {
@ -48,36 +48,52 @@ export default class ListingForm extends Component {
organizations,
organizationId,
} = this.state;
const orgArea = (organizations && organizations.length > 0) ? (
<OrgSettings
organizations={organizations}
organizationId={organizationId}
onToggle={this.handleOrgIdChange}
/>
) : (
const orgArea =
organizations && organizations.length > 0 ? (
<OrgSettings
organizations={organizations}
organizationId={organizationId}
onToggle={this.handleOrgIdChange}
/>
) : (
''
);
if (id === null) {
return(
return (
<div>
<Title defaultValue={title} onChange={linkState(this, 'title')} />
<BodyMarkdown defaultValue={bodyMarkdown} onChange={linkState(this, 'bodyMarkdown')} />
<Categories categoriesForSelect={categoriesForSelect} categoriesForDetails={categoriesForDetails} onChange={linkState(this, 'category')} category={category} />
<Tags defaultValue={tagList} category={category} onInput={linkState(this, 'tagList')} />
<BodyMarkdown
defaultValue={bodyMarkdown}
onChange={linkState(this, 'bodyMarkdown')}
/>
<Categories
categoriesForSelect={categoriesForSelect}
categoriesForDetails={categoriesForDetails}
onChange={linkState(this, 'category')}
category={category}
/>
<Tags
defaultValue={tagList}
category={category}
onInput={linkState(this, 'tagList')}
/>
{orgArea}
{/* add contact via connect checkbox later */}
</div>
)
);
}
// WIP code for edit
return(
return (
<div>
<Title defaultValue={title} onChange={linkState(this, 'title')} />
<BodyMarkdown defaultValue={bodyMarkdown} onChange={linkState(this, 'bodyMarkdown')} />
<BodyMarkdown
defaultValue={bodyMarkdown}
onChange={linkState(this, 'bodyMarkdown')}
/>
<Tags defaultValue={tagList} onInput={linkState(this, 'tagList')} />
{orgArea}
{/* add contact via connect checkbox later */}
</div>
)
);
}
}

View file

@ -369,13 +369,7 @@ export class Listings extends Component {
onSubmit={this.handleSubmitMessage}
>
<p>
<b>
Contact
{' '}
{openedListing.author.name}
{' '}
via DEV Connect
</b>
<b>Contact {openedListing.author.name} via DEV Connect</b>
</p>
<textarea
value={this.state.message}
@ -391,12 +385,7 @@ export class Listings extends Component {
<p>
<em>
Message must be relevant and on-topic with the listing. All
private interactions
{' '}
<b>must</b>
{' '}
abide by the
{' '}
private interactions <b>must</b> abide by the{' '}
<a href="/code-of-conduct">code of conduct</a>
</em>
</p>
@ -425,12 +414,7 @@ abide by the
</button>
<p>
<em>
All private interactions
{' '}
<b>must</b>
{' '}
abide by the
{' '}
All private interactions <b>must</b> abide by the{' '}
<a href="/code-of-conduct">code of conduct</a>
</em>
</p>

View file

@ -53,7 +53,12 @@ export const SingleListing = ({
<div className={definedClass} id={`single-classified-listing-${listing.id}`}>
<div className="listing-content">
<h3>
<a href={`/listings/${listing.category}/${listing.slug}`} data-no-instant onClick={e => onOpenModal(e, listing)} data-listing-id={listing.id}>
<a
href={`/listings/${listing.category}/${listing.slug}`}
data-no-instant
onClick={e => onOpenModal(e, listing)}
data-listing-id={listing.id}
>
{listing.title}
</a>
</h3>

View file

@ -0,0 +1,69 @@
import 'preact/devtools';
import { h, Component } from 'preact';
import IntroSlide from './components/IntroSlide';
import PersonalInfoForm from './components/PersonalInfoForm';
import EmailListTermsConditionsForm from './components/EmailListTermsConditionsForm';
import ClosingSlide from './components/ClosingSlide';
import FollowTags from './components/FollowTags';
import FollowUsers from './components/FollowUsers';
import BioForm from './components/BioForm';
export default class Onboarding extends Component {
constructor(props) {
super(props);
this.nextSlide = this.nextSlide.bind(this);
this.prevSlide = this.prevSlide.bind(this);
const slides = [
IntroSlide,
EmailListTermsConditionsForm,
BioForm,
PersonalInfoForm,
FollowTags,
FollowUsers,
ClosingSlide,
];
const url = new URL(window.location);
const previousLocation = url.searchParams.get('referrer');
this.slides = slides.map(SlideComponent => (
<SlideComponent
next={this.nextSlide}
prev={this.prevSlide}
previousLocation={previousLocation}
/>
));
this.state = {
currentSlide: 0,
};
}
nextSlide() {
const { currentSlide } = this.state;
const nextSlide = currentSlide + 1;
if (nextSlide < this.slides.length) {
this.setState({
currentSlide: nextSlide,
});
}
}
prevSlide() {
const { currentSlide } = this.state;
const prevSlide = currentSlide - 1;
if (prevSlide >= 0) {
this.setState({
currentSlide: prevSlide,
});
}
}
render() {
const { currentSlide } = this.state;
return <div className="onboarding-body">{this.slides[currentSlide]}</div>;
}
}

View file

@ -0,0 +1,317 @@
import { h } from 'preact';
import { deep } from 'preact-render-spy';
import fetch from 'jest-fetch-mock';
import Onboarding from '../Onboarding';
import BioForm from '../components/BioForm';
import PersonalInfoForm from '../components/PersonalInfoForm';
import EmailTermsConditionsForm from '../components/EmailListTermsConditionsForm';
import FollowTags from '../components/FollowTags';
import FollowUsers from '../components/FollowUsers';
global.fetch = fetch;
function flushPromises() {
return new Promise(resolve => setImmediate(resolve));
}
describe('<Onboarding />', () => {
beforeEach(() => {
fetch.resetMocks();
});
const fakeTagResponse = JSON.stringify([
{
bg_color_hex: '#000000',
id: 715,
name: 'discuss',
text_color_hex: '#ffffff',
},
{
bg_color_hex: '#f7df1e',
id: 6,
name: 'javascript',
text_color_hex: '#000000',
},
{
bg_color_hex: '#2a2566',
id: 630,
name: 'career',
text_color_hex: '#ffffff',
},
]);
const fakeUsersToFollowResponse = JSON.stringify([
{
id: 1,
name: 'Ben Halpern',
profile_image_url: 'ben.jpg',
},
{
id: 2,
name: 'Krusty the Clown',
profile_image_url: 'clown.jpg',
},
{
id: 3,
name: 'dev.to staff',
profile_image_url: 'dev.jpg',
},
]);
const dataUser = JSON.stringify({
followed_tag_names: ['javascript'],
});
describe('IntroSlide', () => {
let onboardingSlides;
beforeEach(() => {
document.body.setAttribute('data-user', null);
onboardingSlides = deep(<Onboarding />);
});
test('renders properly', () => {
expect(onboardingSlides).toMatchSnapshot();
});
test('should move to the next slide upon clicking the next button', () => {
onboardingSlides.find('.next-button').simulate('click');
expect(onboardingSlides.state().currentSlide).toBe(1);
});
});
describe('EmailTermsConditionsForm', () => {
let onboardingSlides;
beforeEach(() => {
document.body.setAttribute('data-user', dataUser);
onboardingSlides = deep(<Onboarding />);
onboardingSlides.setState({ currentSlide: 1 });
});
test('renders properly', () => {
expect(onboardingSlides).toMatchSnapshot();
});
test('should not move if code of conduct is not agreed to', () => {
onboardingSlides.find('.next-button').simulate('click');
expect(onboardingSlides.state().currentSlide).toBe(1);
});
test('should not move if terms and conditions are not met', () => {
const event = {
target: {
value: 'checked_code_of_conduct',
name: 'checked_code_of_conduct',
},
};
onboardingSlides
.find('#checked_code_of_conduct')
.simulate('change', event);
onboardingSlides.find('.next-button').simulate('click');
expect(onboardingSlides.state().currentSlide).toBe(1);
});
test('should move to the next slide if code of conduct and terms and conditions are met', async () => {
fetch.once({});
onboardingSlides.find('#checked_code_of_conduct').simulate('change', {
target: {
value: 'checked_code_of_conduct',
name: 'checked_code_of_conduct',
},
});
onboardingSlides
.find('#checked_terms_and_conditions')
.simulate('change', {
target: {
value: 'checked_terms_and_conditions',
name: 'checked_terms_and_conditions',
},
});
const emailTerms = onboardingSlides.find(<EmailTermsConditionsForm />);
expect(emailTerms.state('checked_code_of_conduct')).toBe(true);
onboardingSlides.find('.next-button').simulate('click');
await flushPromises();
expect(onboardingSlides.state().currentSlide).toBe(2);
});
it('should move to the previous slide upon clicking the back button', () => {
onboardingSlides.find('.back-button').simulate('click');
expect(onboardingSlides.state().currentSlide).toBe(0);
});
});
describe('BioForm', () => {
let onboardingSlides;
const meta = document.createElement('meta');
meta.setAttribute('name', 'csrf-token');
document.body.appendChild(meta);
beforeEach(() => {
onboardingSlides = deep(<Onboarding />);
onboardingSlides.setState({ currentSlide: 2 });
document.body.setAttribute('data-user', dataUser);
});
test('renders properly', () => {
expect(onboardingSlides).toMatchSnapshot();
});
it('should move to the previous slide upon clicking the back button', () => {
onboardingSlides.find('.back-button').simulate('click');
expect(onboardingSlides.state().currentSlide).toBe(1);
});
test('forms can be filled and submitted', async () => {
fetch.once({});
const bioForm = onboardingSlides.find(<BioForm />);
const event = { target: { value: 'my bio', name: 'summary' } };
onboardingSlides.find('textarea').simulate('change', event);
expect(bioForm.state('summary')).toBe('my bio');
bioForm.find('.next-button').simulate('click');
await flushPromises();
expect(onboardingSlides.state().currentSlide).toBe(3);
});
});
describe('PersonalInformationForm', () => {
let onboardingSlides;
const meta = document.createElement('meta');
meta.setAttribute('name', 'csrf-token');
document.body.appendChild(meta);
beforeEach(() => {
onboardingSlides = deep(<Onboarding />);
onboardingSlides.setState({ currentSlide: 3 });
document.body.setAttribute('data-user', dataUser);
});
test('renders properly', () => {
expect(onboardingSlides).toMatchSnapshot();
});
it('should move to the previous slide upon clicking the back button', () => {
onboardingSlides.find('.back-button').simulate('click');
expect(onboardingSlides.state().currentSlide).toBe(2);
});
test('forms can be filled and submitted', async () => {
fetch.once({});
const personalInfoForm = onboardingSlides.find(<PersonalInfoForm />);
const locationEvent = {
target: { value: 'my location', name: 'location' },
};
onboardingSlides.find('#location').simulate('change', locationEvent);
const titleEvent = {
target: { value: 'my title', name: 'employment_title' },
};
onboardingSlides.find('#employment_title').simulate('change', titleEvent);
const employerEvent = {
target: { value: 'my employer name', name: 'employer_name' },
};
onboardingSlides.find('#employer_name').simulate('change', employerEvent);
expect(personalInfoForm.state('location')).toBe('my location');
expect(personalInfoForm.state('employment_title')).toBe('my title');
expect(personalInfoForm.state('employer_name')).toBe('my employer name');
personalInfoForm.find('.next-button').simulate('click');
fetch.once(fakeTagResponse);
await flushPromises();
expect(onboardingSlides.state().currentSlide).toBe(4);
});
});
describe('FollowTags', () => {
let onboardingSlides;
beforeEach(async () => {
document.body.setAttribute('data-user', dataUser);
onboardingSlides = deep(<Onboarding />);
fetch.once(fakeTagResponse);
onboardingSlides.setState({ currentSlide: 4 });
await flushPromises();
});
test('renders properly', () => {
expect(onboardingSlides).toMatchSnapshot();
});
test('it should render three tags', async () => {
expect(onboardingSlides.find('.tag').length).toBe(3);
});
test('adding a tag and submitting works', async () => {
fetch.once({});
const followTags = onboardingSlides.find(<FollowTags />);
onboardingSlides
.find('.tag')
.first()
.simulate('click');
expect(followTags.state('selectedTags').length).toBe(1);
onboardingSlides.find('.next-button').simulate('click');
fetch.once(fakeUsersToFollowResponse);
await flushPromises();
expect(onboardingSlides.state().currentSlide).toBe(5);
});
it('should move to the previous slide upon clicking the back button', () => {
onboardingSlides.find('.back-button').simulate('click');
expect(onboardingSlides.state().currentSlide).toBe(3);
});
});
describe('FollowUsers', () => {
let onboardingSlides;
beforeEach(async () => {
document.body.setAttribute('data-user', dataUser);
onboardingSlides = deep(<Onboarding />);
fetch.once(fakeUsersToFollowResponse);
onboardingSlides.setState({ currentSlide: 5 });
await flushPromises();
});
test('renders properly', () => {
expect(onboardingSlides).toMatchSnapshot();
});
test('it should render three users', async () => {
expect(onboardingSlides.find('.user').length).toBe(3);
});
test('adding a user and submitting works', async () => {
fetch.once({});
const followUsers = onboardingSlides.find(<FollowUsers />);
onboardingSlides
.find('.user')
.first()
.simulate('click');
expect(followUsers.state('selectedUsers').length).toBe(2);
onboardingSlides.find('.next-button').simulate('click');
await flushPromises();
expect(onboardingSlides.state().currentSlide).toBe(6);
});
it('should move to the previous slide upon clicking the back button', async () => {
fetch.once(fakeTagResponse);
onboardingSlides.find('.back-button').simulate('click');
await flushPromises();
expect(onboardingSlides.state().currentSlide).toBe(4);
});
});
describe('CloseSlide', () => {
let onboardingSlides;
beforeEach(() => {
document.body.setAttribute('data-user', null);
onboardingSlides = deep(<Onboarding />);
onboardingSlides.setState({ currentSlide: 6 });
});
test('renders properly', () => {
expect(onboardingSlides).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,431 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Onboarding /> BioForm renders properly 1`] = `
preact-render-spy (1 nodes)
-------
<div class="onboarding-body">
<div class="onboarding-main">
<div class="onboarding-content about">
<h2>About You!</h2>
<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!
<textarea
name="summary"
onChange={[Function bound handleChange]}
maxLength="120"
>
</textarea>
</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>
</div>
`;
exports[`<Onboarding /> CloseSlide renders properly 1`] = `
preact-render-spy (1 nodes)
-------
<div 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>
<div class="onboarding-what-next">
<a href="/welcome">
Join the Welcome Thread
<p class="whatnext-emoji">
<span
role="img"
aria-label="tada"
>
😊
</span>
</p>
</a>
<a href="/new">
Write your first DEV post
<p class="whatnext-emoji">
<span
role="img"
aria-label="tada"
>
✍️
</span>
</p>
</a>
<a href="/top/infinity">
Read all-time top posts
<p class="whatnext-emoji">
<span
role="img"
aria-label="tada"
>
🤓
</span>
</p>
</a>
<a href="/settings">
Customize your profile
<p class="whatnext-emoji">
<span
role="img"
aria-label="tada"
>
💅
</span>
</p>
</a>
</div>
</div>
</div>
</div>
`;
exports[`<Onboarding /> EmailTermsConditionsForm renders properly 1`] = `
preact-render-spy (1 nodes)
-------
<div class="onboarding-body">
<div class="onboarding-main">
<div class="onboarding-content checkbox-slide">
<h2>Some things to check off!</h2>
<form>
<label htmlFor="checked_code_of_conduct">
<input
type="checkbox"
name="checked_code_of_conduct"
id="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_membership_newsletter">
<input
type="checkbox"
name="email_membership_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"
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>
</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>
</div>
`;
exports[`<Onboarding /> FollowTags renders properly 1`] = `
preact-render-spy (1 nodes)
-------
<div class="onboarding-body">
<div class="onboarding-main">
<div class="onboarding-content">
<h2>Follow tags to customize your feed</h2>
<div class="scroll">
<button
type="button"
onClick={[Function onClick]}
style="background-color: #000000; color: #ffffff;"
class="tag"
>
#discuss
<div class="onboarding-tag-follow-indicator"></div>
</button>
<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>
</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>
</div>
`;
exports[`<Onboarding /> FollowUsers renders properly 1`] = `
preact-render-spy (1 nodes)
-------
<div class="onboarding-body">
<div class="onboarding-main">
<div class="onboarding-content">
<h2>
Ok, here are some people we picked for you
</h2>
<div class="scroll">
<div class="select-all-button-wrapper">
<button
type="button"
onClick={[Function onClick]}
>
Select All ✅
</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"
>
<div class="onboarding-user-follow-status">selected</div>
<img
src="clown.jpg"
alt=""
/>
<span>Krusty the Clown</span>
<p></p>
</button>
<button
type="button"
style="background-color: #c7ffe8;"
onClick={[Function onClick]}
class="user"
>
<div class="onboarding-user-follow-status">selected</div>
<img
src="dev.jpg"
alt=""
/>
<span>dev.to staff</span>
<p></p>
</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>
</div>
`;
exports[`<Onboarding /> IntroSlide renders properly 1`] = `
preact-render-spy (1 nodes)
-------
<div class="onboarding-body">
<div class="onboarding-main">
<div class="onboarding-content">
<h1>
<span>Welcome to the </span>
<img
src="/assets/purple-dev-logo.png"
class="sticker-logo"
alt="DEV"
/>
<span>community!</span>
</h1>
<p>
DEV is where programmers share ideas and help each other grow. Its a global community for contributing and discovering great ideas, having debates, and making friends.
</p>
<p>
A couple quick questions for you before you get started...
</p>
</div>
<nav class="onboarding-navigation">
<button
onClick={[Function bound onSubmit]}
class="next-button"
type="button"
>
Continue
</button>
</nav>
</div>
</div>
`;
exports[`<Onboarding /> PersonalInformationForm renders properly 1`] = `
preact-render-spy (1 nodes)
-------
<div class="onboarding-body">
<div class="onboarding-main">
<div class="onboarding-content about">
<h2>About You!</h2>
<form>
<label htmlFor="location">
Where are you located?
<input
type="text"
name="location"
id="location"
onChange={[Function bound handleChange]}
maxLength="60"
/>
</label>
<label htmlFor="employment_title">
What is your title?
<input
type="text"
name="employment_title"
id="employment_title"
onChange={[Function bound handleChange]}
maxLength="60"
/>
</label>
<label htmlFor="employer_name">
Where do you work?
<input
type="text"
name="employer_name"
id="employer_name"
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>
</div>
`;

View file

@ -0,0 +1,88 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import Navigation from './Navigation';
import { getContentOfToken } from '../utilities';
class BioForm extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.state = {
summary: '',
};
}
componentDidMount() {
const csrfToken = getContentOfToken('csrf-token');
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user: { last_onboarding_page: 'bio form' } }),
credentials: 'same-origin',
});
}
onSubmit() {
const csrfToken = getContentOfToken('csrf-token');
const { summary } = this.state;
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user: { summary } }),
credentials: 'same-origin',
}).then(response => {
if (response.ok) {
const { next } = this.props;
next();
}
});
}
handleChange(e) {
const { value } = e.target;
this.setState({
summary: value,
});
}
render() {
const { prev } = this.props;
return (
<div className="onboarding-main">
<div className="onboarding-content about">
<h2>About You!</h2>
<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!
<textarea
name="summary"
onChange={this.handleChange}
maxLength="120"
/>
</label>
</form>
</div>
<Navigation prev={prev} next={this.onSubmit} />
</div>
);
}
}
BioForm.propTypes = {
prev: PropTypes.func.isRequired,
next: PropTypes.string.isRequired,
};
export default BioForm;

View file

@ -0,0 +1,94 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import Navigation from './Navigation';
import { getContentOfToken } from '../utilities';
class ClosingSlide extends Component {
componentDidMount() {
const csrfToken = getContentOfToken('csrf-token');
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user: { last_onboarding_page: 'closing slide' } }),
credentials: 'same-origin',
});
}
render() {
const { previousLocation, prev, next } = this.props;
const previousLocationListElement = () => {
if (previousLocation !== 'none' && previousLocation !== null) {
return (
<a className="onboarding-previous-location" href={previousLocation}>
<div>Or go back to the page you were on before you signed up</div>
<code>{previousLocation}</code>
</a>
);
}
return null;
};
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>
<div className="onboarding-what-next">
<a href="/welcome">
Join the Welcome Thread
<p className="whatnext-emoji">
<span role="img" aria-label="tada">
😊
</span>
</p>
</a>
<a href="/new">
Write your first DEV post
<p className="whatnext-emoji">
<span role="img" aria-label="tada">
</span>
</p>
</a>
<a href="/top/infinity">
Read all-time top posts
<p className="whatnext-emoji">
<span role="img" aria-label="tada">
🤓
</span>
</p>
</a>
<a href="/settings">
Customize your profile
<p className="whatnext-emoji">
<span role="img" aria-label="tada">
💅
</span>
</p>
</a>
</div>
{previousLocationListElement()}
</div>
</div>
);
}
}
ClosingSlide.propTypes = {
prev: PropTypes.func.isRequired,
next: PropTypes.string.isRequired,
previousLocation: PropTypes.string.isRequired,
};
export default ClosingSlide;

View file

@ -0,0 +1,197 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import Navigation from './Navigation';
import { getContentOfToken } from '../utilities';
class EmailTermsConditionsForm extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.checkRequirements = this.checkRequirements.bind(this);
this.state = {
checked_code_of_conduct: false,
checked_terms_and_conditions: false,
email_membership_newsletter: true,
email_digest_periodic: true,
message: '',
textShowing: null,
};
}
componentDidMount() {
const csrfToken = getContentOfToken('csrf-token');
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user: { last_onboarding_page: 'emails, COC and T&C form' },
}),
credentials: 'same-origin',
});
}
onSubmit() {
if (!this.checkRequirements()) return;
const csrfToken = getContentOfToken('csrf-token');
fetch('/onboarding_checkbox_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user: this.state }),
credentials: 'same-origin',
}).then(response => {
if (response.ok) {
localStorage.setItem('shouldRedirectToOnboarding', false);
const { next } = this.props;
next();
}
});
}
checkRequirements() {
const {
checked_code_of_conduct,
checked_terms_and_conditions,
} = this.state;
if (!checked_code_of_conduct) {
this.setState({
message: 'You must agree to our Code of Conduct before continuing!',
});
return;
}
if (!checked_terms_and_conditions) {
this.setState({
message:
'You must agree to our Terms and Conditions before continuing!',
});
return;
}
return true;
}
handleChange(event) {
const { name } = event.target;
this.setState(currentState => ({
[name]: !currentState[name],
}));
}
handleShowText(event, id) {
event.preventDefault();
this.setState({ textShowing: document.getElementById(id).innerHTML });
}
backToSlide() {
this.setState({ textShowing: null });
}
render() {
const {
message,
checked_code_of_conduct,
checked_terms_and_conditions,
email_membership_newsletter,
email_digest_periodic,
textShowing,
} = this.state;
const { prev } = this.props;
if (textShowing) {
return (
<div className="onboarding-main">
<div className="onboarding-content checkbox-slide">
<button onClick={() => this.backToSlide()}>BACK</button>
<div
dangerouslySetInnerHTML={{ __html: textShowing }}
style={{ height: '360px', overflow: 'scroll' }}
/>
</div>
</div>
);
}
return (
<div className="onboarding-main">
<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"
name="checked_code_of_conduct"
id="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_membership_newsletter">
<input
type="checkbox"
name="email_membership_newsletter"
checked={email_membership_newsletter}
onChange={this.handleChange}
/>
Do you want to receive our weekly newsletter emails?
</label>
<label htmlFor="email_digest_periodic">
<input
type="checkbox"
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>
</form>
</div>
<Navigation prev={prev} next={this.onSubmit} />
</div>
);
}
}
EmailTermsConditionsForm.propTypes = {
prev: PropTypes.func.isRequired,
next: PropTypes.string.isRequired,
};
export default EmailTermsConditionsForm;

View file

@ -0,0 +1,123 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import Navigation from './Navigation';
import { getContentOfToken } from '../utilities';
class FollowTags extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleComplete = this.handleComplete.bind(this);
this.state = {
allTags: [],
selectedTags: [],
};
}
componentDidMount() {
fetch('/api/tags/onboarding')
.then(response => response.json())
.then(data => {
this.setState({ allTags: data });
});
const csrfToken = getContentOfToken('csrf-token');
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user: { last_onboarding_page: 'follow tags page' },
}),
credentials: 'same-origin',
});
}
handleClick(tag) {
let { selectedTags } = this.state;
if (!selectedTags.includes(tag)) {
this.setState(prevState => ({
selectedTags: [...prevState.selectedTags, tag],
}));
} else {
selectedTags = [...selectedTags];
const indexToRemove = selectedTags.indexOf(tag);
selectedTags.splice(indexToRemove, 1);
this.setState({
selectedTags,
});
}
}
handleComplete() {
const csrfToken = getContentOfToken('csrf-token');
const { selectedTags } = this.state;
Promise.all(
selectedTags.map(tag =>
fetch('/follows', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
followable_type: 'Tag',
followable_id: tag.id,
verb: 'follow',
}),
credentials: 'same-origin',
}),
),
).then(_ => {
const { next } = this.props;
next();
});
}
render() {
const { prev } = this.props;
const { selectedTags, allTags } = this.state;
return (
<div className="onboarding-main">
<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 ' : ''}
</div>
</button>
))}
</div>
</div>
<Navigation prev={prev} next={this.handleComplete} />
</div>
);
}
}
FollowTags.propTypes = {
prev: PropTypes.func.isRequired,
next: PropTypes.string.isRequired,
};
export default FollowTags;

View file

@ -0,0 +1,143 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import { userInfo } from 'os';
import Navigation from './Navigation';
import { getContentOfToken } from '../utilities';
class FollowUsers extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleComplete = this.handleComplete.bind(this);
this.state = {
users: [],
selectedUsers: [],
};
}
componentDidMount() {
fetch('/api/users?state=follow_suggestions', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
credentials: 'same-origin',
})
.then(response => response.json())
.then(data => {
this.setState({ users: data, selectedUsers: data });
});
const csrfToken = getContentOfToken('csrf-token');
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user: { last_onboarding_page: 'follow users page' },
}),
credentials: 'same-origin',
});
}
handleComplete() {
const csrfToken = getContentOfToken('csrf-token');
const { selectedUsers } = this.state;
const { next } = this.props;
fetch('/api/follows', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({users:selectedUsers}),
credentials: 'same-origin',
})
next();
}
handleSelectAll() {
const { selectedUsers, users } = this.state;
if (selectedUsers.length === users.length) {
this.setState({
selectedUsers: [],
});
} else {
this.setState({
selectedUsers: users,
});
}
}
handleClick(user) {
let { selectedUsers } = this.state;
if (!selectedUsers.includes(user)) {
this.setState(prevState => ({
selectedUsers: [...prevState.selectedUsers, user],
}));
} else {
selectedUsers = [...selectedUsers];
const indexToRemove = selectedUsers.indexOf(user);
selectedUsers.splice(indexToRemove, 1);
this.setState({
selectedUsers,
});
}
}
render() {
const { users, selectedUsers } = this.state;
const { prev } = this.props;
return (
<div className="onboarding-main">
<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>
{users.map(user => (
<button
type="button"
style={{
backgroundColor: selectedUsers.includes(user)
? '#c7ffe8'
: 'white',
}}
onClick={() => this.handleClick(user)}
className="user"
>
<div className="onboarding-user-follow-status">
{selectedUsers.includes(user) ? 'selected' : ''}
</div>
<img src={user.profile_image_url} alt="" />
<span>{user.name}</span>
<p>{user.summary}</p>
</button>
))}
</div>
</div>
<Navigation prev={prev} next={this.handleComplete} />
</div>
);
}
}
FollowUsers.propTypes = {
prev: PropTypes.func.isRequired,
next: PropTypes.string.isRequired,
};
export default FollowUsers;

View file

@ -0,0 +1,68 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import Navigation from './Navigation';
import { getContentOfToken } from '../utilities';
class IntroSlide extends Component {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
}
componentDidMount() {
const csrfToken = getContentOfToken('csrf-token');
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user: { last_onboarding_page: 'intro slide' } }),
credentials: 'same-origin',
});
}
onSubmit() {
const { next } = this.props;
next();
}
render() {
const { prev } = this.props;
return (
<div className="onboarding-main">
<div className="onboarding-content">
<h1>
<span>Welcome to the </span>
<img
src="/assets/purple-dev-logo.png"
className="sticker-logo"
alt="DEV"
/>
<span>community!</span>
</h1>
<p>
DEV is where programmers share ideas and help each other grow. Its
a global community for contributing and discovering great ideas,
having debates, and making friends.
</p>
<p>A couple quick questions for you before you get started...</p>
</div>
<Navigation prev={prev} next={this.onSubmit} hidePrev />
</div>
);
}
}
// const IntroSlide = ({ prev, next }) => (
// );
IntroSlide.propTypes = {
prev: PropTypes.func.isRequired,
next: PropTypes.string.isRequired,
};
export default IntroSlide;

View file

@ -0,0 +1,31 @@
import { h } from 'preact';
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>
)}
</nav>
);
Navigation.propTypes = {
prev: PropTypes.func.isRequired,
next: PropTypes.string.isRequired,
hideNext: PropTypes.bool,
hidePrev: PropTypes.bool,
};
Navigation.defaultProps = {
hideNext: false,
hidePrev: false,
};
export default Navigation;

View file

@ -0,0 +1,129 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import Navigation from './Navigation';
import { getContentOfToken } from '../utilities';
class PersonalInfoForm extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.state = {
location: '',
employment_title: '',
employer_name: '',
last_onboarding_page: 'personal info form',
};
}
componentDidMount() {
const csrfToken = getContentOfToken('csrf-token');
const { last_onboarding_page } = this.state;
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user: { last_onboarding_page } }),
credentials: 'same-origin',
});
}
onSubmit() {
const csrfToken = getContentOfToken('csrf-token');
const {
last_onboarding_page,
location,
employer_name,
employment_title,
} = this.state;
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user: {
last_onboarding_page,
location,
employer_name,
employment_title,
},
}),
credentials: 'same-origin',
}).then(response => {
if (response.ok) {
const { next } = this.props;
next();
}
});
}
handleChange(e) {
const { name, value } = e.target;
this.setState({
[name]: value,
});
}
render() {
const { prev } = this.props;
return (
<div className="onboarding-main">
<div className="onboarding-content about">
<h2>About You!</h2>
<form>
<label htmlFor="location">
Where are you located?
<input
type="text"
name="location"
id="location"
onChange={this.handleChange}
maxLength="60"
/>
</label>
<label htmlFor="employment_title">
What is your title?
<input
type="text"
name="employment_title"
id="employment_title"
onChange={this.handleChange}
maxLength="60"
/>
</label>
<label htmlFor="employer_name">
Where do you work?
<input
type="text"
name="employer_name"
id="employer_name"
onChange={this.handleChange}
maxLength="60"
/>
</label>
</form>
</div>
<Navigation prev={prev} next={this.onSubmit} />
</div>
);
}
}
PersonalInfoForm.propTypes = {
prev: PropTypes.func.isRequired,
next: PropTypes.string.isRequired,
};
export default PersonalInfoForm;

View file

@ -0,0 +1,8 @@
export const jsonToForm = data => {
const form = new FormData();
data.forEach(item => form.append(item.key, item.value));
return form;
};
export const getContentOfToken = token =>
document.querySelector(`meta[name='${token}']`).content;

View file

@ -19,14 +19,9 @@ function isUserSignedIn() {
}
function renderPage() {
import('../src/Onboarding')
import('../onboarding/Onboarding')
.then(({ default: Onboarding }) => {
const waitingForOnboarding = setInterval(function() {
if (document.getElementById('main-head-stylesheet')) {
render(<Onboarding />, document.getElementById('top-bar'));
clearInterval(waitingForOnboarding);
}
}, 3);
render(<Onboarding />, document.getElementById('onboarding-container'));
})
.catch(error => {
// eslint-disable-next-line no-console
@ -41,10 +36,7 @@ document.ready.then(
window.csrfToken = csrfToken;
getUnopenedChannels();
if (isUserSignedIn() && !currentUser.saw_onboarding) {
renderPage();
}
renderPage();
})
.catch(error => {
// eslint-disable-next-line no-console

View file

@ -28,7 +28,11 @@ function loadForm() {
const { article, organizations, version } = root.dataset;
render(
<ArticleForm article={article} organizations={organizations} version={version} />,
<ArticleForm
article={article}
organizations={organizations}
version={version}
/>,
root,
root.firstElementChild,
);

View file

@ -1,21 +1,18 @@
function loadFunctionality() {
if (!document.getElementById(
'notification-subscriptions-area',
)) {
if (!document.getElementById('notification-subscriptions-area')) {
return;
}
const {notifiableId} = document.getElementById(
const { notifiableId } = document.getElementById(
'notification-subscriptions-area',
).dataset;
const {notifiableType} = document.getElementById(
const { notifiableType } = document.getElementById(
'notification-subscriptions-area',
).dataset;
const userStatus = document
.getElementsByTagName('body')[0]
.getAttribute('data-user-status');
if (userStatus === 'logged-in') {
fetch(`/notification_subscriptions/${notifiableType}/${notifiableId}`, {
headers: {
@ -27,13 +24,15 @@ function loadFunctionality() {
})
.then(response => response.json())
.then(result => {
document.getElementById(`notification-subscription-label_${result.config}`).classList.add('selected');
document
.getElementById(`notification-subscription-label_${result.config}`)
.classList.add('selected');
// checkbox.checked = result;
});
}
let updateStatus = () => {};
if (userStatus === 'logged-out') {
updateStatus = () => {
// Disabled because showModal() is globally defined in asset pipeline
@ -41,10 +40,12 @@ function loadFunctionality() {
showModal('notification-subscription');
};
} else {
updateStatus = (target) => {
const allButtons = document.getElementsByClassName('notification-subscription-label');
for(let i = 0; i < allButtons.length; i += 1) {
allButtons[i].classList.remove('selected')
updateStatus = target => {
const allButtons = document.getElementsByClassName(
'notification-subscription-label',
);
for (let i = 0; i < allButtons.length; i += 1) {
allButtons[i].classList.remove('selected');
}
target.classList.add('selected');
fetch(`/notification_subscriptions/${notifiableType}/${notifiableId}`, {
@ -59,17 +60,19 @@ function loadFunctionality() {
config: target.dataset.payload,
// notifiable params are passed via URL
}),
})
});
};
}
const subscriptionButtons = document.getElementsByClassName('notification-subscription-label');
for(let i = 0; i < subscriptionButtons.length; i += 1) {
const subscriptionButtons = document.getElementsByClassName(
'notification-subscription-label',
);
for (let i = 0; i < subscriptionButtons.length; i += 1) {
subscriptionButtons[i].addEventListener('click', e => {
e.preventDefault();
updateStatus(e.target);
if (typeof sendHapticMessage !== "undefined") {
if (typeof sendHapticMessage !== 'undefined') {
sendHapticMessage('medium');
}
});
@ -77,7 +80,7 @@ function loadFunctionality() {
if (e.key === 'Enter') {
updateStatus(e.target);
}
});
});
}
}
@ -85,4 +88,4 @@ window.InstantClick.on('change', () => {
loadFunctionality();
});
loadFunctionality();
loadFunctionality();

View file

@ -0,0 +1,54 @@
import { getUserDataAndCsrfToken } from '../chat/util';
HTMLDocument.prototype.ready = new Promise(resolve => {
if (document.readyState !== 'loading') {
return resolve();
}
document.addEventListener('DOMContentLoaded', () => resolve());
return null;
});
function redirectableLocation() {
return (
window.location.pathname !== '/onboarding' &&
window.location.pathname !== '/signout_confirm'
);
}
function onboardingSkippable(currentUser) {
return (
currentUser.saw_onboarding &&
currentUser.checked_code_of_conduct &&
currentUser.checked_terms_and_conditions
);
}
document.ready.then(
getUserDataAndCsrfToken()
.then(({ currentUser }) => {
if (redirectableLocation() && !onboardingSkippable(currentUser)) {
window.location = `${window.location.origin}/onboarding?referrer=${window.location}`;
}
})
.catch(error => {
// eslint-disable-next-line no-console
console.error('Error getting user and CSRF Token', error);
}),
);
window.InstantClick.on('change', () => {
getUserDataAndCsrfToken()
.then(({ currentUser }) => {
if (
redirectableLocation() &&
localStorage.getItem('shouldRedirectToOnboarding') === null &&
!onboardingSkippable(currentUser)
) {
window.location = `${window.location.origin}/onboarding?referrer=${window.location}`;
}
})
.catch(error => {
// eslint-disable-next-line no-console
console.error('Error getting user and CSRF Token', error);
});
});

View file

@ -1,15 +1,17 @@
const orgCreditsSelect = document.getElementById('org-credits-select')
const orgCreditsNumber = document.getElementById('org-credits-number')
const orgCreditsLink = document.getElementById('org-credits-purchase-link')
const orgCreditsSelect = document.getElementById('org-credits-select');
const orgCreditsNumber = document.getElementById('org-credits-number');
const orgCreditsLink = document.getElementById('org-credits-purchase-link');
orgCreditsNumber.innerText = orgCreditsSelect.selectedOptions[0].dataset.credits
orgCreditsNumber.innerText =
orgCreditsSelect.selectedOptions[0].dataset.credits;
const changeOrgCredits = (event) => {
const selectedOrgCreditsCount = event.target.selectedOptions[0].dataset.credits
const selectedOrgId = event.target.selectedOptions[0].value
const changeOrgCredits = event => {
const selectedOrgCreditsCount =
event.target.selectedOptions[0].dataset.credits;
const selectedOrgId = event.target.selectedOptions[0].value;
orgCreditsNumber.innerText = selectedOrgCreditsCount
orgCreditsLink.href = `/credits/purchase?organization_id=${selectedOrgId}`
}
orgCreditsNumber.innerText = selectedOrgCreditsCount;
orgCreditsLink.href = `/credits/purchase?organization_id=${selectedOrgId}`;
};
orgCreditsSelect.addEventListener('change', changeOrgCredits)
orgCreditsSelect.addEventListener('change', changeOrgCredits);

View file

@ -1,4 +1,4 @@
import { h, render, Component } from 'preact';
import { h, Component } from 'preact';
import OnboardingWelcome from './components/OnboardingWelcome';
import OnboardingFollowTags from './components/OnboardingFollowTags';
import OnboardingFollowUsers from './components/OnboardingFollowUsers';
@ -40,7 +40,6 @@ class Onboarding extends Component {
followRequestSent: false,
articles: [],
savedArticles: [],
saveRequestSent: false,
profileInfo: {},
};
}
@ -59,6 +58,7 @@ class Onboarding extends Component {
document.body.getAttribute('data-user'),
).followed_tag_names;
function checkFollowingStatus(followedTags, jsonTags) {
if (!followedTags || !followedTags.length) return jsonTags;
const newJSON = jsonTags;
jsonTags.map((tag, index) => {
if (followedTags.includes(tag.name)) {
@ -127,7 +127,6 @@ class Onboarding extends Component {
const formData = getFormDataAndAppend([
{ key: 'user', value: JSON.stringify(profileInfo) },
]);
fetch('/onboarding_update', {
method: 'PATCH',
headers: {
@ -135,10 +134,6 @@ class Onboarding extends Component {
},
body: formData,
credentials: 'same-origin',
}).then(response => {
if (response.ok) {
this.setState({ saveRequestSent: true });
}
});
}
@ -361,6 +356,19 @@ class Onboarding extends Component {
}
}
renderCloseButton() {
const btnClassName = 'close-button';
return (
<button
className={btnClassName}
type="button"
onClick={this.closeOnboarding}
>
<img src={cancelSvg} alt="cancel button" />
</button>
);
}
renderBackButton() {
const { pageNumber } = this.state;
if (pageNumber > 1) {
@ -370,9 +378,7 @@ class Onboarding extends Component {
type="button"
onClick={this.handleBackButton}
>
{' '}
BACK
{' '}
</button>
);
}
@ -395,29 +401,24 @@ class Onboarding extends Component {
}
renderPageIndicators() {
const { pageNumber } = this.state;
const firstIndicatorClassName =
pageNumber === 2
? 'pageindicator pageindicator--active'
: 'pageindicator';
const secondIndicatorClassName =
pageNumber === 3
? 'pageindicator pageindicator--active'
: 'pageindicator';
const thirdIndicatorClassName =
pageNumber === 4
? 'pageindicator pageindicator--active'
: 'pageindicator';
return (
<div className="pageindicators">
<div
className={
this.state.pageNumber === 2
? 'pageindicator pageindicator--active'
: 'pageindicator'
}
/>
<div
className={
this.state.pageNumber === 3
? 'pageindicator pageindicator--active'
: 'pageindicator'
}
/>
<div
className={
this.state.pageNumber === 4
? 'pageindicator pageindicator--active'
: 'pageindicator'
}
/>
<div className={firstIndicatorClassName} />
<div className={secondIndicatorClassName} />
<div className={thirdIndicatorClassName} />
</div>
);
}
@ -437,46 +438,37 @@ class Onboarding extends Component {
render() {
const { showOnboarding } = this.state;
if (showOnboarding) {
return (
<div className="global-modal" style="display:none">
<div className="global-modal-bg">
<button className="close-button" onClick={this.closeOnboarding}>
<img src={cancelSvg} alt="cancel button" />
</button>
if (!showOnboarding) return null;
return (
<div className="global-modal" style={{ display: 'none' }}>
<div className="global-modal-bg">{this.renderCloseButton()}</div>
<div className="global-modal-inner">
<div className="modal-header">
<div className="triangle-isosceles">
{this.renderSloanMessage()}
</div>
</div>
<div className="global-modal-inner">
<div className="modal-header">
<div className="triangle-isosceles">
{this.renderSloanMessage()}
</div>
<div className="modal-body">
<div id="sloan-mascot-onboarding-area" className="sloan-bar wiggle">
<img
src="https://res.cloudinary.com/practicaldev/image/fetch/s--iiubRINO--/c_imagga_scale,f_auto,fl_progressive,q_auto,w_300/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/sloan.png"
className="sloan-img"
alt="Sloan, the sloth mascot"
/>
</div>
<div className="modal-body">
<div
id="sloan-mascot-onboarding-area"
className="sloan-bar wiggle"
>
<img
src="https://res.cloudinary.com/practicaldev/image/fetch/s--iiubRINO--/c_imagga_scale,f_auto,fl_progressive,q_auto,w_300/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/sloan.png"
className="sloan-img"
alt="Sloan, the sloth mascot"
/>
</div>
<div className="body-message">{this.toggleOnboardingSlide()}</div>
</div>
<div className="modal-footer">
<div className="modal-footer-left">{this.renderBackButton()}</div>
<div className="modal-footer-center">
{this.renderPageIndicators()}
</div>
<div className="modal-footer-right">
{this.renderNextButton()}
</div>
<div className="body-message">{this.toggleOnboardingSlide()}</div>
</div>
<div className="modal-footer">
<div className="modal-footer-left">{this.renderBackButton()}</div>
<div className="modal-footer-center">
{this.renderPageIndicators()}
</div>
<div className="modal-footer-right">{this.renderNextButton()}</div>
</div>
</div>
);
}
</div>
);
}
}

View file

@ -5,11 +5,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -90,7 +91,7 @@ preact-render-spy (1 nodes)
type="button"
onClick={[Function bound handleBackButton]}
>
BACK
BACK
</button>
</div>
<div class="modal-footer-center">
@ -122,11 +123,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -169,7 +171,7 @@ preact-render-spy (1 nodes)
type="button"
onClick={[Function bound handleBackButton]}
>
BACK
BACK
</button>
</div>
<div class="modal-footer-center">
@ -199,7 +201,7 @@ preact-render-spy (1 nodes)
exports[`<Onboarding /> Final page special next button exists and should reroute user 1`] = `
preact-render-spy (1 nodes)
-------
{undefined}
{null}
`;
@ -208,11 +210,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -320,7 +323,7 @@ preact-render-spy (1 nodes)
type="button"
onClick={[Function bound handleBackButton]}
>
BACK
BACK
</button>
</div>
<div class="modal-footer-center">
@ -352,11 +355,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -437,7 +441,7 @@ preact-render-spy (1 nodes)
type="button"
onClick={[Function bound handleBackButton]}
>
BACK
BACK
</button>
</div>
<div class="modal-footer-center">
@ -469,11 +473,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -588,7 +593,7 @@ preact-render-spy (1 nodes)
type="button"
onClick={[Function bound handleBackButton]}
>
BACK
BACK
</button>
</div>
<div class="modal-footer-center">
@ -620,11 +625,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -693,11 +699,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -812,7 +819,7 @@ preact-render-spy (1 nodes)
type="button"
onClick={[Function bound handleBackButton]}
>
BACK
BACK
</button>
</div>
<div class="modal-footer-center">
@ -844,11 +851,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -956,7 +964,7 @@ preact-render-spy (1 nodes)
type="button"
onClick={[Function bound handleBackButton]}
>
BACK
BACK
</button>
</div>
<div class="modal-footer-center">
@ -988,11 +996,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -1073,7 +1082,7 @@ preact-render-spy (1 nodes)
type="button"
onClick={[Function bound handleBackButton]}
>
BACK
BACK
</button>
</div>
<div class="modal-footer-center">
@ -1105,11 +1114,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -1217,7 +1227,7 @@ preact-render-spy (1 nodes)
type="button"
onClick={[Function bound handleBackButton]}
>
BACK
BACK
</button>
</div>
<div class="modal-footer-center">
@ -1249,11 +1259,12 @@ preact-render-spy (1 nodes)
-------
<div
class="global-modal"
style="display:none"
style="display: none;"
>
<div class="global-modal-bg">
<button
class="close-button"
type="button"
onClick={[Function bound closeOnboarding]}
>
<img
@ -1320,6 +1331,6 @@ preact-render-spy (1 nodes)
exports[`<Onboarding /> when user is not logged in nothing should be rendered 1`] = `
preact-render-spy (1 nodes)
-------
{undefined}
{null}
`;

View file

@ -32,8 +32,7 @@ export const ItemListItem = ({ item, children }) => {
<span className="item-tags">
{adaptedItem.tags.map(tag => (
<a className="item-tag" href={`/t/${tag}`}>
#
{tag}
#{tag}
</a>
))}
</span>

View file

@ -10,8 +10,7 @@ export const ItemListTags = ({ availableTags, selectedTags, onClick }) => {
data-no-instant
onClick={e => onClick(e, tag)}
>
#
{tag}
#{tag}
</a>
));
return <div className="tags">{tagsHTML}</div>;

View file

@ -63,9 +63,9 @@ class UnopenedChannelNotice extends Component {
if (unopenedChannels.length === 0) {
number.classList.remove('showing');
} else {
document.getElementById('connect-link').href = `/connect/${
unopenedChannels[0].adjusted_slug
}`;
document.getElementById(
'connect-link',
).href = `/connect/${unopenedChannels[0].adjusted_slug}`;
}
setTimeout(() => {
component.setState({ visible: false });
@ -118,9 +118,7 @@ class UnopenedChannelNotice extends Component {
padding: '19px 5px 14px',
}}
>
New Message from
{' '}
{channels}
New Message from {channels}
</a>
);
}
@ -134,7 +132,9 @@ function manageChannel(json) {
if (json.length > 0) {
number.classList.add('showing');
number.innerHTML = json.length;
document.getElementById('connect-link').href = `/connect/${json[0].adjusted_slug}`; // Jump the user directly to the channel where appropriate
document.getElementById(
'connect-link',
).href = `/connect/${json[0].adjusted_slug}`; // Jump the user directly to the channel where appropriate
} else {
number.classList.remove('showing');
}

View file

@ -7,6 +7,10 @@ class UserPolicy < ApplicationPolicy
true
end
def onboarding_checkbox_update?
true
end
def update?
current_user?
end

View file

@ -39,7 +39,7 @@
<% if core_pages? %>
<%= javascript_include_tag "base", defer: true %>
<% if user_signed_in? %>
<%= javascript_pack_tag "Onboarding", defer: true %>
<%= javascript_pack_tag "onboardingRedirectCheck", defer: true %>
<% end %>
<% end %>
<% if !core_pages? %>

View file

@ -0,0 +1,43 @@
<p>All participants of The DEV Community are expected to abide by our Code of Conduct, both online and during in-person events that are hosted and/or associated with DEV.</p>
<h2>Our Pledge</h2>
<p>In the interest of fostering an open and welcoming environment, we as moderators of
<a href="https://dev.to">DEV</a> pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
</p>
<h2>Our Standards</h2>
<p>Examples of behavior that contributes to creating a positive environment include:</p>
<ul>
<li>Using welcoming and inclusive language</li>
<li>Being respectful of differing viewpoints and experiences</li>
<li>Referring to people by their preferred pronouns and using gender-neutral pronouns when uncertain</li>
<li>Gracefully accepting constructive criticism</li>
<li>Focusing on what is best for the community</li>
<li>Showing empathy towards other community members</li>
</ul>
<p>Examples of unacceptable behavior by participants include:</p>
<ul>
<li>The use of sexualized language or imagery and unwelcome sexual attention or advances</li>
<li>Trolling, insulting/derogatory comments, and personal or political attacks</li>
<li>Public or private harassment</li>
<li>Publishing others' private information, such as a physical or electronic address, without explicit permission</li>
<li>Other conduct which could reasonably be considered inappropriate in a professional setting</li>
<li>Dismissing or attacking inclusion-oriented requests</li>
</ul>
<p>We pledge to prioritize marginalized peoples safety over privileged peoples comfort. We will not act on complaints regarding:</p>
<ul>
<li>Reverse -isms, including reverse racism, reverse sexism, and cisphobia</li>
<li>Reasonable communication of boundaries, such as 'leave me alone,' 'go away,' or 'Im not discussing this with you.'</li>
<li>Someones refusal to explain or debate social justice concepts</li>
<li>Criticisms of racist, sexist, cissexist, or otherwise oppressive behavior or assumptions</li>
</ul>
<h2>Enforcement</h2>
<p>Violations of the Code of Conduct may be reported by contacting the team via the
<a href="https://dev.to/report-abuse">abuse report form</a> or by sending an email to yo@dev.to. All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately.
</p>
<p>Moderators have the right and responsibility to remove comments or other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any members for other behaviors that they deem inappropriate, threatening, offensive, or harmful.</p>
<h2>Attribution</h2>
<p>This Code of Conduct is adapted from:</p>
<ul>
<li><a href="http://contributor-covenant.org/version/1/4">Contributor Covenant, version 1.4</a></li>
<li><a href="http://www.writespeakcode.com/code-of-conduct.html">Write/Speak/Code</a></li>
<li><a href="https://geekfeminism.org/about/code-of-conduct/">Geek Feminism</a></li>
</ul>

View file

@ -0,0 +1,132 @@
<h3 id="terms">
1. Terms
</h3>
<p>
By accessing this web site, you are agreeing to be bound by these
web site Terms and Conditions of Use, our
<a href="/privacy">Privacy Policy</a>, all applicable laws and regulations,
and agree that you are responsible for compliance with any applicable local
laws. If you do not agree with any of these terms, you are prohibited from
using or accessing this site. The materials contained in this web site are
protected by applicable copyright and trade mark law.
</p>
<h3 id="use-licence">
2. Use License
</h3>
<ol type="a">
<li>
Permission is granted to temporarily download one copy of the materials
(information or software) on DEV's web site for personal,
non-commercial transitory viewing only. This is the grant of a license,
not a transfer of title, and under this license you may not:
<ol type="i">
<li>modify or copy the materials;</li>
<li>use the materials for any commercial purpose, or for any public display (commercial or non-commercial);</li>
<li>attempt to decompile or reverse engineer any software contained on DEV's web site;</li>
<li>remove any copyright or other proprietary notations from the materials; or</li>
<li>transfer the materials to another person or "mirror" the materials on any other server.</li>
</ol>
</li>
<li>
This license shall automatically terminate if you violate any of these restrictions and may be terminated by DEV at any time. Upon terminating your viewing of these materials or upon the termination of this license, you must destroy any downloaded materials in your possession whether in electronic or printed format.
</li>
</ol>
<h3 id="disclaimer">
3. Disclaimer
</h3>
<ol type="a">
<li>
The materials on DEV's web site are provided "as is". DEV makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties, including without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights. Further, DEV does not warrant or make any representations concerning the accuracy, likely results, or reliability of the use of the materials on its Internet web site or otherwise relating to such materials or on any sites linked to this site.
</li>
</ol>
<h3 id="limitations">
4. Limitations
</h3>
<p>
In no event shall DEV or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption,) arising out of the use or inability to use the materials on DEV's Internet site, even if DEV or an authorized representative has been notified orally or in writing of the possibility of such damage. Because some jurisdictions do not allow limitations on implied warranties, or limitations of liability for consequential or incidental damages, these limitations may not apply to you.
</p>
<h3 id="revisions-and-errata">
5. Revisions and Errata
</h3>
<p>
The materials appearing on DEV's web site could include technical, typographical, or photographic errors. DEV does not warrant that any of the materials on its web site are accurate, complete, or current. DEV may make changes to the materials contained on its web site at any time without notice. DEV does not, however, make any commitment to update the materials.
</p>
<h3 id="links">
6. Links
</h3>
<p>
DEV has not reviewed all of the sites linked to its Internet web site and is not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement by DEV of the site. Use of any such linked web site is at the user's own risk.
</p>
<h3 id="copyright-takedown">
7. Copyright / Takedown
</h3>
<p>
Users agree and certify that they have rights to share all content that they post on dev.to — including, but not limited to, information posted in articles, discussions, and comments. This rule applies to prose, code snippets, collections of links, etc. Regardless of citation, users may not post copy and pasted content that does not belong to them. Users assume all risk for the content they post, including someone else's reliance on its accuracy, claims relating to intellectual property, or other legal rights. If you believe that a user has plagiarized content, misrepresented their identity, misappropriated work, or otherwise run afoul of DMCA regulations, please email
<a href="mailto:yo@dev.to">yo@dev.to</a>. DEV may remove any content users post for any reason.
</p>
<h3 id="site-terms-of-use-modifications">
8. Site Terms of Use Modifications
</h3>
<p>
DEV may revise these terms of use for its web site at any time without notice. By using this web site you are agreeing to be bound by the then current version of these Terms and Conditions of Use.
</p>
<h3 id="dev-trademarks-and-logo-policy">
9. DEV Trademarks and Logos Policy
</h3>
<p>
All uses of the DEV logo, DEV badges, brand slogans, iconography, and the like, may only be used with express permission from DEV. DEV reserves all rights, even if certain assets are included in DEV open source projects. Please contact yo@dev.to with any questions or to request permission.
</p>
<h3 id="reserved-names">
10. Reserved Names
</h3>
<p>
DEV has the right to maintain a list of reserved names which will not be made publicly available. These reserved names may be set aside for purposes of proactive trademark protection, avoiding user confusion, security measures, or any other reason (or no reason).
</p>
<p>
Additionally, DEV reserves the right to change any already-claimed name at its sole discretion. In such cases, DEV will make reasonable effort to find a suitable alternative and assist with any transition-related concerns.
</p>
<h3 id="content-policy">
11. Content Policy
</h3>
<p>
Users must make a good-faith effort to share content that is on-topic, of high-quality, and is not designed primarily for the purposes of promotion or creating backlinks. Additionally, posts must contain substantial content — they may not merely reference an external link that contains the full post. This policy applies to comments, articles, and and all other works shared on the DEV platform.
</p>
<p>
DEV reserves the right to remove any content that it deems to be in violation of this policy at its sole discretion. Additionally, DEV reserves the right to restrict any users ability to participate on the platform at its sole discretion.
</p>
<h3 id="governing-law">
12. Governing Law
</h3>
<p>
Any claim relating to DEV's web site shall be governed by the laws of the State of New York without regard to its conflict of law provisions.
</p>
<p>
General Terms and Conditions applicable to Use of a Web Site.
</p>

View file

@ -23,48 +23,6 @@
<h1>Code of Conduct</h1>
</div>
<div class="body">
<p>All participants of The DEV Community are expected to abide by our Code of Conduct, both online and during in-person events that are hosted and/or associated with DEV.</p>
<h2>Our Pledge</h2>
<p>In the interest of fostering an open and welcoming environment, we as moderators of
<a href="https://dev.to">DEV</a> pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
</p>
<h2>Our Standards</h2>
<p>Examples of behavior that contributes to creating a positive environment include:</p>
<ul>
<li>Using welcoming and inclusive language</li>
<li>Being respectful of differing viewpoints and experiences</li>
<li>Referring to people by their preferred pronouns and using gender-neutral pronouns when uncertain</li>
<li>Gracefully accepting constructive criticism</li>
<li>Focusing on what is best for the community</li>
<li>Showing empathy towards other community members</li>
</ul>
<p>Examples of unacceptable behavior by participants include:</p>
<ul>
<li>The use of sexualized language or imagery and unwelcome sexual attention or advances</li>
<li>Trolling, insulting/derogatory comments, and personal or political attacks</li>
<li>Public or private harassment</li>
<li>Publishing others' private information, such as a physical or electronic address, without explicit permission</li>
<li>Other conduct which could reasonably be considered inappropriate in a professional setting</li>
<li>Dismissing or attacking inclusion-oriented requests</li>
</ul>
<p>We pledge to prioritize marginalized peoples safety over privileged peoples comfort. We will not act on complaints regarding:</p>
<ul>
<li>Reverse -isms, including reverse racism, reverse sexism, and cisphobia</li>
<li>Reasonable communication of boundaries, such as 'leave me alone,' 'go away,' or 'Im not discussing this with you.'</li>
<li>Someones refusal to explain or debate social justice concepts</li>
<li>Criticisms of racist, sexist, cissexist, or otherwise oppressive behavior or assumptions</li>
</ul>
<h2>Enforcement</h2>
<p>Violations of the Code of Conduct may be reported by contacting the team via the
<a href="https://dev.to/report-abuse">abuse report form</a> or by sending an email to yo@dev.to. All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately.
</p>
<p>Moderators have the right and responsibility to remove comments or other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any members for other behaviors that they deem inappropriate, threatening, offensive, or harmful.</p>
<h2>Attribution</h2>
<p>This Code of Conduct is adapted from:</p>
<ul>
<li><a href="http://contributor-covenant.org/version/1/4">Contributor Covenant, version 1.4</a></li>
<li><a href="http://www.writespeakcode.com/code-of-conduct.html">Write/Speak/Code</a></li>
<li><a href="https://geekfeminism.org/about/code-of-conduct/">Geek Feminism</a></li>
</ul>
<%= render "coc_text" %>
</div>
</div>

View file

@ -0,0 +1,26 @@
<% title "Welcome to #{ApplicationConfig["COMMUNITY_NAME"]}" %>
<style>
<% cache "onboarding-css-#{ApplicationConfig['DEPLOYMENT_SIGNATURE']}", expires_in: 10.hours do %>
<%= Rails.application.assets["onboarding.css"].to_s.html_safe %>
<%= Rails.application.assets["preact/onboarding-modal.css"].to_s.html_safe %>
<% end %>
footer {
display: none;
}
.top-bar {
display: none;
}
</style>
<div id="onboarding-container">
<%= javascript_pack_tag "Onboarding", defer: true %>
</div>
<div id="terms" style="display: none;">
<%= render "terms_text" %>
</div>
<div id="coc" style="display: none;">
<%= render "coc_text" %>
</div>

View file

@ -29,137 +29,6 @@
</h1>
</div>
<div class="body">
<h3 id="terms">
1. Terms
</h3>
<p>
By accessing this web site, you are agreeing to be bound by these
web site Terms and Conditions of Use, our
<a href="/privacy">Privacy Policy</a>, all applicable laws and regulations,
and agree that you are responsible for compliance with any applicable local
laws. If you do not agree with any of these terms, you are prohibited from
using or accessing this site. The materials contained in this web site are
protected by applicable copyright and trade mark law.
</p>
<h3 id="use-licence">
2. Use License
</h3>
<ol type="a">
<li>
Permission is granted to temporarily download one copy of the materials
(information or software) on DEV's web site for personal,
non-commercial transitory viewing only. This is the grant of a license,
not a transfer of title, and under this license you may not:
<ol type="i">
<li>modify or copy the materials;</li>
<li>use the materials for any commercial purpose, or for any public display (commercial or non-commercial);</li>
<li>attempt to decompile or reverse engineer any software contained on DEV's web site;</li>
<li>remove any copyright or other proprietary notations from the materials; or</li>
<li>transfer the materials to another person or "mirror" the materials on any other server.</li>
</ol>
</li>
<li>
This license shall automatically terminate if you violate any of these restrictions and may be terminated by DEV at any time. Upon terminating your viewing of these materials or upon the termination of this license, you must destroy any downloaded materials in your possession whether in electronic or printed format.
</li>
</ol>
<h3 id="disclaimer">
3. Disclaimer
</h3>
<ol type="a">
<li>
The materials on DEV's web site are provided "as is". DEV makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties, including without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights. Further, DEV does not warrant or make any representations concerning the accuracy, likely results, or reliability of the use of the materials on its Internet web site or otherwise relating to such materials or on any sites linked to this site.
</li>
</ol>
<h3 id="limitations">
4. Limitations
</h3>
<p>
In no event shall DEV or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption,) arising out of the use or inability to use the materials on DEV's Internet site, even if DEV or an authorized representative has been notified orally or in writing of the possibility of such damage. Because some jurisdictions do not allow limitations on implied warranties, or limitations of liability for consequential or incidental damages, these limitations may not apply to you.
</p>
<h3 id="revisions-and-errata">
5. Revisions and Errata
</h3>
<p>
The materials appearing on DEV's web site could include technical, typographical, or photographic errors. DEV does not warrant that any of the materials on its web site are accurate, complete, or current. DEV may make changes to the materials contained on its web site at any time without notice. DEV does not, however, make any commitment to update the materials.
</p>
<h3 id="links">
6. Links
</h3>
<p>
DEV has not reviewed all of the sites linked to its Internet web site and is not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement by DEV of the site. Use of any such linked web site is at the user's own risk.
</p>
<h3 id="copyright-takedown">
7. Copyright / Takedown
</h3>
<p>
Users agree and certify that they have rights to share all content that they post on dev.to — including, but not limited to, information posted in articles, discussions, and comments. This rule applies to prose, code snippets, collections of links, etc. Regardless of citation, users may not post copy and pasted content that does not belong to them. Users assume all risk for the content they post, including someone else's reliance on its accuracy, claims relating to intellectual property, or other legal rights. If you believe that a user has plagiarized content, misrepresented their identity, misappropriated work, or otherwise run afoul of DMCA regulations, please email
<a href="mailto:yo@dev.to">yo@dev.to</a>. DEV may remove any content users post for any reason.
</p>
<h3 id="site-terms-of-use-modifications">
8. Site Terms of Use Modifications
</h3>
<p>
DEV may revise these terms of use for its web site at any time without notice. By using this web site you are agreeing to be bound by the then current version of these Terms and Conditions of Use.
</p>
<h3 id="dev-trademarks-and-logo-policy">
9. DEV Trademarks and Logos Policy
</h3>
<p>
All uses of the DEV logo, DEV badges, brand slogans, iconography, and the like, may only be used with express permission from DEV. DEV reserves all rights, even if certain assets are included in DEV open source projects. Please contact yo@dev.to with any questions or to request permission.
</p>
<h3 id="reserved-names">
10. Reserved Names
</h3>
<p>
DEV has the right to maintain a list of reserved names which will not be made publicly available. These reserved names may be set aside for purposes of proactive trademark protection, avoiding user confusion, security measures, or any other reason (or no reason).
</p>
<p>
Additionally, DEV reserves the right to change any already-claimed name at its sole discretion. In such cases, DEV will make reasonable effort to find a suitable alternative and assist with any transition-related concerns.
</p>
<h3 id="content-policy">
11. Content Policy
</h3>
<p>
Users must make a good-faith effort to share content that is on-topic, of high-quality, and is not designed primarily for the purposes of promotion or creating backlinks. Additionally, posts must contain substantial content — they may not merely reference an external link that contains the full post. This policy applies to comments, articles, and and all other works shared on the DEV platform.
</p>
<p>
DEV reserves the right to remove any content that it deems to be in violation of this policy at its sole discretion. Additionally, DEV reserves the right to restrict any users ability to participate on the platform at its sole discretion.
</p>
<h3 id="governing-law">
12. Governing Law
</h3>
<p>
Any claim relating to DEV's web site shall be governed by the laws of the State of New York without regard to its conflict of law provisions.
</p>
<p>
General Terms and Conditions applicable to Use of a Web Site.
</p>
<%= render "terms_text" %>
</div>
</div>

View file

@ -25,6 +25,7 @@ class ReservedWords
contact
merch
onboarding_update
onboarding_checkbox_update
rlygenerator
orlygenerator
rlyslack

View file

@ -166,6 +166,7 @@ Rails.application.routes.draw do
get "/notification_subscriptions/:notifiable_type/:notifiable_id" => "notification_subscriptions#show"
post "/notification_subscriptions/:notifiable_type/:notifiable_id" => "notification_subscriptions#upsert"
patch "/onboarding_update" => "users#onboarding_update"
patch "/onboarding_checkbox_update" => "users#onboarding_checkbox_update"
get "email_subscriptions/unsubscribe"
post "/chat_channels/:id/moderate" => "chat_channels#moderate"
post "/chat_channels/:id/open" => "chat_channels#open"
@ -249,6 +250,7 @@ Rails.application.routes.draw do
get "/welcome" => "pages#welcome"
get "/challenge" => "pages#challenge"
get "/badge" => "pages#badge"
get "/onboarding" => "pages#onboarding"
get "/shecoded" => "pages#shecoded"
get "/💸", to: redirect("t/hiring")
get "/security", to: "pages#bounty"

View file

@ -0,0 +1,5 @@
class AddOnboardingChecklistToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :onboarding_checklist, :string, array: true, default: []
end
end

View file

@ -0,0 +1,5 @@
class AddCheckedTermsAndConditionsToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :checked_terms_and_conditions, :bool, default: false
end
end

View file

@ -0,0 +1,5 @@
class AddLastOnboardingPageToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :last_onboarding_page, :string
end
end

View file

@ -937,6 +937,7 @@ ActiveRecord::Schema.define(version: 2019_07_23_094834) do
t.string "bg_color_hex"
t.text "cached_chat_channel_memberships"
t.boolean "checked_code_of_conduct", default: false
t.boolean "checked_terms_and_conditions", default: false
t.integer "comments_count", default: 0, null: false
t.string "config_font", default: "default"
t.string "config_theme", default: "default"
@ -997,6 +998,7 @@ ActiveRecord::Schema.define(version: 2019_07_23_094834) do
t.datetime "last_followed_at"
t.datetime "last_moderation_notification", default: "2017-01-01 05:00:00"
t.datetime "last_notification_activity"
t.string "last_onboarding_page"
t.datetime "last_sign_in_at"
t.inet "last_sign_in_ip"
t.string "linkedin_url"
@ -1012,6 +1014,7 @@ ActiveRecord::Schema.define(version: 2019_07_23_094834) do
t.string "name"
t.string "old_old_username"
t.string "old_username"
t.string "onboarding_checklist", default: [], array: true
t.datetime "onboarding_package_form_submmitted_at"
t.boolean "onboarding_package_fulfilled", default: false
t.boolean "onboarding_package_requested", default: false

View file

@ -20,6 +20,8 @@ FactoryBot.define do
website_url { Faker::Internet.url }
confirmed_at { Time.current }
saw_onboarding { true }
checked_code_of_conduct { true }
checked_terms_and_conditions { true }
signup_cta_variant { "navbar_basic" }
email_digest_periodic { false }

View file

@ -8,7 +8,7 @@ RSpec.describe "FollowsApi", type: :request do
let(:user4) { create(:user) }
let(:user5) { create(:user) }
let(:users_hash) do
[{ id: user2.id }, { id: user3.id }, { id: user4.id }, { id: user5.id }].to_json
[{ id: user2.id }, { id: user3.id }, { id: user4.id }, { id: user5.id }]
end
it "returns empty if user not signed in" do
@ -27,7 +27,7 @@ RSpec.describe "FollowsApi", type: :request do
run_background_jobs_immediately do
post "/api/follows", params: { users: users_hash }
end
expect(Follow.all.size).to eq(JSON.parse(users_hash).size)
expect(Follow.all.size).to eq(users_hash.size)
end
end
end

View file

@ -39,8 +39,7 @@ RSpec.describe "Authenticating with twitter" do
visit root_path
click_link "Sign In With Twitter"
expect(page).to have_link("Write your first post now")
expect(page).to have_link("Welcome Thread")
expect(page.html).to include("onboarding-container")
end
it "logging in with twitter using invalid credentials" do