MVP of follow sidebar

This commit is contained in:
Zhao-Andy 2018-04-25 13:53:10 -04:00
parent 64d21a3266
commit be14ed029d
13 changed files with 224 additions and 4 deletions

View file

@ -24,6 +24,7 @@
@import 'live';
@import 'delete-confirm';
@import 'preact/onboarding-modal';
@import 'preact/sidebar-widget';
@import 'tag-edit';
@import 'ltags/LiquidTags';

View file

@ -0,0 +1,52 @@
@import 'variables';
.sidebar-additional{
.widget{
.widget-suggested-follows-container{
.widget-body{
.widget-list-item__suggestions{
.widget-list-item__close-button{
float: right;
width: 24px;
height: 19px;
margin: 3px;
}
.widget-list-item__content{
max-width: 180px;
margin-left: 60px;
margin-right: 20px;
min-height: 55px;
a{
display: block;
padding: 5px 0px;
font-size: 18px;
color: $black;
&:hover {
text-decoration: underline;
}
}
img{
width: 48px;
height: 48px;
border-radius: 50%;
position: absolute;
top: unset;
left: 15px;
}
.widget-list-item__follow-button{
border: 2px solid transparent;
border-radius: 3px;
padding: 2px 8px;
text-align: center;
background-color: $green;
color: $black;
font-family: $helvetica-condensed;
font-weight: 900;
font-size: 0.8em;
}
}
}
}
}
}
}

View file

@ -12,6 +12,12 @@ module Api
end
end
def sidebar_suggestions
given_tag = params[:tag]
@users = UserFollowSuggester.new(nil).sidebar_suggestions(given_tag)
render "index.json.jbuilder"
end
def less_than_one_day_old?(user)
range = (Time.now.beginning_of_day - 1.day)..(Time.now)
user_identity_age = user.github_created_at ||

View file

@ -0,0 +1,9 @@
import { h, render } from 'preact';
import SidebarWidget from '../sidebar-widget/SidebarWidget';
HTMLDocument.prototype.ready = new Promise((resolve) => {
if (document.readyState !== 'loading') { return resolve(); }
document.addEventListener('DOMContentLoaded', () => resolve());
});
render(<SidebarWidget />, document.getElementById('sidebarWidget__pack'));

View file

@ -0,0 +1,75 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import sendFollowUser from '../src/utils/sendFollowUser';
import SidebarUser from './sidebarUser';
class SidebarWidget extends Component {
constructor(props) {
super(props);
this.getSuggestedUsers = this.getSuggestedUsers.bind(this);
this.getTagInfo = this.getTagInfo.bind(this);
this.followUser = this.followUser.bind(this);
this.state = {
tagInfo: {},
suggestedUsers: [],
};
}
componentDidMount() {
this.getTagInfo();
this.getSuggestedUsers();
}
getTagInfo() {
this.setState({ tagInfo: JSON.parse(document.getElementById('sidebarWidget__pack').dataset.tagInfo) });
}
getSuggestedUsers() {
fetch(`/api/users/sidebar_suggestions?tag=${this.state.tagInfo.name}`, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
credentials: 'same-origin',
})
.then(response => response.json())
.then((json) => {
this.setState({ suggestedUsers: json });
})
.catch((error) => {
console.log(error);
});
}
followUser(user) {
const toggleFollowState = (newFollowState) => {
const updatedUser = user;
const updatedSuggestedUsers = this.state.suggestedUsers;
const userIndex = this.state.suggestedUsers.indexOf(user);
updatedUser.following = newFollowState === 'followed';
updatedSuggestedUsers[userIndex] = updatedUser;
this.setState({ suggestedUsers: updatedSuggestedUsers });
};
sendFollowUser(user, toggleFollowState);
}
render() {
const users = this.state.suggestedUsers.map((user) => {
return(
<SidebarUser key={user.id} user={user} followUser={this.followUser} />
);
});
return (
<div className="widget-suggested-follows-container">
<header>
{"<WHO TO FOLLOW>"}
</header>
<div className="widget-body">
{users}
</div>
</div>
);
}
}
export default SidebarWidget;

View file

@ -0,0 +1,37 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
class SidebarUser extends Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick() {
this.props.followUser(this.props.user);
}
render() {
return(
<div className="widget-list-item__suggestions">
<div className="widget-list-item__content">
<a href={`/${this.props.user.username}`}>
<img src={this.props.user.profile_image_url} alt={this.props.user.name} className="widget-list-item__profile-pic" />
{this.props.user.name}
</a>
<button className="widget-list-item__follow-button" type="button" onClick={this.onClick}>
{this.props.user.following ? '✓ FOLLOWING' : '+ FOLLOW'}
</button>
</div>
<hr />
</div>
)
}
}
SidebarUser.propTypes = {
followUser: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
};
export default SidebarUser;

View file

@ -164,7 +164,6 @@ class Onboarding extends Component {
formData.append('followable_type', 'Tag');
formData.append('followable_id', tag.id);
formData.append('verb', (tag.following ? 'unfollow' : 'follow'));
formData.append('authenticity_token', csrfToken);
fetch('/follows', {
method: 'POST',

View file

@ -0,0 +1,25 @@
export default function sendFollowUser(user, successCb, failureCb) {
const csrfToken = document.querySelector("meta[name='csrf-token']").content;
const formData = new FormData();
formData.append('followable_type', 'User');
formData.append('followable_id', user.id);
formData.append('verb', (user.following ? 'unfollow' : 'follow'));
fetch('/follows', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
},
body: formData,
credentials: 'same-origin',
}).then((response) => {
return response.json();
}).then((json) => {
console.log('in fetch request: ' + json.outcome)
successCb(json.outcome);
// json is followed or unfollowed
}).catch((error) => {
console.log(error);
});
}

View file

@ -33,11 +33,20 @@ class UserFollowSuggester
users
end
def sidebar_suggestions(given_tag)
user_ids = Article.tagged_with([given_tag], any: true).
where("published = ? AND positive_reactions_count > ? AND published_at > ?",
true, 15, 7.months.ago).
pluck(:user_id)
User.where(id: user_ids).order("reputation_modifier DESC").limit(3).to_a
end
def tagged_article_user_ids
Article.
tagged_with(user.decorate.cached_followed_tag_names, any: true).
where(published: true).
where("positive_reactions_count > ? AND published_at > ?", article, 7.months.ago).pluck(:user_id).
where("positive_reactions_count > ? AND published_at > ?", article, 7.months.ago).
pluck(:user_id).
each_with_object(Hash.new(0)) { |value, counts| counts[value] += 1 }.
sort_by { |_key, value| value }.
map { |arr| arr[0] }

View file

@ -3,4 +3,5 @@ json.array! @users.each do |user|
json.name user.name
json.username user.username
json.profile_image_url ProfileImage.new(user).get(90)
json.following current_user ? current_user.following?(user) : false
end

View file

@ -57,6 +57,8 @@
</div>
</div>
<% end %>
<%= javascript_pack_tag "sidebarWidget", defer: true %>
<div class="widget" id="sidebarWidget__pack" data-tag-info="<%= @tag_model.attributes.slice("id", "text_color_hex", "bg_color_hex", "name").to_json %>" />
<% elsif @query %>
<% elsif @podcast_index %>
<% else %>

View file

@ -39,7 +39,7 @@
<% if (Flipflop.sendbird?) %>
<div id="sb_chat" class="live-chat" data-sendbird-app-id="<%= ENV["SENDBIRD_APP_ID"] %>" data-sendbird-livechat-url="<%= ENV["SENDBIRD_LIVECHAT_URL"] %>"></div>
<% else %>
<div id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>"</div>
<div id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>"></div>
<% end %>
</div>
</div>

View file

@ -50,7 +50,11 @@ Rails.application.routes.draw do
post "/onboarding", to: "reactions#onboarding"
end
end
resources :users, only: [:index]
resources :users, only: [:index] do
collection do
get "/sidebar_suggestions", to: "users#sidebar_suggestions"
end
end
resources :tags, only: [:index] do
collection do
get "/onboarding", to: "tags#onboarding"