diff --git a/client/coral-reaction/actions.js b/client/coral-reaction/actions.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/client/coral-reaction/components/CoralReaction.js b/client/coral-reaction/components/CoralReaction.js
new file mode 100644
index 000000000..f691e2bb7
--- /dev/null
+++ b/client/coral-reaction/components/CoralReaction.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+import styles from './style.css';
+
+import {capitalize} from '../helpers';
+import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
+
+class CoralReaction extends React.Component {
+ handleClick = () => {
+ const name = this.props.children;
+ const {postReaction, deleteReaction, showSignInDialog} = this.props;
+
+ const {root: {me}, comment} = this.props;
+
+ const myActionSummary = getMyActionSummary(
+ `${capitalize(name)}ActionSummary`,
+ comment
+ );
+
+ // If the current user does not exist, trigger sign in dialog.
+ if (!me) {
+ showSignInDialog();
+ return;
+ }
+
+ // If the current user is banned, do nothing.
+ if (me.status === 'BANNED') {
+ return;
+ }
+
+ if (myActionSummary) {
+ deleteReaction(myActionSummary.current_user.id, comment.id);
+ } else {
+ postReaction({
+ item_id: comment.id,
+ item_type: 'COMMENTS'
+ });
+ }
+ };
+
+ render() {
+ const name = this.props.children;
+ const {comment} = this.props;
+
+ console.log(this.props)
+
+ if (!comment) {
+ return null;
+ }
+
+ const myReaction = getMyActionSummary(`${capitalize(name)}ActionSummary`, comment);
+ let count = getTotalActionCount(`${capitalize(name)}ActionSummary`, comment);
+
+ return (
+
+
+
+ );
+ }
+}
+
+CoralReaction.propTypes = {
+ children: PropTypes.string.isRequired,
+ icon: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
+ tag: PropTypes.string,
+ translations: PropTypes.object
+};
+
+export default CoralReaction;
+
+/**
+ *
+ * icon: Could be a string or a component https://material.io/icons/
+ **/
diff --git a/client/coral-reaction/components/style.css b/client/coral-reaction/components/style.css
new file mode 100644
index 000000000..a82f20ff6
--- /dev/null
+++ b/client/coral-reaction/components/style.css
@@ -0,0 +1,30 @@
+.like {
+ display: inline-block;
+}
+
+.button {
+ color: #2a2a2a;
+ margin: 5px 10px 5px 0px;
+ background: none;
+ padding: 0px;
+ border: none;
+ font-size: inherit;
+
+ &:hover {
+ color: #767676;
+ cursor: pointer;
+ }
+
+ &.liked {
+ color: rgb(0,134,227);
+
+ &:hover {
+ color: rgb(0,134,227);
+ cursor: pointer;
+ }
+ }
+}
+
+.icon {
+ padding: 0 5px;
+}
diff --git a/client/coral-reaction/constants.js b/client/coral-reaction/constants.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/client/coral-reaction/containers/CoralReaction.js b/client/coral-reaction/containers/CoralReaction.js
new file mode 100644
index 000000000..f8880a935
--- /dev/null
+++ b/client/coral-reaction/containers/CoralReaction.js
@@ -0,0 +1,188 @@
+import get from 'lodash/get';
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {compose, gql, graphql} from 'react-apollo';
+import CoralReaction from '../components/CoralReaction';
+import withFragments from 'coral-framework/hocs/withFragments';
+import {showSignInDialog} from 'coral-framework/actions/auth';
+import {capitalize} from '../helpers';
+
+const name = 'love';
+
+const isReaction = a => a.__typename === `${capitalize(name)}ActionSummary`;
+
+const COMMENT_FRAGMENT = gql`
+ fragment ${capitalize(name)}Button_updateFragment on Comment {
+ action_summaries {
+ ... on ${capitalize(name)}ActionSummary {
+ count
+ current_user {
+ id
+ }
+ }
+ }
+ }
+`;
+
+const withDeleteReaction = graphql(
+ gql`
+ mutation deleteReaction($id: ID!) {
+ deleteAction(id:$id) {
+ errors {
+ translation_key
+ }
+ }
+ }
+ `,
+ {
+ props: ({mutate}) => ({
+ deleteReaction: (id, commentId) => {
+ return mutate({
+ variables: {id},
+ optimisticResponse: {
+ deleteAction: {
+ __typename: 'DeleteActionResponse',
+ errors: null
+ }
+ },
+ update: proxy => {
+ const fragmentId = `Comment_${commentId}`;
+
+ // Read the data from our cache for this query.
+ const data = proxy.readFragment({
+ fragment: COMMENT_FRAGMENT,
+ id: fragmentId
+ });
+
+ // Check whether we liked this comment.
+ const idx = data.action_summaries.findIndex(isReaction);
+ if (
+ idx < 0 ||
+ get(data.action_summaries[idx], 'current_user.id') !== id
+ ) {
+ return;
+ }
+
+ data.action_summaries[idx] = {
+ ...data.action_summaries[idx],
+ count: data.action_summaries[idx].count - 1,
+ current_user: null
+ };
+
+ // Write our data back to the cache.
+ proxy.writeFragment({
+ fragment: COMMENT_FRAGMENT,
+ id: fragmentId,
+ data
+ });
+ }
+ });
+ }
+ })
+ }
+);
+
+const withPostReaction = graphql(
+ gql`
+ mutation create${capitalize(name)}($${name}: Create${capitalize(name)}Input!) {
+ create${capitalize(name)}(${name}: $${capitalize(name)}) {
+ ${name} {
+ id
+ }
+ errors {
+ translation_key
+ }
+ }
+ }
+ `,
+ {
+ props: ({mutate}) => ({
+ postReaction: reaction => {
+ return mutate({
+ variables: {reaction},
+ optimisticResponse: {
+ [`create${capitalize(name)}`]: {
+ __typename: `Create${capitalize(name)}Response`,
+ errors: null,
+ [name]: {
+ __typename: `${capitalize(name)}Action`,
+ id: 'pending'
+ }
+ }
+ },
+ update: (proxy, mutationResult) => {
+ const fragmentId = `Comment_${reaction.item_id}`;
+
+ // Read the data from our cache for this query.
+ const data = proxy.readFragment({
+ fragment: COMMENT_FRAGMENT,
+ id: fragmentId
+ });
+
+ // Add our comment from the mutation to the end.
+ let idx = data.action_summaries.findIndex(isReaction);
+
+ // Check whether we already reactioned this comment.
+ if (idx >= 0 && data.action_summaries[idx].current_user) {
+ return;
+ }
+
+ if (idx < 0) {
+ // Add initial action when it doesn't exist.
+ data.action_summaries.push({
+ __typename: `${capitalize(name)}ActionSummary`,
+ count: 0,
+ current_user: null
+ });
+ idx = data.action_summaries.length - 1;
+ }
+
+ data.action_summaries[idx] = {
+ ...data.action_summaries[idx],
+ count: data.action_summaries[idx].count + 1,
+ current_user: mutationResult.data[`create${capitalize(name)}`][name]
+ };
+
+ // Write our data back to the cache.
+ proxy.writeFragment({
+ fragment: COMMENT_FRAGMENT,
+ id: fragmentId,
+ data
+ });
+ }
+ });
+ }
+ })
+ }
+);
+
+const mapDispatchToProps = dispatch =>
+ bindActionCreators({showSignInDialog}, dispatch);
+
+const enhance = compose(
+ withFragments({
+ root: gql`
+ fragment ${capitalize(name)}Button_root on RootQuery {
+ me {
+ status
+ }
+ }
+ `,
+ comment: gql`
+ fragment ${capitalize(name)}Button_comment on Comment {
+ action_summaries {
+ ... on ${capitalize(name)}ActionSummary {
+ count
+ current_user {
+ id
+ }
+ }
+ }
+ }`
+ }),
+ connect(null, mapDispatchToProps),
+ withDeleteReaction,
+ withPostReaction
+);
+
+export default enhance(CoralReaction);
diff --git a/client/coral-reaction/helpers.js b/client/coral-reaction/helpers.js
new file mode 100644
index 000000000..0a267da59
--- /dev/null
+++ b/client/coral-reaction/helpers.js
@@ -0,0 +1,4 @@
+export function capitalize(str) {
+ const newString = new String(str);
+ return newString.charAt(0).toUpperCase() + newString.slice(1);
+}
diff --git a/client/coral-reaction/index.js b/client/coral-reaction/index.js
new file mode 100644
index 000000000..d8527b9fd
--- /dev/null
+++ b/client/coral-reaction/index.js
@@ -0,0 +1,3 @@
+import CoralReaction from './containers/CoralReaction';
+
+export default CoralReaction;
\ No newline at end of file
diff --git a/client/coral-reaction/reducer.js b/client/coral-reaction/reducer.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/client/coral-reaction/typeDefGenerator.js b/client/coral-reaction/typeDefGenerator.js
new file mode 100644
index 000000000..63c20684e
--- /dev/null
+++ b/client/coral-reaction/typeDefGenerator.js
@@ -0,0 +1,80 @@
+export function typeDefGenerator(type = '') {
+ function capitalize(str) {
+ const newString = String.new(str);
+ return newString.charAt(0).toUpperCase() + newString.slice(1);
+ }
+
+ return `
+enum ACTION_TYPE {
+
+ # Represents a Like.
+ LIKE
+}
+
+enum ASSET_METRICS_SORT {
+
+ # Represents a LikeAction.
+ LIKE
+}
+
+input CreateLikeInput {
+
+ # The item's id for which we are to create a like.
+ item_id: ID!
+
+ # The type of the item for which we are to create the like.
+ item_type: ACTION_ITEM_TYPE!
+}
+
+# LikeAction is used by users who "like" a specific entity.
+type LikeAction implements Action {
+
+ # The ID of the action.
+ id: ID!
+
+ # The author of the action.
+ user: User
+
+ # The time when the Action was updated.
+ updated_at: Date
+
+ # The time when the Action was created.
+ created_at: Date
+}
+
+type LikeActionSummary implements ActionSummary {
+
+ # The count of actions with this group.
+ count: Int
+
+ # The current user's action.
+ current_user: LikeAction
+}
+
+# A summary of counts related to all the Likes on an Asset.
+type LikeAssetActionSummary implements AssetActionSummary {
+
+ # Number of likes associated with actionable types on this this Asset.
+ actionCount: Int
+
+ # Number of unique actionable types that are referenced by the likes.
+ actionableItemCount: Int
+}
+
+type CreateLikeResponse implements Response {
+
+ # The like that was created.
+ like: LikeAction
+
+ # An array of errors relating to the mutation that occurred.
+ errors: [UserError]
+}
+
+type RootMutation {
+
+ # Creates a like on an entity.
+ createLike(like: CreateLikeInput!): CreateLikeResponse
+}
+
+`;
+}
\ No newline at end of file