diff --git a/app/javascript/packs/TagsIndex.jsx b/app/javascript/packs/TagsIndex.jsx new file mode 100644 index 000000000..c165adc02 --- /dev/null +++ b/app/javascript/packs/TagsIndex.jsx @@ -0,0 +1,90 @@ +import { h, render } from 'preact'; +import { getUserDataAndCsrfToken } from '@utilities/getUserDataAndCsrfToken'; + +/* global showLoginModal */ + +function renderPage(currentUser) { + import('../tags/Tag') + .then(({ Tag }) => { + const tagCards = document.getElementsByClassName('tag-card'); + + const followedTags = JSON.parse(currentUser.followed_tags); + Array.from(tagCards).forEach((element) => { + const followedTag = followedTags.find( + (tag) => tag.id == element.dataset.tagId, + ); + const following = followedTag?.points >= 0; + const hidden = followedTag?.points < 0; + + render( + , + document.getElementById(`tag-buttons-${element.dataset.tagId}`), + ); + }); + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error('Unable to load tags', error); + }); +} + +/** + * Adds an event listener to the inner page content, to handle any and all follow button clicks with a single handler + */ +function listenForButtonClicks() { + document + .getElementById('page-content-inner') + .addEventListener('click', handleButtonClick); +} + +function handleButtonClick({ target }) { + let trigger; + + if (target.classList.contains('follow-action-button')) { + trigger = 'follow_button'; + } + + if (target.classList.contains('hide-action-button')) { + trigger = 'hide_button'; + } + + if (trigger) { + const trackingData = { + referring_source: 'tag', + trigger, + }; + showLoginModal(trackingData); + } +} + +document.ready.then(() => { + const userStatus = document.body.getAttribute('data-user-status'); + if (userStatus === 'logged-out') { + listenForButtonClicks(); + return; + } + + getUserDataAndCsrfToken() + .then(({ currentUser, csrfToken }) => { + window.currentUser = currentUser; + window.csrfToken = csrfToken; + renderPage(currentUser); + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error('Error getting user and CSRF Token', error); + }); +}); + +Document.prototype.ready = new Promise((resolve) => { + if (document.readyState !== 'loading') { + return resolve(); + } + document.addEventListener('DOMContentLoaded', () => resolve()); + return null; +}); diff --git a/app/javascript/tags/Tag.jsx b/app/javascript/tags/Tag.jsx new file mode 100644 index 000000000..c19491bc0 --- /dev/null +++ b/app/javascript/tags/Tag.jsx @@ -0,0 +1,97 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; +import PropTypes from 'prop-types'; +/* global browserStoreCache */ + +/** + * Renders the updated buttons component for a given tag card + * @param {number} props.id The id of the tag + * @param {string} props.name The name of the tag + * @param {boolean} props.isFollowing Whether the user is following the tag + * @param {string} props.isHidden Whether the tag is hidden + * + * @returns Updates the given Tag buttons (Follow and Hide) with the correct labels, buttons and actions. + */ +export const Tag = ({ id, name, isFollowing, isHidden }) => { + const [following, setFollowing] = useState(isFollowing); + const [hidden, setHidden] = useState(isHidden); + + let followingButton; + + const toggleFollowButton = () => { + setFollowing(!following); + browserStoreCache('remove'); + postFollowItem({ following: !following, hidden }); + }; + + const toggleHideButton = () => { + setHidden(!hidden); + const updatedFollowing = true; + setFollowing(updatedFollowing); + browserStoreCache('remove'); + postFollowItem({ hidden: !hidden, following: updatedFollowing }); + }; + + const postFollowItem = ({ following, hidden }) => { + fetch('/follows', { + method: 'POST', + headers: { + Accept: 'application/json', + 'X-CSRF-Token': window.csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + followable_type: 'Tag', + followable_id: id, + verb: `${following ? '' : 'un'}follow`, + explicit_points: hidden ? -1 : 1, + }), + credentials: 'same-origin', + }).then((response) => { + if (response.status !== 200) { + // TODO: replace this with an actual modal + alert('An error occurred'); + } + }); + }; + + const hideButtonLabel = hidden ? 'Unhide' : 'Hide'; + const followButtonLabel = following ? 'Following' : 'Follow'; + + if (!hidden) { + followingButton = ( + + ); + } + + return ( +
+ {followingButton} + +
+ ); +}; + +Tag.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + following: PropTypes.bool.isRequired, + hidden: PropTypes.bool.isRequired, +}; diff --git a/app/views/tags/index.html.erb b/app/views/tags/index.html.erb index e7a4cc4d4..27183f485 100644 --- a/app/views/tags/index.html.erb +++ b/app/views/tags/index.html.erb @@ -31,6 +31,7 @@
<%= link_to t("views.tags.following_tags"), dashboard_following_tags_path, class: "crayons-btn crayons-btn--ghost" %> + <%= link_to t("views.tags.hidden_tags"), dashboard_hidden_tags_path, class: "crayons-btn crayons-btn--ghost" %>
" name="q" placeholder="<%= t("views.tags.search.placeholder") %>" autocomplete="off" /> - <% if tag.badge_id && tag.badge %> - - <% end %> +
- <% end %> + <% if tag.badge_id && tag.badge %> + + <% end %> +
+ <% end %> <% end %> -<%= javascript_packs_with_chunks_tag "followButtons", defer: true %> + +<%= javascript_packs_with_chunks_tag "TagsIndex", defer: true %> diff --git a/config/locales/views/tags/en.yml b/config/locales/views/tags/en.yml index 419625359..c2610fe3b 100644 --- a/config/locales/views/tags/en.yml +++ b/config/locales/views/tags/en.yml @@ -38,7 +38,12 @@ en: placeholder: Optional - Can be used for challenges, i.e. submission guidelines. submit: Save follow: Follow + hide: Hide + aria_labels: + follow: "Follow tag: %{tag_name}" + hide: "Hide tag: %{tag_name}" following_tags: Following tags + hidden_tags: Hidden tags moderator: text_html: "comma separated, space is optional. Example: %{code}" code: 123 diff --git a/config/locales/views/tags/fr.yml b/config/locales/views/tags/fr.yml index c12f5030a..e6703b84a 100644 --- a/config/locales/views/tags/fr.yml +++ b/config/locales/views/tags/fr.yml @@ -39,7 +39,12 @@ fr: submit: Save markdown_html: "%{tag} is the name of the tag used in Markdown." follow: Suivre + aria_labels: + follow: "Follow tag: %{tag_name}" + hide: "Hide tag: %{tag_name}" + hide: Hide following_tags: Sujets suivis + hidden_tags: Hidden tags moderator: text_html: "comma separated, space is optional. Example: %{code}" code: 123 diff --git a/cypress/e2e/seededFlows/loggedOutFlows/showLoginModal.spec.js b/cypress/e2e/seededFlows/loggedOutFlows/showLoginModal.spec.js index e4b83c5a9..dae9a4932 100644 --- a/cypress/e2e/seededFlows/loggedOutFlows/showLoginModal.spec.js +++ b/cypress/e2e/seededFlows/loggedOutFlows/showLoginModal.spec.js @@ -80,7 +80,6 @@ describe('Show log in modal', () => { it('should show login modal for tag follow button click', () => { cy.visit('/tags'); cy.findByRole('heading', { name: 'Tags' }); - cy.get('[data-follow-clicks-initialized]'); verifyLoginModalBehavior(() => cy.findByRole('button', { name: 'Follow tag: tag1' }), @@ -94,6 +93,15 @@ describe('Show log in modal', () => { ); }); + it('should show login modal for tag hide button click', () => { + cy.visit('/tags'); + cy.findByRole('heading', { name: 'Tags' }); + + verifyLoginModalBehavior(() => + cy.findByRole('button', { name: 'Hide tag: tag1' }), + ); + }); + it('should show login modal for user profile follow button click', () => { cy.visit('/admin_mcadmin'); cy.get('[data-follow-clicks-initialized]'); diff --git a/cypress/e2e/seededFlows/tagsFlows/followTag.spec.js b/cypress/e2e/seededFlows/tagsFlows/followTag.spec.js index b136cacd0..e1b128539 100644 --- a/cypress/e2e/seededFlows/tagsFlows/followTag.spec.js +++ b/cypress/e2e/seededFlows/tagsFlows/followTag.spec.js @@ -7,14 +7,13 @@ describe('Follow tag', () => { cy.get('@user').then((user) => { cy.loginAndVisit(user, '/tags').then(() => { cy.findByRole('heading', { name: 'Tags' }); - cy.get('[data-follow-clicks-initialized]'); }); }); }); it('Follows and unfollows a tag from the tag index page', () => { cy.intercept('/follows').as('followsRequest'); - cy.findByRole('button', { name: 'Follow tag: tag1' }).as('followButton'); + cy.findByRole('button', { name: 'Follow tag: tag0' }).as('followButton'); cy.get('@followButton').click(); cy.wait('@followsRequest'); @@ -28,6 +27,34 @@ describe('Follow tag', () => { cy.get('@followButton').should('have.text', 'Follow'); cy.get('@followButton').should('have.attr', 'aria-pressed', 'false'); }); + + it('hides and unhides a tag', () => { + cy.intercept('/follows').as('followsRequest'); + cy.findByRole('button', { name: 'Follow tag: tag0' }).as('followButton'); + cy.findByRole('button', { name: 'Hide tag: tag0' }).as('hideButton'); + + cy.get('@hideButton').click(); + cy.wait('@followsRequest'); + + // clicking on 'Hide' should change it to an 'Unhide' + // and remove the Follow button + cy.get('@hideButton').should('have.text', 'Unhide'); + cy.get('@followButton').should('not.exist'); + + // clicking on 'Unhide' should change it back to 'Hide' + // and show a 'Following' button + cy.get('@hideButton').click(); + cy.wait('@followsRequest'); + + cy.get('@hideButton').should('have.text', 'Hide'); + cy.get('@followButton').should('not.exist'); + cy.findByRole('button', { name: 'Following tag: tag0' }).as( + 'followingButton', + ); + cy.get('@followingButton').should('exist'); + cy.get('@followingButton').should('have.text', 'Following'); + cy.get('@followingButton').should('have.attr', 'aria-pressed', 'true'); + }); }); describe('Tag feed page', () => {