Add users to signup modal (and other adjustments) (#124)
* Add a missing bounty hunter * Add a msg about updating social usernames * Add mailto: link for yo@dev in terms * Use render partial for help tag * Lint a file * Add first draft of onboarding follow users * WIP onboarding * Change users api controller * Create FollowSuggester to return suggested follows for user * Create initial follow users flow in onboarding * Move follow user logic to top lvl * Add follow api end point for onboarding * Add profile img and update follow action * Remove checkbox and use a tag instead * Lint and update tests * Remove incomplete test for now * Final adjustments to signup modal * Update jest snapshot * Add more jest tests and fix typo
This commit is contained in:
parent
f5a1dcb1da
commit
8a1f9991f1
23 changed files with 699 additions and 21 deletions
|
|
@ -178,7 +178,7 @@
|
|||
font-size:28px;
|
||||
font-weight:400;
|
||||
}
|
||||
a{
|
||||
a.big{
|
||||
display:block;
|
||||
border: 3px solid $black;
|
||||
font-size:2.2em;
|
||||
|
|
@ -191,6 +191,7 @@
|
|||
img{
|
||||
width: 90%;
|
||||
max-width: 270px;
|
||||
min-height: 350px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@
|
|||
padding-left: calc(3% + 2px);
|
||||
max-width: 730px;
|
||||
float:left;
|
||||
font-size: calc(0.8em + 6px);
|
||||
}
|
||||
|
||||
.tags-slide {
|
||||
|
|
@ -107,6 +108,10 @@
|
|||
height: 40vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
font-size: calc(0.77em + 6px);
|
||||
@media screen and (min-width: 900px) {
|
||||
font-size: 27px;
|
||||
}
|
||||
.col-1{
|
||||
float: left;
|
||||
width: 50%;
|
||||
|
|
@ -165,6 +170,9 @@
|
|||
font-weight:900;
|
||||
line-height:26px;
|
||||
font-size:1.1em;
|
||||
@media screen and (min-width: 900px) {
|
||||
top: 6px;
|
||||
}
|
||||
&.following-butt {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
top:8px;
|
||||
|
|
@ -178,6 +186,65 @@
|
|||
|
||||
}
|
||||
|
||||
.onboarding-user-container{
|
||||
margin-top: calc(0.5vw + 8px);
|
||||
text-align: left;
|
||||
.onboarding-user-cta{
|
||||
font-size: 0.9em;
|
||||
color: $black;
|
||||
margin: 20px auto 25px;
|
||||
}
|
||||
.onboarding-user-list{
|
||||
color: $black;
|
||||
font-size: 0.7em;
|
||||
overflow: scroll;
|
||||
border: 1px solid darken($purple, 20%);
|
||||
box-shadow: 4px 5px 0px darken($purple, 20%);
|
||||
.onboarding-user-list-body{
|
||||
height: calc(39vh - 20px);
|
||||
overflow: scroll;
|
||||
}
|
||||
.onboarding-user-list-row{
|
||||
padding: 10px 0px;
|
||||
img{
|
||||
height: 33px;
|
||||
width: 33px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
vertical-align: -0.5em;
|
||||
margin-right: 10px;
|
||||
}
|
||||
&:nth-child(odd){
|
||||
background: $light-gray;
|
||||
}
|
||||
&.onboarding-user-list-header{
|
||||
background: $purple;
|
||||
font-size:1.1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.onboarding-user-list-key{
|
||||
width: calc(100% - 80px);
|
||||
display: inline-block;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.onboarding-user-list-checkbox{
|
||||
width: 70px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
button{
|
||||
border: 0px;
|
||||
background: transparent;
|
||||
font-size:1.1em;
|
||||
&.checked{
|
||||
background: $green;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
height:10%;
|
||||
display:block;
|
||||
|
|
@ -203,7 +270,7 @@
|
|||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: calc(1vw + 20px);
|
||||
padding: 4px 6px;
|
||||
padding: 4px 10px;
|
||||
a {
|
||||
color: black;
|
||||
}
|
||||
|
|
|
|||
15
app/controllers/api/v0/follows_controller.rb
Normal file
15
app/controllers/api/v0/follows_controller.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
module Api
|
||||
module V0
|
||||
class FollowsController < ApplicationController
|
||||
def create
|
||||
return unless user_signed_in?
|
||||
users = JSON.parse(params[:users])
|
||||
users.each do |user_hash|
|
||||
followable = User.find(user_hash['id'])
|
||||
current_user.delay.follow(followable)
|
||||
end
|
||||
render json: { outcome: "followed 50 users" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
app/controllers/api/v0/users_controller.rb
Normal file
15
app/controllers/api/v0/users_controller.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
module Api
|
||||
module V0
|
||||
class UsersController < ApplicationController
|
||||
def index
|
||||
unless user_signed_in?
|
||||
@users = []
|
||||
return
|
||||
end
|
||||
if params[:state] == "follow_suggestions"
|
||||
@users = UserFollowSuggester.new(current_user).suggestions
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -43,11 +43,11 @@ class UsersController < ApplicationController
|
|||
current_user.saw_onboarding = true
|
||||
if current_user.save!
|
||||
respond_to do |format|
|
||||
format.json { render json: { outcome: 'onboarding closed' } }
|
||||
format.json { render json: { outcome: "onboarding closed" } }
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.json { render json: { outcome: 'onboarding opened' } }
|
||||
format.json { render json: { outcome: "onboarding opened" } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { h, render, Component } from 'preact';
|
||||
import OnboardingWelcome from './components/OnboardingWelcome';
|
||||
import OnboardingFollowTags from './components/OnboardingFollowTags';
|
||||
import OnboardingFollowUsers from './components/OnboardingFollowUsers';
|
||||
import OnboardingWelcomeThread from './components/OnboardingWelcomeThread';
|
||||
import cancelSvg from '../../assets/images/cancel.svg';
|
||||
|
||||
|
|
@ -11,13 +12,19 @@ class Onboarding extends Component {
|
|||
this.handleBackButton = this.handleBackButton.bind(this);
|
||||
this.closeOnboarding = this.closeOnboarding.bind(this);
|
||||
this.handleFollowTag = this.handleFollowTag.bind(this);
|
||||
this.handleNextHover = this.handleNextHover.bind(this);
|
||||
this.updateUserData = this.updateUserData.bind(this);
|
||||
this.getUserTags = this.getUserTags.bind(this);
|
||||
this.handleCheckAllUsers = this.handleCheckAllUsers.bind(this);
|
||||
this.handleCheckUser = this.handleCheckUser.bind(this);
|
||||
this.getUsersToFollow = this.getUsersToFollow.bind(this);
|
||||
this.state = {
|
||||
pageNumber: 1,
|
||||
showOnboarding: false,
|
||||
userData: null,
|
||||
allTags: [],
|
||||
users: [],
|
||||
checkedUsers: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -45,11 +52,47 @@ class Onboarding extends Component {
|
|||
const updatedJSON = checkFollowingStatus(followedTagNames, json);
|
||||
this.setState({ allTags: updatedJSON });
|
||||
})
|
||||
.catch((huh) => {
|
||||
console.log(huh);
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
getUsersToFollow() {
|
||||
fetch('/api/users?state=follow_suggestions', {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
this.setState({ users: json, checkedUsers: json });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
handleBulkFollowUsers(users) {
|
||||
if (this.state.checkedUsers.length > 0) {
|
||||
const csrfToken = document.querySelector("meta[name='csrf-token']").content;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('users', JSON.stringify(users));
|
||||
|
||||
fetch('/api/follows', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: formData,
|
||||
credentials: 'same-origin',
|
||||
}).then(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateUserData() {
|
||||
this.setState({ userData: JSON.parse(document.body.getAttribute('data-user')) });
|
||||
if (this.state.userData.saw_onboarding === true) {
|
||||
|
|
@ -97,10 +140,35 @@ class Onboarding extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
handleCheckAllUsers() {
|
||||
if (this.state.checkedUsers.length < 50) {
|
||||
this.setState({ checkedUsers: this.state.users.slice() });
|
||||
} else {
|
||||
this.setState({ checkedUsers: [] });
|
||||
}
|
||||
}
|
||||
|
||||
handleCheckUser(user) {
|
||||
const newCheckedUsers = this.state.checkedUsers.slice();
|
||||
if (this.state.checkedUsers.indexOf(user) > -1) {
|
||||
const index = newCheckedUsers.indexOf(user);
|
||||
newCheckedUsers.splice(index, 1);
|
||||
} else {
|
||||
newCheckedUsers.push(user);
|
||||
}
|
||||
this.setState({ checkedUsers: newCheckedUsers });
|
||||
}
|
||||
|
||||
handleNextButton() {
|
||||
if (this.state.pageNumber < 3) {
|
||||
|
||||
if (this.state.pageNumber === 2 && this.state.users.length === 0) {
|
||||
this.getUsersToFollow();
|
||||
}
|
||||
|
||||
if (this.state.pageNumber < 4) {
|
||||
this.setState({ pageNumber: this.state.pageNumber + 1 });
|
||||
} else if (this.state.pageNumber === 3) {
|
||||
} else if (this.state.pageNumber === 4) {
|
||||
this.closeOnboarding();
|
||||
}
|
||||
}
|
||||
|
|
@ -112,6 +180,7 @@ class Onboarding extends Component {
|
|||
}
|
||||
|
||||
closeOnboarding() {
|
||||
this.handleBulkFollowUsers(this.state.checkedUsers);
|
||||
const csrfToken = document.querySelector("meta[name='csrf-token']").content;
|
||||
document.getElementsByTagName('body')[0].classList.remove('modal-open');
|
||||
const formData = new FormData();
|
||||
|
|
@ -149,6 +218,15 @@ class Onboarding extends Component {
|
|||
/>
|
||||
);
|
||||
} else if (this.state.pageNumber === 3) {
|
||||
return (
|
||||
<OnboardingFollowUsers
|
||||
users={this.state.users}
|
||||
checkedUsers={this.state.checkedUsers}
|
||||
handleCheckUser={this.handleCheckUser}
|
||||
handleCheckAllUsers={this.handleCheckAllUsers}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.pageNumber === 4) {
|
||||
return (
|
||||
<OnboardingWelcomeThread />
|
||||
);
|
||||
|
|
@ -163,11 +241,17 @@ class Onboarding extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleNextHover() {
|
||||
if (this.state.pageNumber === 2) {
|
||||
this.getUsersToFollow();
|
||||
}
|
||||
}
|
||||
|
||||
renderNextButton() {
|
||||
const onclick = this.handleNextButton;
|
||||
return (
|
||||
<button className="button cta" onClick={this.handleNextButton}>
|
||||
{this.state.pageNumber < 3 ? 'NEXT' : <a href="/welcome" data-no-instant>LET'S GO</a>}
|
||||
<button className="button cta" onClick={this.handleNextButton} onMouseOver={this.handleNextHover}>
|
||||
{this.state.pageNumber < 4 ? 'NEXT' : "LET'S GO"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -176,7 +260,8 @@ class Onboarding extends Component {
|
|||
const messages = {
|
||||
1: 'WELCOME!',
|
||||
2: 'FOLLOW TAGS!',
|
||||
3: 'GET INVOLVED!',
|
||||
3: 'FOLLOW SOME DEVS!',
|
||||
4: 'GET INVOLVED!',
|
||||
};
|
||||
return messages[this.state.pageNumber];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,16 +94,19 @@ describe('<Onboarding />', () => {
|
|||
expect(context.state('pageNumber')).toEqual(2);
|
||||
expect(context.find('[onClick]')).toMatchSnapshot();
|
||||
// going to page three
|
||||
context.find('.button').at(1).simulate('click');
|
||||
expect(context.state('pageNumber')).toEqual(3);
|
||||
// going to page four
|
||||
context.find('.button').at(1).simulate('click');
|
||||
expect(context.state('pageNumber')).toEqual(4);
|
||||
fetch.mockResponse(JSON.stringify({ outcome: 'onboarding closed' }));
|
||||
context.find('.button').at(1).simulate('click');
|
||||
expect(context.state('pageNumber')).toEqual(3);
|
||||
// going back to page two
|
||||
context.find('.button').at(0).simulate('click');
|
||||
expect(context.state('pageNumber')).toEqual(2);
|
||||
// going back to page three
|
||||
context.find('.button').at(1).simulate('click');
|
||||
context.find('.button').at(0).simulate('click');
|
||||
expect(context.state('pageNumber')).toEqual(3);
|
||||
// evaluting the button text on page 3
|
||||
// going back to page four
|
||||
context.find('.button').at(1).simulate('click');
|
||||
expect(context.state('pageNumber')).toEqual(4);
|
||||
// evaluting the button text on page 4
|
||||
expect(context.find('[onClick]').at(2).text()).toEqual("LET'S GO");
|
||||
context.find('.button').at(1).simulate('click');
|
||||
// clicking the final button
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ FindWrapper {
|
|||
"attributes": Object {
|
||||
"className": "button cta",
|
||||
"onClick": [Function],
|
||||
"onMouseOver": [Function],
|
||||
},
|
||||
"children": Array [
|
||||
"NEXT",
|
||||
|
|
@ -50,6 +51,7 @@ FindWrapper {
|
|||
exports[`<Onboarding /> display if there is current user 1`] = `
|
||||
Object {
|
||||
"allTags": Array [],
|
||||
"checkedUsers": Array [],
|
||||
"pageNumber": 1,
|
||||
"showOnboarding": true,
|
||||
"userData": Object {
|
||||
|
|
@ -68,5 +70,6 @@ Object {
|
|||
},
|
||||
],
|
||||
},
|
||||
"users": Array [],
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
13
app/javascript/src/components/OnboardingFollowUsers.jsx
Normal file
13
app/javascript/src/components/OnboardingFollowUsers.jsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { h } from 'preact';
|
||||
import OnboardingUsers from './OnboardingUsers';
|
||||
|
||||
const OnboardingFollowUsers = ({ users, checkedUsers, handleCheckUser, handleCheckAllUsers }) => (
|
||||
<OnboardingUsers
|
||||
users={users}
|
||||
checkedUsers={checkedUsers}
|
||||
handleCheckUser={handleCheckUser}
|
||||
handleCheckAllUsers={handleCheckAllUsers}
|
||||
/>
|
||||
);
|
||||
|
||||
export default OnboardingFollowUsers;
|
||||
|
|
@ -16,7 +16,7 @@ class OnboardingTags extends Component {
|
|||
return (
|
||||
<div className="tags-slide">
|
||||
<p>
|
||||
<strong class="yellow">What topics are you interested in?</strong> Here are a few tags to follow (There are plenty more you can choose from later)
|
||||
<strong class="yellow">What topics are you interested in?</strong> Here are a few tags to follow (There are plenty more you can choose from at any time)
|
||||
</p>
|
||||
<div className="tags-col-container">
|
||||
<div className="col-1">
|
||||
|
|
|
|||
63
app/javascript/src/components/OnboardingUsers.jsx
Normal file
63
app/javascript/src/components/OnboardingUsers.jsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { h, render, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class OnboardingUsers extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleAllClick = this.handleAllClick.bind(this);
|
||||
}
|
||||
|
||||
handleAllClick() {
|
||||
this.props.handleCheckAllUsers();
|
||||
}
|
||||
|
||||
render() {
|
||||
const followList = this.props.users.map((user) => {
|
||||
return (
|
||||
<div className="onboarding-user-list-row" key={user.id} >
|
||||
<div className="onboarding-user-list-key">
|
||||
<img
|
||||
src={user.profile_image_url}
|
||||
alt={user.name}
|
||||
/>
|
||||
{user.name}
|
||||
</div>
|
||||
<div className="onboarding-user-list-checkbox">
|
||||
<button onClick={this.props.handleCheckUser.bind(this, user)} className={this.props.checkedUsers.indexOf(user) > -1 ? 'checked' : ''}>
|
||||
{this.props.checkedUsers.indexOf(user) > -1 ? '✓' : '+'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="onboarding-user-container">
|
||||
<div className="onboarding-user-cta">
|
||||
Here are some folks from the community you might want to follow <span class="yellow">based on your interests</span>
|
||||
</div>
|
||||
<div className="onboarding-user-list">
|
||||
<div className="onboarding-user-list-header onboarding-user-list-row">
|
||||
<div className="onboarding-user-list-key">
|
||||
Follow All
|
||||
</div>
|
||||
<div className="onboarding-user-list-checkbox">
|
||||
<button id="onboarding-user-follow-all-btn" onClick={this.handleAllClick} className={this.props.checkedUsers.length === this.props.users.length ? 'checked' : ''}>
|
||||
{this.props.checkedUsers.length === this.props.users.length ? '✓' : '+'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="onboarding-user-list-body">{followList}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OnboardingUsers.propTypes = {
|
||||
handleCheckUser: PropTypes.func.isRequired,
|
||||
handleCheckAllUsers: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
export default OnboardingUsers;
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import OnboardingFollowUsers from '../OnboardingFollowUsers';
|
||||
|
||||
describe('<OnboardingFollowUsers />', () => {
|
||||
it('renders properly when given users', () => {
|
||||
const users = [
|
||||
{
|
||||
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 checkedUsers = [
|
||||
{
|
||||
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 handleCheckUser = jest.fn();
|
||||
const handleCheckAllUsers = jest.fn();
|
||||
const tree = render(<OnboardingFollowUsers
|
||||
users={users}
|
||||
checkedUsers={checkedUsers}
|
||||
handleCheckUser={handleCheckUser}
|
||||
handleCheckAllUsers={handleCheckAllUsers}
|
||||
/>);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { h } from 'preact';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import { render } from 'preact-render-to-json';
|
||||
import OnboardingUsers from '../OnboardingUsers';
|
||||
|
||||
describe('<OnboardingUsers />', () => {
|
||||
const users = [
|
||||
{
|
||||
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 checkedUsers = [
|
||||
{
|
||||
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 handleCheckUser = jest.fn();
|
||||
const handleCheckAllUsers = jest.fn();
|
||||
|
||||
describe('when given users to follow', () => {
|
||||
it('renders correctly', () => {
|
||||
const context = render(<OnboardingUsers
|
||||
users={users}
|
||||
checkedUsers={checkedUsers}
|
||||
handleCheckUser={handleCheckUser}
|
||||
handleCheckAllUsers={handleCheckAllUsers}
|
||||
/>);
|
||||
expect(context).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('responds to clicking Follow All', () => {
|
||||
const context = shallow(<OnboardingUsers
|
||||
users={users}
|
||||
checkedUsers={checkedUsers}
|
||||
handleCheckUser={handleCheckUser}
|
||||
handleCheckAllUsers={handleCheckAllUsers}
|
||||
/>);
|
||||
expect(context.find('#onboarding-user-follow-all-btn').text()).toEqual('✓');
|
||||
context.find('#onboarding-user-follow-all-btn').simulate('click');
|
||||
expect(handleCheckAllUsers).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -10,7 +10,7 @@ exports[`<OnboardingFollowTags /> renders properly when given a tag 1`] = `
|
|||
>
|
||||
What topics are you interested in?
|
||||
</strong>
|
||||
Here are a few tags to follow (There are plenty more you can choose from later)
|
||||
Here are a few tags to follow (There are plenty more you can choose from at any time)
|
||||
</p>
|
||||
<div
|
||||
class="tags-col-container"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<OnboardingFollowUsers /> renders properly when given users 1`] = `
|
||||
<div
|
||||
class="onboarding-user-container"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-cta"
|
||||
>
|
||||
Here are some folks from the community you might want to follow
|
||||
<span
|
||||
class="yellow"
|
||||
>
|
||||
based on your interests
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-header onboarding-user-list-row"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-key"
|
||||
>
|
||||
Follow All
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-checkbox"
|
||||
>
|
||||
<button
|
||||
class="checked"
|
||||
id="onboarding-user-follow-all-btn"
|
||||
onClick={[Function]}
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-body"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-row"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-key"
|
||||
>
|
||||
<img
|
||||
alt="Ben Halpern"
|
||||
src="ben.jpg"
|
||||
/>
|
||||
Ben Halpern
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-checkbox"
|
||||
>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-row"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-key"
|
||||
>
|
||||
<img
|
||||
alt="Krusty the Clown"
|
||||
src="clown.jpg"
|
||||
/>
|
||||
Krusty the Clown
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-checkbox"
|
||||
>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-row"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-key"
|
||||
>
|
||||
<img
|
||||
alt="dev.to staff"
|
||||
src="dev.jpg"
|
||||
/>
|
||||
dev.to staff
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-checkbox"
|
||||
>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<OnboardingUsers /> when given users to follow renders correctly 1`] = `
|
||||
<div
|
||||
class="onboarding-user-container"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-cta"
|
||||
>
|
||||
Here are some folks from the community you might want to follow
|
||||
<span
|
||||
class="yellow"
|
||||
>
|
||||
based on your interests
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-header onboarding-user-list-row"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-key"
|
||||
>
|
||||
Follow All
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-checkbox"
|
||||
>
|
||||
<button
|
||||
class="checked"
|
||||
id="onboarding-user-follow-all-btn"
|
||||
onClick={[Function]}
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-body"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-row"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-key"
|
||||
>
|
||||
<img
|
||||
alt="Ben Halpern"
|
||||
src="ben.jpg"
|
||||
/>
|
||||
Ben Halpern
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-checkbox"
|
||||
>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-row"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-key"
|
||||
>
|
||||
<img
|
||||
alt="Krusty the Clown"
|
||||
src="clown.jpg"
|
||||
/>
|
||||
Krusty the Clown
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-checkbox"
|
||||
>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-row"
|
||||
>
|
||||
<div
|
||||
class="onboarding-user-list-key"
|
||||
>
|
||||
<img
|
||||
alt="dev.to staff"
|
||||
src="dev.jpg"
|
||||
/>
|
||||
dev.to staff
|
||||
</div>
|
||||
<div
|
||||
class="onboarding-user-list-checkbox"
|
||||
>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
41
app/labor/user_follow_suggester.rb
Normal file
41
app/labor/user_follow_suggester.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
class UserFollowSuggester
|
||||
|
||||
attr_accessor :user
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def suggestions
|
||||
user_ids = tagged_article_user_ids
|
||||
if user.decorate.cached_followed_tag_names.any?
|
||||
group_1 = User.where(id: user_ids).
|
||||
order("reputation_modifier DESC").limit(25).to_a
|
||||
group_2 = User.where(id: user_ids).
|
||||
order("twitter_following_count DESC").limit(25).to_a
|
||||
group_3 = User.where(id: user_ids).
|
||||
order("articles_count DESC").limit(25).to_a
|
||||
group_4 = User.order("reputation_modifier DESC").limit(30).to_a
|
||||
group_5 = User.order("comments_count DESC").limit(25).to_a
|
||||
users = (group_1 + group_2 + group_3 + group_4 + group_5 - [user]).
|
||||
uniq.shuffle.first(50)
|
||||
else
|
||||
group_1 = User.order("reputation_modifier DESC").limit(100).to_a
|
||||
group_2 = User.where("articles_count > ?", 5).
|
||||
order("twitter_following_count DESC").limit(100).to_a
|
||||
group_3 = User.order("comments_count DESC").limit(100).to_a
|
||||
users = (group_1 + group_2 + group_3 - [user]).
|
||||
uniq.shuffle.first(50)
|
||||
end
|
||||
users
|
||||
end
|
||||
|
||||
def tagged_article_user_ids
|
||||
Article.
|
||||
tagged_with(user.decorate.cached_followed_tag_names, any: true).
|
||||
where(published: true).
|
||||
where("positive_reactions_count > ?", 1).pluck(:user_id).
|
||||
each_with_object(Hash.new(0)) { |value, counts| counts[value] += 1 }.
|
||||
sort_by { |_key, value| value }.
|
||||
map { |arr| arr[0] }
|
||||
end
|
||||
end
|
||||
6
app/views/api/v0/users/index.json.jbuilder
Normal file
6
app/views/api/v0/users/index.json.jbuilder
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
json.array! @users.each do |user|
|
||||
json.id user.id
|
||||
json.name user.name
|
||||
json.username user.username
|
||||
json.profile_image_url ProfileImage.new(user).get(90)
|
||||
end
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
<% else %>
|
||||
<div class="no-articles">
|
||||
<h3>This is where you can manage your posts, but you haven't written anything yet.</h3>
|
||||
<a href="/new" class="cta">
|
||||
<a href="/new" class="cta big">
|
||||
Write your first post now
|
||||
</a>
|
||||
<%=image_tag(cl_image_path(asset_path("/assets/sloan.png"),
|
||||
|
|
@ -91,6 +91,8 @@
|
|||
:flags => "progressive",
|
||||
:fetch_format => "auto",
|
||||
:sign_url => true), class:"sloan", alt:"sloth-mascot") %>
|
||||
<br/>
|
||||
<h3>Also check out the <a href="/welcome" data-no-instant>Welcome Thread</a> to introduce yourself!</h3>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
<div class="field">
|
||||
<%= f.label :username %>
|
||||
<%= f.text_field :username, maxlength: 30 %>
|
||||
Signing out and back in will update your Twitter/Github username.
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= f.label :name %>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"latest",
|
||||
"contact",
|
||||
"merch",
|
||||
"onboarding_update",
|
||||
"rlygenerator",
|
||||
"orlygenerator",
|
||||
"rlyslack",
|
||||
|
|
|
|||
|
|
@ -42,11 +42,13 @@ Rails.application.routes.draw do
|
|||
resources :comments
|
||||
resources :podcast_episodes
|
||||
resources :reactions, only: [:create]
|
||||
resources :users, only: [:index]
|
||||
resources :tags, only: [:index] do
|
||||
collection do
|
||||
get "/onboarding", to: "tags#onboarding"
|
||||
end
|
||||
end
|
||||
resources :follows, only: [:create]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
18
spec/labor/user_follow_suggester_spec.rb
Normal file
18
spec/labor/user_follow_suggester_spec.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe UserFollowSuggester, vcr: {} do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it "does not include calling user" do
|
||||
create(:user)
|
||||
create(:user)
|
||||
create(:user)
|
||||
expect(described_class.new(user).suggestions).not_to include(user)
|
||||
end
|
||||
it "does not include calling user" do
|
||||
create(:user)
|
||||
create(:user)
|
||||
create(:user)
|
||||
expect(described_class.new(user).suggestions.size).to eq(3)
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue