BarkedIn is a fullstack clone of LinkedIn, but for dogs. Users can create their own profiles, connect with other dogs in their network and post to the news feed.
A user's profile displays their profile photo (hosted through S3), headline information, an about me section and a clip of their activity (the full extent of a user's activity can be viewed on the activity page). If a user is visiting their own profile, it includes the ability to upload a profile photo, delete a profile photo and reset it to the default photo, edit any of their 'about me' information.
User's can add posts to the network. If a post is their own, they have the ability to edit or delete it. User's can also comment on or react to posts with a variety of reactions including 'wag', 'high five' or 'throw a bone'.
Devising a plan for implementing connections and connection requests was a large task with a variety of implementation options. It was important that users be able to initiate, withdraw and respond to connection requests, as well as maintain a list of their connected users. While there were many possible approaches, I ultimately decided that my biggest priority was to best maintain the integrity of the data in the database.
As such, there is only one data table for both connections and connection requests, and only one entry per connection. While there was a bit more code required to find a user on the table, this approach ensured that connections were always reciprocal and that updates to connection statuses (from pending to connected, or a withdrawn request or connection) were smooth. The potential for a bit of latency with searching users on the table was somewhat addressed with indexing both the user_id1, user_id2, and the pair, but would potentially need to be addressed further should the application be rescoped for a larger scale.
The data table includes a column for user_id1
, user_id2
and the connection status
. There are check constraints on the table as well as model level validations to ensure that user_id1
< user_id2
(ensuring no duplicate connections) and status
is either 'connected', 'pending_user1' or 'pending_user2'.
Joins Table | Column | Data Type | Details |
---|---|---|---|
id |
integer | not null, primary key | |
user_id1 |
integer | not null, indexed, foreign key | |
user_id2 |
integer | not null, indexed, foreign key | |
status |
string | not null | |
created_at |
datetime | not null | |
updated_at |
datetime | not null |
DB Validations:
Custom associations were required due to the nature of the single table.
def connections
Connection.where("(user_id1 = ? OR user_id2 = ?) AND status = 'connected'", self.id, self.id)
end
def connected_users
users = connections.map do |connection|
if connection.user_id1 == self.id
connection.user_id2
else
connection.user_id1
end
end
User.where("id IN (?)", users)
end
Dropdowns and modals enlisted a lot of reused code, so they were exported into individual components that passed out information depending on the name of the modal or dropdown being triggered. Modal rendering is handled via the Redux State.
ui: {
modal: modalName/false
},
export const updateModal = (modalName, post) => ({
type: 'UPDATE_MODAL',
modalName,
post
});
const UIReducer = (state = _defaultUi, action) => {
Object.freeze(state);
switch (action.type) {
case UPDATE_MODAL:
const modal = state.modal;
let newState = Object.assign({}, state);
if (modal) {
newState['modal'] = false;
} else {
newState['modal'] = action.modalName;
if (action.post) {
newState['post'] = action.post;
} else {
delete newState['post'];
}
}
return newState;
default:
return state;
}
}
<i className="fas fa-pencil-alt" onClick={() => this.props.updateModal('EditAboutMe')}></i>
const Modal = (props) => {
let modal;
let {name, ...otherProps} = props;
switch (props.name) {
case "ContactInfo":
modal = <ContactInfoModal {...otherProps}/>;
break;
case "EditProfileIntro":
modal = <EditProfileIntroModal {...otherProps}/>;
break;
case "ProfilePhoto":
modal = <ProfilePhotoModal {...otherProps}/>;
break;
case "EditAboutMe":
modal = <EditAboutMeModal {...otherProps}/>;
break;
case 'DeletePhoto':
modal = <DeletePhotoModal {...otherProps}/>
break;
case 'NewPost':
modal = <NewPostModal {...otherProps}/>;
break;
case 'EditPost':
modal = <EditPostModal {...otherProps}/>;
break;
case 'DeletePost':
modal = <DeletePostModal {...otherProps}/>
break;
}
return (
<div>
<div className="modal-background" onClick={props.updateModal}>
{modal}
</div>
</div>
)
}
class EditAboutMeModal extends React.Component {
constructor (props) {
super(props);
this.state = props.user;
this.handleSubmit = this.handleSubmit.bind(this);
this.updateAboutMe = this.updateAboutMe.bind(this);
}
updateAboutMe (e) {
this.setState({
aboutMe: e.target.value
})
}
handleSubmit (e) {
e.preventDefault();
this.props.updateUser(this.state);
this.props.toggleAboutMe();
this.props.updateModal();
}
render () {
return (
<div className="modal-box edit-profile-intro-modal-box edit-about-me-modal-box" onClick={(e) => e.stopPropagation()}>
<header>
<h4>Edit summary</h4>
<i className="fas fa-times" onClick={this.props.updateModal}></i>
</header>
<form>
<label>Description
<textarea cols="30" rows="10" value={this.state.aboutMe} onChange={this.updateAboutMe}></textarea>
</label>
<footer>
<button onClick={this.handleSubmit}>Save</button>
</footer>
</form>
</div>
)
}
}
export default EditAboutMeModal;