@@ -45,7 +47,7 @@ export const Meta = ({ article, organization }) => {
{filterXSS(
article.class_name === 'User'
@@ -53,6 +55,14 @@ export const Meta = ({ article, organization }) => {
: article.user.name,
)}
+
{organization &&
!document.getElementById('organization-article-index') && (
diff --git a/app/javascript/packs/feedPreviewCards.jsx b/app/javascript/packs/feedPreviewCards.jsx
new file mode 100644
index 000000000..3fbe3c5f9
--- /dev/null
+++ b/app/javascript/packs/feedPreviewCards.jsx
@@ -0,0 +1,6 @@
+import('../previewCards/feedPreviewCards').then(
+ ({ initializeFeedPreviewCards, listenForHoveredOrFocusedStoryCards }) => {
+ initializeFeedPreviewCards();
+ listenForHoveredOrFocusedStoryCards();
+ },
+);
diff --git a/app/javascript/previewCards/feedPreviewCards.jsx b/app/javascript/previewCards/feedPreviewCards.jsx
new file mode 100644
index 000000000..2e757f08d
--- /dev/null
+++ b/app/javascript/previewCards/feedPreviewCards.jsx
@@ -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(, 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);
+});
diff --git a/app/javascript/profilePreviewCards/MinimalProfilePreviewCard.jsx b/app/javascript/profilePreviewCards/MinimalProfilePreviewCard.jsx
new file mode 100644
index 000000000..13a0c5f53
--- /dev/null
+++ b/app/javascript/profilePreviewCards/MinimalProfilePreviewCard.jsx
@@ -0,0 +1,61 @@
+import { h } from 'preact';
+
+export const MinimalProfilePreviewCard = ({
+ triggerId,
+ contentId,
+ username,
+ name,
+ profileImage,
+ userId,
+}) => (
+
+);
diff --git a/app/javascript/profilePreviewCards/UserMetadata.jsx b/app/javascript/profilePreviewCards/UserMetadata.jsx
new file mode 100644
index 000000000..26bc83d53
--- /dev/null
+++ b/app/javascript/profilePreviewCards/UserMetadata.jsx
@@ -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 (
+
+ {summary && {summary}
}
+
+
+ );
+ },
+);
diff --git a/app/views/articles/_single_story.html.erb b/app/views/articles/_single_story.html.erb
index 20873ad1e..fceb01398 100644
--- a/app/views/articles/_single_story.html.erb
+++ b/app/views/articles/_single_story.html.erb
@@ -35,9 +35,46 @@
- <%= javascript_packs_with_chunks_tag "storiesList", defer: true %>
+ <%= javascript_packs_with_chunks_tag "storiesList", "followButtons", "feedPreviewCards", defer: true %>
<% end %>
diff --git a/app/views/collections/show.html.erb b/app/views/collections/show.html.erb
index f4f22dc31..02ca401da 100644
--- a/app/views/collections/show.html.erb
+++ b/app/views/collections/show.html.erb
@@ -5,7 +5,7 @@
<%= render "collections/meta", title_string: title_string, description_string: description_string, path: @collection.path %>
<% end %>
-