Follow and Hidden Buttons on /tags page (#20005)
This commit is contained in:
parent
02db638d9d
commit
f4a9c192c0
7 changed files with 252 additions and 12 deletions
90
app/javascript/packs/TagsIndex.jsx
Normal file
90
app/javascript/packs/TagsIndex.jsx
Normal file
|
|
@ -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(
|
||||
<Tag
|
||||
id={element.dataset.tagId}
|
||||
name={element.dataset.tagName}
|
||||
isFollowing={following || hidden}
|
||||
isHidden={hidden}
|
||||
/>,
|
||||
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;
|
||||
});
|
||||
97
app/javascript/tags/Tag.jsx
Normal file
97
app/javascript/tags/Tag.jsx
Normal file
|
|
@ -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 = (
|
||||
<button
|
||||
onClick={toggleFollowButton}
|
||||
className={`crayons-btn ${
|
||||
following ? 'crayons-btn--outlined' : 'crayons-btn--primary'
|
||||
}`}
|
||||
aria-pressed={following}
|
||||
aria-label={`${followButtonLabel} tag: ${name}`}
|
||||
>
|
||||
{followButtonLabel}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{followingButton}
|
||||
<button
|
||||
onClick={toggleHideButton}
|
||||
className={`crayons-btn ${
|
||||
hidden ? 'crayons-btn--danger' : 'crayons-btn--ghost'
|
||||
}`}
|
||||
aria-label={`${hideButtonLabel} tag: ${name}`}
|
||||
>
|
||||
{hideButtonLabel}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Tag.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
following: PropTypes.bool.isRequired,
|
||||
hidden: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
<form accept-charset="UTF-8" method="get" action="<%= tags_path %>" role="search">
|
||||
<div class="crayons-fields crayons-fields--horizontal">
|
||||
<%= 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" %>
|
||||
<div class="crayons-field flex-1 relative">
|
||||
<input class="crayons-header--search-input crayons-textfield js-search-input" type="text" id="nav-search" aria-label="<%= t("views.tags.search.aria_labels.search_input") %>" name="q" placeholder="<%= t("views.tags.search.placeholder") %>" autocomplete="off" />
|
||||
<button type="submit" aria-label="<%= t("views.tags.search.aria_labels.submit") %>" class="c-btn c-btn--icon-alone absolute inset-px left-auto mt-0 py-0">
|
||||
|
|
@ -48,7 +49,7 @@
|
|||
<% else %>
|
||||
<div class="grid gap-3 m:gap-4 l:gap-4 m:grid-cols-2 l:grid-cols-4 px-3 m:px-0 pb-3" data-follow-button-container="true">
|
||||
<% @tags.includes([:badge]).each do |tag| %>
|
||||
<div class="tag-card crayons-card p-3 pt-2 m:p-5 m:pt-4 relative flex flex-col">
|
||||
<div data-tag-id="<%= tag.id %>" data-tag-name="<%= tag.name %>" id="tag-<%= tag.id %>" class="tag-card crayons-card p-3 pt-2 m:p-5 m:pt-4 relative flex flex-col">
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<h4 class="-ml-2">
|
||||
|
|
@ -60,21 +61,28 @@
|
|||
<p class="mb-6 fs-s color-base-70 truncate-at-3"><%= sanitize tag.short_summary %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if tag %>
|
||||
<div class="mt-auto flex items-end justify-between">
|
||||
<div id="tag-buttons-<%= tag.id %>" class="mt-auto flex items-end justify-between">
|
||||
<div>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--primary follow-action-button"
|
||||
data-info='<%= DataInfo.to_json(object: tag, class_name: "Tag", followStyle: "primary") %>'>
|
||||
aria-label="<%= t("views.tags.aria_labels.follow", tag_name: tag.name) %>">
|
||||
<%= t("views.tags.follow") %>
|
||||
</button>
|
||||
<% if tag.badge_id && tag.badge %>
|
||||
<img src="<%= optimized_image_url(tag.badge.badge_image_url, width: 180) %>" class="tag-card__badge" alt="" loading="lazy" />
|
||||
<% end %>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--ghost hide-action-button"
|
||||
aria-label="<%= t("views.tags.aria_labels.hide", tag_name: tag.name) %>">
|
||||
<%= t("views.tags.hide") %>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if tag.badge_id && tag.badge %>
|
||||
<img src="<%= optimized_image_url(tag.badge.badge_image_url, width: 180) %>" class="tag-card__badge" alt="" loading="lazy" />
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</main>
|
||||
<%= javascript_packs_with_chunks_tag "followButtons", defer: true %>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "TagsIndex", defer: true %>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]');
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue