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:
parent
6cc0d125b5
commit
52c60ce37e
78 changed files with 3050 additions and 1326 deletions
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -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. -->
|
||||
|
|
|
|||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -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. -->
|
||||
|
|
|
|||
BIN
app/assets/images/mobile-onboarding-background.png
Normal file
BIN
app/assets/images/mobile-onboarding-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
app/assets/images/onboarding-background-white.png
Normal file
BIN
app/assets/images/onboarding-background-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 339 KiB |
BIN
app/assets/images/onboarding-background.png
Normal file
BIN
app/assets/images/onboarding-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 369 KiB |
BIN
app/assets/images/purple-dev-logo.png
Normal file
BIN
app/assets/images/purple-dev-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -29,5 +29,6 @@ module.exports = {
|
|||
filterXSS: false,
|
||||
Pusher: false,
|
||||
algoliasearch: false,
|
||||
ga: false,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"> </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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
));
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
69
app/javascript/onboarding/Onboarding.jsx
Normal file
69
app/javascript/onboarding/Onboarding.jsx
Normal 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>;
|
||||
}
|
||||
}
|
||||
317
app/javascript/onboarding/__tests__/Onboarding.test.jsx
Normal file
317
app/javascript/onboarding/__tests__/Onboarding.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
You‘re 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. It’s 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>
|
||||
|
||||
`;
|
||||
88
app/javascript/onboarding/components/BioForm.jsx
Normal file
88
app/javascript/onboarding/components/BioForm.jsx
Normal 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'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;
|
||||
94
app/javascript/onboarding/components/ClosingSlide.jsx
Normal file
94
app/javascript/onboarding/components/ClosingSlide.jsx
Normal 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‘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;
|
||||
|
|
@ -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;
|
||||
123
app/javascript/onboarding/components/FollowTags.jsx
Normal file
123
app/javascript/onboarding/components/FollowTags.jsx
Normal 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;
|
||||
143
app/javascript/onboarding/components/FollowUsers.jsx
Normal file
143
app/javascript/onboarding/components/FollowUsers.jsx
Normal 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;
|
||||
68
app/javascript/onboarding/components/IntroSlide.jsx
Normal file
68
app/javascript/onboarding/components/IntroSlide.jsx
Normal 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. It’s
|
||||
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;
|
||||
31
app/javascript/onboarding/components/Navigation.jsx
Normal file
31
app/javascript/onboarding/components/Navigation.jsx
Normal 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;
|
||||
129
app/javascript/onboarding/components/PersonalInfoForm.jsx
Normal file
129
app/javascript/onboarding/components/PersonalInfoForm.jsx
Normal 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;
|
||||
8
app/javascript/onboarding/utilities.js
Normal file
8
app/javascript/onboarding/utilities.js
Normal 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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
54
app/javascript/packs/onboardingRedirectCheck.jsx
Normal file
54
app/javascript/packs/onboardingRedirectCheck.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ class UserPolicy < ApplicationPolicy
|
|||
true
|
||||
end
|
||||
|
||||
def onboarding_checkbox_update?
|
||||
true
|
||||
end
|
||||
|
||||
def update?
|
||||
current_user?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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? %>
|
||||
|
|
|
|||
43
app/views/pages/_coc_text.html.erb
Normal file
43
app/views/pages/_coc_text.html.erb
Normal 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 people’s safety over privileged people’s 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 'I’m not discussing this with you.'</li>
|
||||
<li>Someone’s 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>
|
||||
132
app/views/pages/_terms_text.html.erb
Normal file
132
app/views/pages/_terms_text.html.erb
Normal 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 user’s 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>
|
||||
|
|
@ -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 people’s safety over privileged people’s 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 'I’m not discussing this with you.'</li>
|
||||
<li>Someone’s 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>
|
||||
|
|
|
|||
26
app/views/pages/onboarding.html.erb
Normal file
26
app/views/pages/onboarding.html.erb
Normal 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>
|
||||
|
|
@ -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 user’s 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>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class ReservedWords
|
|||
contact
|
||||
merch
|
||||
onboarding_update
|
||||
onboarding_checkbox_update
|
||||
rlygenerator
|
||||
orlygenerator
|
||||
rlyslack
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
class AddOnboardingChecklistToUsers < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :users, :onboarding_checklist, :string, array: true, default: []
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class AddCheckedTermsAndConditionsToUsers < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :users, :checked_terms_and_conditions, :bool, default: false
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
class AddLastOnboardingPageToUsers < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :users, :last_onboarding_page, :string
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue