Add author profile preview cards to feed (#14340)
* Add the preview card to logged out feed initial content * initialise the initial dropdowns added on the logged out feed * minor tweak to selector * flip the follow button and the summary * add minimal preview card to build article HTML * WIP: data fetched an inserted into card on logged out feed * WIP: cards added to logged in feed * create separate profile preview card component * small style tweak, import pack on each page that shows feed cards * rename * tweak some styling issues * make sure follow buttons init in cards * populate all matching metadata placeholders after fetch * don't render full preview card upfront on logged out feed * refactors from PR comments * fix issue in person search results * remove check for article author link that will be superseded by cypress test for preview card * Revert "remove check for article author link that will be superseded by cypress test for preview card" This reverts commit 9b42804ffd0f051891c87293d0b791ed2bb0367f. * Revert "fix issue in person search results" This reverts commit 04941e3520c0895212141193b60f2933faed5ca1. * only show the preview cards on story cards for Posts (not users etc in search results) * correct display on collections view * remove link check that will be replaced by cypress test * tweaks to small issues, add a test for the logged out feed * add tests for logged in home feed, logged out tag index * add search test and tag index logged in test * fixes to preview profile spec * tweak to followauthor spec * add cypress test for preview on series page * use a unique test user for series test * correct the jsdoc comments * tweaks following PR review * allow feed preview cards to reposition * move to separate file from pack
This commit is contained in:
parent
a041e5996e
commit
80096f63a7
27 changed files with 803 additions and 68 deletions
|
|
@ -172,44 +172,54 @@ function buildArticleHTML(article) {
|
|||
}
|
||||
}
|
||||
|
||||
var meta =
|
||||
'<div class="crayons-story__meta">\
|
||||
<div class="crayons-story__author-pic">\
|
||||
' +
|
||||
organizationLogo +
|
||||
'\
|
||||
<a href="/' +
|
||||
profileUsername +
|
||||
'" class="crayons-avatar ' +
|
||||
organizationClasses +
|
||||
'">\
|
||||
<img src="' +
|
||||
picUrl +
|
||||
'" alt="' +
|
||||
profileUsername +
|
||||
' profile" class="crayons-avatar__image" loading="lazy" />\
|
||||
</a>\
|
||||
</div>\
|
||||
<div>\
|
||||
<p>\
|
||||
<a href="/' +
|
||||
profileUsername +
|
||||
'" class="crayons-story__secondary fw-medium">' +
|
||||
filterXSS(article.user.name) +
|
||||
'</a>\
|
||||
' +
|
||||
forOrganization +
|
||||
'\
|
||||
</p>\
|
||||
<a href="' +
|
||||
article.path +
|
||||
'" class="crayons-story__tertiary fs-xs">\
|
||||
' +
|
||||
publishDate +
|
||||
'\
|
||||
</a>\
|
||||
</div>\
|
||||
</div>';
|
||||
// We only show profile preview cards for Posts
|
||||
var isArticle = article.class_name === 'Article';
|
||||
|
||||
var previewCardContent = `
|
||||
<div id="story-author-preview-content-${article.id}" class="profile-preview-card__content crayons-dropdown" data-repositioning-dropdown="true" style="border-top: var(--su-7) solid var(--card-color);" data-testid="profile-preview-card">
|
||||
<div class="gap-4 grid">
|
||||
<div class="-mt-4">
|
||||
<a href="/${profileUsername}" class="flex">
|
||||
<span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0">
|
||||
<img src="${picUrl}" class="crayons-avatar__image" alt="" loading="lazy" />
|
||||
</span>
|
||||
<span class="crayons-link crayons-subtitle-2 mt-5">${article.user.name}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="print-hidden">
|
||||
<button class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100" data-info='{"id": ${article.user_id}, "className": "User", "style": "full"}'>Follow</button>
|
||||
</div>
|
||||
<div class="author-preview-metadata-container" data-author-id="${article.user_id}"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
var meta = `
|
||||
<div class="crayons-story__meta">
|
||||
<div class="crayons-story__author-pic">
|
||||
${organizationLogo}
|
||||
<a href="/${profileUsername}" class="crayons-avatar ${organizationClasses}">
|
||||
<img src="${picUrl}" alt="${profileUsername} profile" class="crayons-avatar__image" loading="lazy" />
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<a href="/${profileUsername}" class="crayons-story__secondary fw-medium ${
|
||||
isArticle ? 'm:hidden' : ''
|
||||
}">${filterXSS(article.user.name)}</a>
|
||||
${
|
||||
isArticle
|
||||
? `<div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"><button id="story-author-preview-trigger-${article.id}" aria-controls="story-author-preview-content-${article.id}" class="profile-preview-card__trigger fs-s crayons-btn crayons-btn--ghost p-1 -ml-1" aria-label="${article.user.name} profile details">${article.user.name}</button>${previewCardContent}</div>`
|
||||
: ''
|
||||
}
|
||||
${forOrganization}
|
||||
</p>
|
||||
<a href="${
|
||||
article.path
|
||||
}" class="crayons-story__tertiary fs-xs">${publishDate}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
var bodyTextSnippet = '';
|
||||
var searchSnippetHTML = '';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
.profile-preview-card {
|
||||
&__content.crayons-dropdown {
|
||||
transition: border, border-top;
|
||||
transition-duration: 300ms;
|
||||
color: var(--base-100);
|
||||
padding-top: 0;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
font-size: var(--fs-base);
|
||||
}
|
||||
|
||||
&__content.crayons-dropdown:hover {
|
||||
|
|
|
|||
|
|
@ -59,11 +59,73 @@ Object {
|
|||
<div>
|
||||
<p>
|
||||
<a
|
||||
class="crayons-story__secondary fw-medium"
|
||||
class="crayons-story__secondary fw-medium m:hidden"
|
||||
href="/Emil99"
|
||||
>
|
||||
Stella Macejkovic
|
||||
</a>
|
||||
<div
|
||||
class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"
|
||||
>
|
||||
<button
|
||||
aria-controls="story-author-preview-content-62407"
|
||||
aria-label="Stella Macejkovic profile details"
|
||||
class="profile-preview-card__trigger fs-s p-1 crayons-btn crayons-btn--ghost -ml-1"
|
||||
id="story-author-preview-trigger-62407"
|
||||
>
|
||||
Stella Macejkovic
|
||||
</button>
|
||||
<div
|
||||
class="profile-preview-card__content crayons-dropdown"
|
||||
data-repositioning-dropdown="true"
|
||||
data-testid="profile-preview-card"
|
||||
id="story-author-preview-content-62407"
|
||||
style=""
|
||||
>
|
||||
<div
|
||||
class="gap-4 grid"
|
||||
>
|
||||
<div
|
||||
class="-mt-4"
|
||||
>
|
||||
<a
|
||||
class="flex"
|
||||
href="/Emil99"
|
||||
>
|
||||
<span
|
||||
class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="crayons-avatar__image"
|
||||
loading="lazy"
|
||||
src="/images/10.png"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="crayons-link crayons-subtitle-2 mt-5"
|
||||
>
|
||||
Stella Macejkovic
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="print-hidden"
|
||||
>
|
||||
<button
|
||||
class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100"
|
||||
data-info="{\\"id\\":23289,\\"className\\":\\"User\\",\\"style\\":\\"full\\"}"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="author-preview-metadata-container"
|
||||
data-author-id="23289"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<a
|
||||
class="crayons-story__tertiary fs-xs"
|
||||
|
|
@ -245,11 +307,73 @@ Object {
|
|||
<div>
|
||||
<p>
|
||||
<a
|
||||
class="crayons-story__secondary fw-medium"
|
||||
class="crayons-story__secondary fw-medium m:hidden"
|
||||
href="/Emil99"
|
||||
>
|
||||
Stella Macejkovic
|
||||
</a>
|
||||
<div
|
||||
class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"
|
||||
>
|
||||
<button
|
||||
aria-controls="story-author-preview-content-62407"
|
||||
aria-label="Stella Macejkovic profile details"
|
||||
class="profile-preview-card__trigger fs-s p-1 crayons-btn crayons-btn--ghost -ml-1"
|
||||
id="story-author-preview-trigger-62407"
|
||||
>
|
||||
Stella Macejkovic
|
||||
</button>
|
||||
<div
|
||||
class="profile-preview-card__content crayons-dropdown"
|
||||
data-repositioning-dropdown="true"
|
||||
data-testid="profile-preview-card"
|
||||
id="story-author-preview-content-62407"
|
||||
style=""
|
||||
>
|
||||
<div
|
||||
class="gap-4 grid"
|
||||
>
|
||||
<div
|
||||
class="-mt-4"
|
||||
>
|
||||
<a
|
||||
class="flex"
|
||||
href="/Emil99"
|
||||
>
|
||||
<span
|
||||
class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="crayons-avatar__image"
|
||||
loading="lazy"
|
||||
src="/images/10.png"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="crayons-link crayons-subtitle-2 mt-5"
|
||||
>
|
||||
Stella Macejkovic
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="print-hidden"
|
||||
>
|
||||
<button
|
||||
class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100"
|
||||
data-info="{\\"id\\":23289,\\"className\\":\\"User\\",\\"style\\":\\"full\\"}"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="author-preview-metadata-container"
|
||||
data-author-id="23289"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<a
|
||||
class="crayons-story__tertiary fs-xs"
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import {
|
|||
articlePropTypes,
|
||||
organizationPropType,
|
||||
} from '../../common-prop-types';
|
||||
import { MinimalProfilePreviewCard } from '../../profilePreviewCards/MinimalProfilePreviewCard';
|
||||
import { PublishDate } from './PublishDate';
|
||||
|
||||
export const Meta = ({ article, organization }) => {
|
||||
const orgArticleIndexClassAbsent = !document.getElementById(
|
||||
'organization-article-index',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="crayons-story__meta">
|
||||
<div className="crayons-story__author-pic">
|
||||
|
|
@ -45,7 +47,7 @@ export const Meta = ({ article, organization }) => {
|
|||
<p>
|
||||
<a
|
||||
href={`/${article.user.username}`}
|
||||
className="crayons-story__secondary fw-medium"
|
||||
className="crayons-story__secondary fw-medium m:hidden"
|
||||
>
|
||||
{filterXSS(
|
||||
article.class_name === 'User'
|
||||
|
|
@ -53,6 +55,14 @@ export const Meta = ({ article, organization }) => {
|
|||
: article.user.name,
|
||||
)}
|
||||
</a>
|
||||
<MinimalProfilePreviewCard
|
||||
triggerId={`story-author-preview-trigger-${article.id}`}
|
||||
contentId={`story-author-preview-content-${article.id}`}
|
||||
username={article.user.username}
|
||||
name={article.user.name}
|
||||
profileImage={article.user.profile_image_90}
|
||||
userId={article.user_id}
|
||||
/>
|
||||
{organization &&
|
||||
!document.getElementById('organization-article-index') && (
|
||||
<span>
|
||||
|
|
|
|||
6
app/javascript/packs/feedPreviewCards.jsx
Normal file
6
app/javascript/packs/feedPreviewCards.jsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import('../previewCards/feedPreviewCards').then(
|
||||
({ initializeFeedPreviewCards, listenForHoveredOrFocusedStoryCards }) => {
|
||||
initializeFeedPreviewCards();
|
||||
listenForHoveredOrFocusedStoryCards();
|
||||
},
|
||||
);
|
||||
115
app/javascript/previewCards/feedPreviewCards.jsx
Normal file
115
app/javascript/previewCards/feedPreviewCards.jsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { h, render } from 'preact';
|
||||
import { UserMetadata } from '../profilePreviewCards/UserMetadata';
|
||||
import {
|
||||
initializeDropdown,
|
||||
getDropdownRepositionListener,
|
||||
} from '@utilities/dropdownUtils';
|
||||
import { request } from '@utilities/http/request';
|
||||
|
||||
const cachedAuthorMetadata = {};
|
||||
|
||||
async function populateMissingMetadata(metadataPlaceholder) {
|
||||
const { authorId, fetched } = metadataPlaceholder.dataset;
|
||||
|
||||
// If the metadata is already being fetched, do nothing
|
||||
if (fetched) {
|
||||
return;
|
||||
}
|
||||
metadataPlaceholder.dataset.fetched = 'true';
|
||||
|
||||
const previouslyFetchedAuthorMetadata = cachedAuthorMetadata[authorId];
|
||||
|
||||
if (previouslyFetchedAuthorMetadata) {
|
||||
renderMetadata(previouslyFetchedAuthorMetadata, metadataPlaceholder);
|
||||
} else {
|
||||
const response = await request(`/profile_preview_cards/${authorId}`);
|
||||
const authorMetadata = await response.json();
|
||||
|
||||
cachedAuthorMetadata[authorId] = authorMetadata;
|
||||
renderMetadata(authorMetadata, metadataPlaceholder);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMetadata(metadata, placeholder) {
|
||||
const container = placeholder.parentElement;
|
||||
|
||||
render(<UserMetadata {...metadata} />, container, placeholder);
|
||||
|
||||
container
|
||||
.closest('.profile-preview-card__content')
|
||||
.style.setProperty('--card-color', metadata.card_color);
|
||||
}
|
||||
|
||||
function checkForPreviewCardDetails(event) {
|
||||
const { target } = event;
|
||||
|
||||
if (target.classList.contains('profile-preview-card__trigger')) {
|
||||
const metadataPlaceholder = target.parentElement.getElementsByClassName(
|
||||
'author-preview-metadata-container',
|
||||
)[0];
|
||||
|
||||
if (metadataPlaceholder) {
|
||||
// User is within one of the story cards - and the metadata has not been fetched yet
|
||||
populateMissingMetadata(metadataPlaceholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function listenForHoveredOrFocusedStoryCards() {
|
||||
const mainContent = document.getElementById('main-content');
|
||||
|
||||
mainContent.addEventListener('mouseover', checkForPreviewCardDetails);
|
||||
mainContent.addEventListener('focusin', checkForPreviewCardDetails);
|
||||
}
|
||||
|
||||
export function initializeFeedPreviewCards() {
|
||||
// Select all preview card triggers that haven't already been initialized
|
||||
const allPreviewCardTriggers = document.querySelectorAll(
|
||||
'button[id^=story-author-preview-trigger]:not([data-initialized])',
|
||||
);
|
||||
|
||||
for (const previewTrigger of allPreviewCardTriggers) {
|
||||
const dropdownContentId = previewTrigger.getAttribute('aria-controls');
|
||||
const dropdownElement = document.getElementById(dropdownContentId);
|
||||
|
||||
if (dropdownElement) {
|
||||
initializeDropdown({
|
||||
triggerElementId: previewTrigger.id,
|
||||
dropdownContentId,
|
||||
onOpen: () => dropdownElement?.classList.add('showing'),
|
||||
onClose: () => dropdownElement?.classList.remove('showing'),
|
||||
});
|
||||
|
||||
previewTrigger.dataset.initialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
mutationsList.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
initializeFeedPreviewCards();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (document.getElementById('index-container')) {
|
||||
observer.observe(document.getElementById('index-container'), {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Preview card dropdowns reposition on scroll
|
||||
const dropdownRepositionListener = getDropdownRepositionListener();
|
||||
document.addEventListener('scroll', dropdownRepositionListener);
|
||||
|
||||
InstantClick.on('change', () => {
|
||||
observer.disconnect();
|
||||
document.removeEventListener('scroll', dropdownRepositionListener);
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
observer.disconnect();
|
||||
document.removeEventListener('scroll', dropdownRepositionListener);
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { h } from 'preact';
|
||||
|
||||
export const MinimalProfilePreviewCard = ({
|
||||
triggerId,
|
||||
contentId,
|
||||
username,
|
||||
name,
|
||||
profileImage,
|
||||
userId,
|
||||
}) => (
|
||||
<div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block">
|
||||
<button
|
||||
id={triggerId}
|
||||
aria-controls={contentId}
|
||||
class="profile-preview-card__trigger fs-s p-1 crayons-btn crayons-btn--ghost -ml-1"
|
||||
aria-label={`${name} profile details`}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
|
||||
<div
|
||||
id={contentId}
|
||||
class="profile-preview-card__content crayons-dropdown"
|
||||
style="border-top: var(--su-7) solid var(--card-color);"
|
||||
data-repositioning-dropdown="true"
|
||||
data-testid="profile-preview-card"
|
||||
>
|
||||
<div class="gap-4 grid">
|
||||
<div class="-mt-4">
|
||||
<a href={`/${username}`} class="flex">
|
||||
<span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0">
|
||||
<img
|
||||
src={profileImage}
|
||||
class="crayons-avatar__image"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
/>
|
||||
</span>
|
||||
<span class="crayons-link crayons-subtitle-2 mt-5">{name}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="print-hidden">
|
||||
<button
|
||||
class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100"
|
||||
data-info={JSON.stringify({
|
||||
id: userId,
|
||||
className: 'User',
|
||||
style: 'full',
|
||||
})}
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="author-preview-metadata-container"
|
||||
data-author-id={userId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
70
app/javascript/profilePreviewCards/UserMetadata.jsx
Normal file
70
app/javascript/profilePreviewCards/UserMetadata.jsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { h, Fragment } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
/**
|
||||
* Component which renders the user metadata detail in a profile preview card.
|
||||
*
|
||||
* @param {object} props
|
||||
* @param {string} props.email The user's email (if set to be publicly displayed)
|
||||
* @param {string} props.location The user's location
|
||||
* @param {string} props.created_at The user's join date string
|
||||
* @param {string} props.education The user's education detail
|
||||
* @param {string} props.work The user's work details
|
||||
*/
|
||||
export const UserMetadata = memo(
|
||||
({ email, location, summary, created_at, education, work }) => {
|
||||
const joinedOnDate = new Date(created_at);
|
||||
const joinedOnDateString = new Intl.DateTimeFormat(
|
||||
navigator.language || 'default',
|
||||
{
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
},
|
||||
).format(joinedOnDate);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{summary && <div className="color-base-70">{summary}</div>}
|
||||
<div className="user-metadata-details">
|
||||
<ul class="user-metadata-details-inner">
|
||||
{email && (
|
||||
<li>
|
||||
<div class="key">Email</div>
|
||||
<div class="value">
|
||||
<a href={`mailto:${email}`}>{email}</a>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{work && (
|
||||
<li>
|
||||
<div className="key">Work</div>
|
||||
<div className="value">{work}</div>
|
||||
</li>
|
||||
)}
|
||||
{location && (
|
||||
<li>
|
||||
<div class="key">Location</div>
|
||||
<div class="value">{location}</div>
|
||||
</li>
|
||||
)}
|
||||
{education && (
|
||||
<li>
|
||||
<div class="key">Education</div>
|
||||
<div class="value">{education}</div>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<div class="key">Joined</div>
|
||||
<div class="value">
|
||||
<time datetime={created_at} class="date">
|
||||
{joinedOnDateString}
|
||||
</time>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -35,9 +35,46 @@
|
|||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<a href="/<%= story.cached_user.username %>" class="crayons-story__secondary fw-medium">
|
||||
<a href="/<%= story.cached_user.username %>" class="crayons-story__secondary fw-medium m:hidden">
|
||||
<%= story.cached_user.name %>
|
||||
</a>
|
||||
<div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block">
|
||||
<button id="story-author-preview-trigger-<%= story.id %>" aria-controls="story-author-preview-content-<%= story.id %>" class="profile-preview-card__trigger fs-s p-1 -ml-1 crayons-btn crayons-btn--ghost" aria-label="<%= story.cached_user.name %> profile details">
|
||||
<%= story.cached_user.name %>
|
||||
</button>
|
||||
<div
|
||||
id="story-author-preview-content-<%= story.id %>"
|
||||
class="profile-preview-card__content crayons-dropdown"
|
||||
style="border-top: var(--su-7) solid var(--card-color);"
|
||||
data-repositioning-dropdown="true"
|
||||
data-testid="profile-preview-card">
|
||||
<div class="gap-4 grid">
|
||||
<div class="-mt-4">
|
||||
<a href="/<%= story.cached_user.username %>" class="flex">
|
||||
<span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0">
|
||||
<img
|
||||
src="<%= Images::Profile.call(story.cached_user.profile_image_url, length: 90) %>"
|
||||
class="crayons-avatar__image"
|
||||
alt=""
|
||||
loading="lazy" />
|
||||
</span>
|
||||
<span class="crayons-link crayons-subtitle-2 mt-5"><%= story.cached_user.name %></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="print-hidden">
|
||||
<button
|
||||
class="crayons-btn follow-action-button whitespace-nowrap follow-user w-100"
|
||||
data-info='{"id": <%= story.user_id %>, "className": "User", "style": "full"}'>
|
||||
Follow
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="author-preview-metadata-container"
|
||||
data-author-id="<%= story.user_id %>"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if story.cached_organization && !@organization_article_index %>
|
||||
<span>
|
||||
<span class="crayons-story__tertiary fw-normal"> for </span>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
<%= render "articles/sidebar" %>
|
||||
|
||||
<%# BEGIN Feed menu bar %>
|
||||
<main class="articles-list crayons-layout__content" id="main-content">
|
||||
<main class="articles-list crayons-layout__content" id="main-content" data-follow-button-container="true">
|
||||
<%= render(partial: "onboardings/task_card") if user_signed_in? %>
|
||||
|
||||
<header class="flex justify-between items-center p-2 px-3 m:p-0 m:px-0 m:pb-2">
|
||||
|
|
@ -90,5 +90,5 @@
|
|||
<%= render "articles/sidebar_additional" %>
|
||||
</div>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", defer: true %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<%= render "collections/meta", title_string: title_string, description_string: description_string, path: @collection.path %>
|
||||
<% end %>
|
||||
|
||||
<div class="crayons-layout crayons-layout--1-col">
|
||||
<main id="main-content" class="crayons-layout crayons-layout--1-col">
|
||||
<header class="px-2 m:px-0">
|
||||
<h1><%= title_string %></h1>
|
||||
<%= link_to "Back to #{@user.name}'s Series", user_series_path(@user.username) %>
|
||||
|
|
@ -17,9 +17,10 @@
|
|||
<% @articles.each do |article| %>
|
||||
<%= render "articles/single_story", story: article.decorate, featured: article.main_image.present? %>
|
||||
<% end %>
|
||||
<%= javascript_packs_with_chunks_tag "followButtons", "feedPreviewCards", defer: true %>
|
||||
<% else %>
|
||||
<p>This series doesn't have any articles</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@
|
|||
<span class="crayons-link crayons-subtitle-2 mt-5"><%= actor.name %></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="print-hidden">
|
||||
<%= follow_button(actor, style = "", classes = "w-100") %>
|
||||
</div>
|
||||
<% if actor.tag_line.present? %>
|
||||
<div class="color-base-70">
|
||||
<%= truncate (actor.tag_line || actor.summary || "Posts in this tag"), length: 200 %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="print-hidden">
|
||||
<%= follow_button(actor, style = "", classes = "w-100") %>
|
||||
</div>
|
||||
|
||||
<% if actor.class.name == "User" %>
|
||||
<div class="user-metadata-details">
|
||||
<ul class="user-metadata-details-inner">
|
||||
|
|
|
|||
|
|
@ -26,4 +26,4 @@
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "homePageFeedShortcuts", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "homePageFeedShortcuts", "followButtons", "feedPreviewCards", defer: true %>
|
||||
|
|
|
|||
|
|
@ -62,4 +62,4 @@
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "searchParams", "storiesList", "followButtons", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "searchParams", "storiesList", "followButtons", "feedPreviewCards", defer: true %>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
data-requires-approval="<%= @tag_model.requires_approval %>"
|
||||
data-articles-since="<%= Timeframe.datetime_iso8601(params[:timeframe]) %>">
|
||||
<%= render "stories/tagged_articles/sidebar" %>
|
||||
<main class="articles-list" id="main-content">
|
||||
<main class="articles-list" id="main-content" data-follow-button-container="true">
|
||||
<header class="flex items-center p-2 m:p-0 m:pb-2" id="on-page-nav-controls">
|
||||
<button type="button" class="crayons-btn crayons-btn--ghost crayons-btn--icon mr-2 inline-block m:hidden" id="on-page-nav-butt-left" aria-label="nav-button-left">
|
||||
<div class="crayons-icon nav-icon ">
|
||||
|
|
@ -113,5 +113,5 @@
|
|||
<%= render "stories/tagged_articles/sidebar_additional" %>
|
||||
</div>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", defer: true %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<main id="main-content">
|
||||
<% title @user.name %>
|
||||
|
||||
<%= render "shared/payment_pointer", user: @user %>
|
||||
|
|
@ -157,5 +158,6 @@
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", defer: true %>
|
||||
<%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", defer: true %>
|
||||
|
|
|
|||
5
cypress/fixtures/users/seriesUser.json
Normal file
5
cypress/fixtures/users/seriesUser.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"username": "series_user",
|
||||
"email": "series-user@forem.local",
|
||||
"password": "password"
|
||||
}
|
||||
|
|
@ -5,11 +5,7 @@ describe('Follow author from article sidebar', () => {
|
|||
cy.fixture('users/articleEditorV1User.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginAndVisit(user, '/').then(() => {
|
||||
cy.findAllByRole('link', { name: 'Test article' })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/test-article-slug').then(() => {
|
||||
cy.get('[data-follow-clicks-initialized]');
|
||||
cy.findByRole('heading', { name: 'Test article' });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,14 +37,11 @@ describe('Preview user profile from article page', () => {
|
|||
cy.fixture('users/articleEditorV1User.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginAndVisit(user, '/').then(() => {
|
||||
cy.findAllByRole('link', { name: 'Test article' })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
cy.loginAndVisit(user, '/admin_mcadmin/test-article-slug');
|
||||
|
||||
cy.get('[data-follow-clicks-initialized]');
|
||||
cy.findByRole('heading', { name: 'Test article' });
|
||||
});
|
||||
// Wait for the page to load
|
||||
cy.findByRole('button', { name: 'Share post options' });
|
||||
cy.get('[data-follow-clicks-initialized]');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
describe('Preview profile from series', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/articleEditorV1User.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginAndVisit(user, '/series_user/series');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a preview on a series article card', () => {
|
||||
// Go to an individual series listing
|
||||
cy.findByRole('link', { name: 'seriestest (1 Part Series)' }).click();
|
||||
cy.findByRole('heading', { name: "seriestest Series' Articles" });
|
||||
|
||||
// Find the preview button and check it functions as expected
|
||||
cy.findByRole('button', { name: 'Series User profile details' }).as(
|
||||
'previewButton',
|
||||
);
|
||||
cy.get('@previewButton').should('have.attr', 'data-initialized');
|
||||
cy.get('@previewButton').should('have.attr', 'aria-expanded', 'false');
|
||||
cy.get('@previewButton').click();
|
||||
|
||||
cy.findByTestId('profile-preview-card').within(() => {
|
||||
cy.findByRole('link', {
|
||||
name: 'Series User',
|
||||
}).should('have.focus');
|
||||
|
||||
// Check all the expected user data sections are present
|
||||
cy.findByText('Series user summary');
|
||||
cy.findByText('Software developer at Company');
|
||||
cy.findByText('Edinburgh');
|
||||
cy.findByText('University of Life');
|
||||
|
||||
cy.findByRole('button', { name: 'Follow' }).click();
|
||||
|
||||
// Check that the follow button has updated as expected
|
||||
cy.findByRole('button', { name: 'Follow' }).should('not.exist');
|
||||
cy.findByRole('button', { name: 'Following' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
describe('Home feed profile preview cards', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/articleEditorV1User.json').as('user');
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginAndVisit(user, '/');
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a profile preview card for a post's author", () => {
|
||||
cy.findAllByRole('button', { name: 'Admin McAdmin profile details' })
|
||||
.first()
|
||||
.as('previewButton');
|
||||
cy.get('@previewButton').should('have.attr', 'data-initialized');
|
||||
cy.get('@previewButton').click();
|
||||
|
||||
cy.findAllByTestId('profile-preview-card')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByRole('link', {
|
||||
name: 'Admin McAdmin',
|
||||
}).should('have.focus');
|
||||
|
||||
// Check all the expected user data sections are present
|
||||
cy.findByText('Admin user summary');
|
||||
cy.findByText('Software developer at Company');
|
||||
cy.findByText('Edinburgh');
|
||||
cy.findByText('University of Life');
|
||||
|
||||
cy.findByRole('button', { name: 'Follow' }).click();
|
||||
|
||||
// Check that following status has been updated
|
||||
cy.findByRole('button', { name: 'Follow' }).should('not.exist');
|
||||
cy.findByRole('button', { name: 'Following' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
describe('Logged out Home feed', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it("shows a profile preview card for an article's author", () => {
|
||||
cy.findAllByRole('button', { name: 'Admin McAdmin profile details' })
|
||||
.first()
|
||||
.as('previewButton');
|
||||
cy.get('@previewButton').should('have.attr', 'data-initialized');
|
||||
cy.get('@previewButton').click();
|
||||
|
||||
cy.findAllByTestId('profile-preview-card')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByRole('link', {
|
||||
name: 'Admin McAdmin',
|
||||
}).should('have.focus');
|
||||
|
||||
// Check all the expected user data sections are present
|
||||
cy.findByText('Admin user summary');
|
||||
cy.findByText('Software developer at Company');
|
||||
cy.findByText('Edinburgh');
|
||||
cy.findByText('University of Life');
|
||||
|
||||
cy.findByRole('button', { name: 'Follow' }).click();
|
||||
});
|
||||
|
||||
// Clicking a follow button while logged out should always trigger the log in to continue modal
|
||||
cy.findByTestId('modal-container').findByRole('heading', {
|
||||
name: 'Log in to continue',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
describe('Logged out - tag index page', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.visit('/t/tag1');
|
||||
});
|
||||
|
||||
it('shows a preview profile card for posts in the tag feed', () => {
|
||||
cy.visit('/t/tag1');
|
||||
cy.findAllByRole('button', { name: 'Admin McAdmin profile details' })
|
||||
.first()
|
||||
.as('previewButton');
|
||||
cy.get('@previewButton').should('have.attr', 'data-initialized');
|
||||
cy.get('@previewButton').click();
|
||||
|
||||
cy.findAllByTestId('profile-preview-card')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByRole('link', {
|
||||
name: 'Admin McAdmin',
|
||||
}).should('have.focus');
|
||||
|
||||
// Check all the expected user data sections are present
|
||||
cy.findByText('Admin user summary');
|
||||
cy.findByText('Software developer at Company');
|
||||
cy.findByText('Edinburgh');
|
||||
cy.findByText('University of Life');
|
||||
|
||||
cy.findByRole('button', { name: 'Follow' }).click();
|
||||
});
|
||||
|
||||
// Clicking a follow button while logged out should always trigger the log in to continue modal
|
||||
cy.findByTestId('modal-container').findByRole('heading', {
|
||||
name: 'Log in to continue',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
describe('Preview profile from post search results', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/articleEditorV1User.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginAndVisit(user, '/search?q=test');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows profile preview cards on posts in search results', () => {
|
||||
cy.findAllByRole('button', { name: 'Admin McAdmin profile details' })
|
||||
.first()
|
||||
.as('previewButton');
|
||||
cy.get('@previewButton').should('have.attr', 'data-initialized');
|
||||
cy.get('@previewButton').click();
|
||||
|
||||
cy.findAllByTestId('profile-preview-card')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByRole('link', {
|
||||
name: 'Admin McAdmin',
|
||||
}).should('have.focus');
|
||||
|
||||
// Check all the expected user data sections are present
|
||||
cy.findByText('Admin user summary');
|
||||
cy.findByText('Software developer at Company');
|
||||
cy.findByText('Edinburgh');
|
||||
cy.findByText('University of Life');
|
||||
|
||||
cy.findByRole('button', { name: 'Follow' }).click();
|
||||
|
||||
// Check that following status has been updated
|
||||
cy.findByRole('button', { name: 'Follow' }).should('not.exist');
|
||||
cy.findByRole('button', { name: 'Following' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
describe('Preview profile from the tag index page', () => {
|
||||
beforeEach(() => {
|
||||
cy.testSetup();
|
||||
cy.fixture('users/articleEditorV1User.json').as('user');
|
||||
|
||||
cy.get('@user').then((user) => {
|
||||
cy.loginAndVisit(user, '/t/tag1');
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a profile preview card for a tagged post's author", () => {
|
||||
cy.findAllByRole('button', { name: 'Admin McAdmin profile details' })
|
||||
.first()
|
||||
.as('previewButton');
|
||||
cy.get('@previewButton').should('have.attr', 'data-initialized');
|
||||
cy.get('@previewButton').click();
|
||||
|
||||
cy.findAllByTestId('profile-preview-card')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.findByRole('link', {
|
||||
name: 'Admin McAdmin',
|
||||
}).should('have.focus');
|
||||
|
||||
// Check all the expected user data sections are present
|
||||
cy.findByText('Admin user summary');
|
||||
cy.findByText('Software developer at Company');
|
||||
cy.findByText('Edinburgh');
|
||||
cy.findByText('University of Life');
|
||||
|
||||
cy.findByRole('button', { name: 'Follow' }).click();
|
||||
|
||||
// Check that following status has been updated
|
||||
cy.findByRole('button', { name: 'Follow' }).should('not.exist');
|
||||
cy.findByRole('button', { name: 'Following' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -379,6 +379,57 @@ end
|
|||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_doesnt_exist(User, "email", "series-user@forem.com") do
|
||||
series_user = User.create!(
|
||||
name: "Series User",
|
||||
email: "series-user@forem.local",
|
||||
username: "series_user",
|
||||
summary: Faker::Lorem.paragraph_by_chars(number: 199, supplemental: false),
|
||||
profile_image: File.open(Rails.root.join("app/assets/images/#{rand(1..40)}.png")),
|
||||
website_url: Faker::Internet.url,
|
||||
confirmed_at: Time.current,
|
||||
password: "password",
|
||||
password_confirmation: "password",
|
||||
saw_onboarding: true,
|
||||
checked_code_of_conduct: true,
|
||||
checked_terms_and_conditions: true,
|
||||
)
|
||||
series_user.profile.update(
|
||||
summary: "Series user summary",
|
||||
work: "Software developer at Company",
|
||||
location: "Edinburgh",
|
||||
education: "University of Life",
|
||||
)
|
||||
series_user.notification_setting.update(
|
||||
email_comment_notifications: false,
|
||||
email_follower_notifications: false,
|
||||
)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_doesnt_exist(Article, "title", "Series test article") do
|
||||
markdown = <<~MARKDOWN
|
||||
---
|
||||
title: Series test article
|
||||
published: true
|
||||
cover_image: #{Faker::Company.logo}
|
||||
series: seriestest
|
||||
---
|
||||
#{Faker::Hipster.paragraph(sentence_count: 2)}
|
||||
#{Faker::Markdown.random}
|
||||
#{Faker::Hipster.paragraph(sentence_count: 2)}
|
||||
MARKDOWN
|
||||
Article.create(
|
||||
body_markdown: markdown,
|
||||
featured: true,
|
||||
show_comments: true,
|
||||
user_id: User.find_by(email: "series-user@forem.local").id,
|
||||
)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_none(ListingCategory) do
|
||||
ListingCategory.create!(
|
||||
slug: "cfp",
|
||||
|
|
@ -426,6 +477,28 @@ Settings::General.sidebar_tags = %i[tag1]
|
|||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_doesnt_exist(Article, "title", "Tag test article") do
|
||||
markdown = <<~MARKDOWN
|
||||
---
|
||||
title: Tag test article
|
||||
published: true
|
||||
cover_image: #{Faker::Company.logo}
|
||||
tags: tag1
|
||||
---
|
||||
#{Faker::Hipster.paragraph(sentence_count: 2)}
|
||||
#{Faker::Markdown.random}
|
||||
#{Faker::Hipster.paragraph(sentence_count: 2)}
|
||||
MARKDOWN
|
||||
Article.create(
|
||||
body_markdown: markdown,
|
||||
featured: true,
|
||||
show_comments: true,
|
||||
user_id: admin_user.id,
|
||||
)
|
||||
end
|
||||
|
||||
##############################################################################
|
||||
|
||||
seeder.create_if_none(Badge) do
|
||||
Badge.create!(
|
||||
title: "#{Faker::Lorem.word} #{rand(100)}",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ RSpec.describe "Display articles search spec", type: :system, js: true do
|
|||
expect(page).to have_selector("button[data-reactable-id=\"#{found_article_one.id}\"]")
|
||||
expect(page).to have_content("5 min read")
|
||||
expect(find_link("#ruby")["href"]).to include("/t/ruby")
|
||||
expect(find_link(found_article_one.user.name)["href"]).to include(found_article_one.username)
|
||||
expect(page).to have_content("3 reactions")
|
||||
expect(page).to have_content("2 comments")
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue