Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions packages/shared/src/components/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,18 +173,19 @@ const calculateRow = (index: number, numCards: number): number =>
const calculateColumn = (index: number, numCards: number): number =>
index % numCards;

export const PostModalMap: Record<PostType, typeof ArticlePostModal> = {
[PostType.Article]: ArticlePostModal,
[PostType.Share]: SharePostModal,
[PostType.Welcome]: SharePostModal,
[PostType.Freeform]: SharePostModal,
[PostType.VideoYouTube]: ArticlePostModal,
[PostType.Collection]: CollectionPostModal,
[PostType.Brief]: BriefPostModal,
[PostType.Digest]: ArticlePostModal,
[PostType.Poll]: PollPostModal,
[PostType.SocialTwitter]: SocialTwitterPostModal,
};
export const PostModalMap: Partial<Record<PostType, typeof ArticlePostModal>> =
{
[PostType.Article]: ArticlePostModal,
[PostType.Share]: SharePostModal,
[PostType.Welcome]: SharePostModal,
[PostType.Freeform]: SharePostModal,
[PostType.VideoYouTube]: ArticlePostModal,
[PostType.Collection]: CollectionPostModal,
[PostType.Brief]: BriefPostModal,
[PostType.Digest]: ArticlePostModal,
[PostType.Poll]: PollPostModal,
[PostType.SocialTwitter]: SocialTwitterPostModal,
};

export default function Feed<T>({
feedName,
Expand Down Expand Up @@ -596,7 +597,9 @@ export default function Feed<T>({
const isMiddleClick = event?.type === 'auxclick' || event?.button === 1;
const isModifierClick = !!(event && (event.ctrlKey || event.metaKey));
const readerEligible = isReaderEligiblePost(post);
const skipsPostModal = post.type === PostType.LiveRoom;
const shouldOpenModal =
!skipsPostModal &&
!isAuxClick &&
!isMiddleClick &&
!isModifierClick &&
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/components/FeedItemComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import PollGrid from './cards/poll/PollGrid';
import { PollList } from './cards/poll/PollList';
import { SocialTwitterGrid } from './cards/socialTwitter/SocialTwitterGrid';
import { SocialTwitterList } from './cards/socialTwitter/SocialTwitterList';
import { LiveRoomPostGrid } from './cards/liveRoom/LiveRoomPostGrid';
import { LiveRoomPostList } from './cards/liveRoom/LiveRoomPostList';
import { SignalList } from './cards/common/list/SignalList';
import { OtherFeedPage } from '../lib/query';
import { isSourceSquadOrMachine } from '../graphql/sources';
Expand Down Expand Up @@ -124,6 +126,7 @@ const PostTypeToTagCard: Record<PostType, React.ComponentType<any>> = {
[PostType.Poll]: PollGrid,
[PostType.SocialTwitter]: SocialTwitterGrid,
[PostType.Digest]: ArticleGrid,
[PostType.LiveRoom]: LiveRoomPostGrid,
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -138,6 +141,7 @@ const PostTypeToTagList: Record<PostType, React.ComponentType<any>> = {
[PostType.Poll]: PollList,
[PostType.SocialTwitter]: SocialTwitterList,
[PostType.Digest]: ArticleList,
[PostType.LiveRoom]: LiveRoomPostList,
};

const getPostTypeForCard = (post?: Post): PostType => {
Expand Down
25 changes: 14 additions & 11 deletions packages/shared/src/components/cards/common/PostCardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ interface CardHeaderProps {
openNewTab?: boolean;
flagProps?: FlagProps;
showFeedback?: boolean;
primaryAction?: ReactNode;
}

const Container = getGroupedHoverContainer('span');
Expand All @@ -59,6 +60,7 @@ export const PostCardHeader = ({
postLink,
openNewTab,
showFeedback,
primaryAction,
}: CardHeaderProps): ReactElement => {
const isFeedPreview = useFeedPreviewMode();
const isSharedPostDeleted = post.sharedPost?.id === DeletedPostId;
Expand Down Expand Up @@ -112,17 +114,18 @@ export const PostCardHeader = ({
>
{!isFeedPreview && (
<>
{!isSharedPostDeleted && (
<ReadArticleButton
content={getReadPostButtonText(post)}
className="mr-2"
variant={ButtonVariant.Primary}
href={articleLink ?? ''}
onClick={onReadArticleClick}
openNewTab={openNewTab}
icon={getReadPostButtonIcon(post)}
/>
)}
{!isSharedPostDeleted &&
(primaryAction ?? (
<ReadArticleButton
content={getReadPostButtonText(post)}
className="mr-2"
variant={ButtonVariant.Primary}
href={articleLink ?? ''}
onClick={onReadArticleClick}
openNewTab={openNewTab}
icon={getReadPostButtonIcon(post)}
/>
))}
<PostOptionButton post={post} />
</>
)}
Expand Down
26 changes: 15 additions & 11 deletions packages/shared/src/components/cards/common/list/PostCardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface CardHeaderProps {
openNewTab?: boolean;
readButtonContent?: string;
readButtonIcon?: ReactElement;
primaryAction?: ReactNode;
metadata?: {
topLabel?: PostMetadataProps['topLabel'];
bottomLabel?: PostMetadataProps['bottomLabel'];
Expand All @@ -60,6 +61,7 @@ export const PostCardHeader = ({
openNewTab,
readButtonContent,
readButtonIcon,
primaryAction,
metadata,
}: CardHeaderProps): ReactElement => {
const isFeedPreview = useFeedPreviewMode();
Expand All @@ -73,6 +75,7 @@ export const PostCardHeader = ({
const showCTA =
!isFeedPreview &&
([PostType.Article, PostType.VideoYouTube].includes(post.type) ||
!!primaryAction ||
!!readButtonContent);

return (
Expand Down Expand Up @@ -115,17 +118,18 @@ export const PostCardHeader = ({
>
{!isFeedPreview && (
<>
{showCTA && (
<ReadArticleButton
content={readButtonContent ?? postButtonText ?? ''}
className="mr-2"
variant={ButtonVariant.Tertiary}
icon={readButtonIcon ?? <OpenLinkIcon />}
href={postLink ?? ''}
onClick={onReadArticleClick}
openNewTab={openNewTab}
/>
)}
{showCTA &&
(primaryAction ?? (
<ReadArticleButton
content={readButtonContent ?? postButtonText ?? ''}
className="mr-2"
variant={ButtonVariant.Tertiary}
icon={readButtonIcon ?? <OpenLinkIcon />}
href={postLink ?? ''}
onClick={onReadArticleClick}
openNewTab={openNewTab}
/>
))}
<PostOptionButton post={post} />
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const typeToClassName: Partial<Record<PostType, string>> = {

const typeToLabel: Partial<Record<PostType, string>> = {
[PostType.VideoYouTube]: 'Video',
[PostType.LiveRoom]: 'Standup',
};

const excludedTypes = new Set<PostType>([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import type { RenderResult } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { QueryClient } from '@tanstack/react-query';
import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import post from '../../../../__tests__/fixture/post';
import { TestBootProvider } from '../../../../__tests__/helpers/boot';
import type { LiveRoomPost } from '../../../graphql/liveRooms';
import { LiveRoomStatus } from '../../../graphql/liveRooms';
import { PostType } from '../../../graphql/posts';
import type { Post } from '../../../graphql/posts';
import type { PostCardProps } from '../common/common';
import { LiveRoomPostGrid } from './LiveRoomPostGrid';

jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));

const room: LiveRoomPost = {
id: 'room-1',
topic: 'Weekly product standup',
status: LiveRoomStatus.Created,
scheduledStart: '2026-05-20T10:00:00.000Z',
subscribed: false,
};

const liveRoomPost: Post = {
...post,
id: 'post-1',
title: 'Fallback title',
type: PostType.LiveRoom,
liveRoom: room,
};

const defaultProps: PostCardProps = {
post: liveRoomPost,
onPostClick: jest.fn(),
onUpvoteClick: jest.fn(),
onCommentClick: jest.fn(),
onBookmarkClick: jest.fn(),
onShare: jest.fn(),
onCopyLinkClick: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
jest.mocked(useRouter).mockImplementation(
() =>
({
pathname: '/',
} as unknown as NextRouter),
);
});

const renderComponent = (props: Partial<PostCardProps> = {}): RenderResult =>
render(
<TestBootProvider client={new QueryClient()}>
<LiveRoomPostGrid {...defaultProps} {...props} />
</TestBootProvider>,
);

it('renders live room post title, scheduled time, and RSVP action', async () => {
renderComponent();

expect(await screen.findByText('Weekly product standup')).toBeInTheDocument();
expect(screen.getByText(/May 20 at/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /RSVP/ })).toBeInTheDocument();
});

it('links the card to the standup route', async () => {
renderComponent();

const link = await screen.findByLabelText('Weekly product standup');
expect(link).toHaveAttribute(
'href',
expect.stringContaining('/standups/room-1'),
);
});
126 changes: 126 additions & 0 deletions packages/shared/src/components/cards/liveRoom/LiveRoomPostGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { ReactElement, Ref } from 'react';
import React, { forwardRef } from 'react';
import classNames from 'classnames';
import type { PostCardProps } from '../common/common';
import type { Source } from '../../../graphql/sources';
import { Container } from '../common/common';
import FeedItemContainer from '../common/FeedItemContainer';
import {
CardSpace,
CardTextContainer,
CardTitle,
getPostClassNames,
} from '../common/Card';
import { PostCardHeader } from '../common/PostCardHeader';
import PostTags from '../common/PostTags';
import { PostCardFooter } from '../common/PostCardFooter';
import ActionButtons from '../common/ActionButtons';
import {
getLiveRoomPostPath,
getLiveRoomPostRoom,
getLiveRoomPostTitle,
LiveRoomPostKicker,
LiveRoomPostOverlay,
LiveRoomPostRsvpButton,
LiveRoomPostScheduledStart,
} from './common';

export const LiveRoomPostGrid = forwardRef(function LiveRoomPostGrid(
{
post,
onPostClick,
onPostAuxClick,
onUpvoteClick,
onDownvoteClick,
onCommentClick,
onBookmarkClick,
onShare,
onCopyLinkClick,
openNewTab,
children,
domProps = {},
eagerLoadImage = false,
}: PostCardProps,
ref: Ref<HTMLElement>,
): ReactElement {
const { className, style } = domProps;
const { pinnedAt, trending } = post;
const room = getLiveRoomPostRoom(post);
const title = getLiveRoomPostTitle(post);
const { source } = post;

if (!source) {
throw new Error(`Live room post ${post.id} is missing source`);
}

const onPostCardClick = (event: React.MouseEvent<HTMLAnchorElement>) =>
onPostClick?.(post, event);
const onPostCardAuxClick = () => onPostAuxClick?.(post);

return (
<FeedItemContainer
domProps={{
...domProps,
style,
className: getPostClassNames(post, classNames(className), 'min-h-card'),
}}
ref={ref}
flagProps={{ pinnedAt, trending }}
bookmarked={post.bookmarked}
>
<LiveRoomPostOverlay
post={post}
room={room}
onPostCardClick={onPostCardClick}
onPostCardAuxClick={onPostCardAuxClick}
/>
<div className="flex flex-1 flex-col">
<CardTextContainer>
<PostCardHeader
post={post}
className="flex"
openNewTab={openNewTab}
source={source as Source}
postLink={post.permalink ?? getLiveRoomPostPath(room)}
primaryAction={
<LiveRoomPostRsvpButton
room={room}
hostUserId={post.author?.id}
/>
}
/>
<LiveRoomPostKicker room={room} className="mt-2" />
<CardTitle>{title}</CardTitle>
</CardTextContainer>
<Container>
<CardSpace />
<div className="mx-4 flex items-center">
<PostTags post={post} />
</div>
<LiveRoomPostScheduledStart className="mx-4" room={room} />
</Container>
<Container>
<PostCardFooter
openNewTab={openNewTab ?? false}
post={post}
onShare={onShare}
className={{
image: classNames('px-1'),
}}
eagerLoadImage={eagerLoadImage}
/>

<ActionButtons
post={post}
onUpvoteClick={onUpvoteClick}
onCommentClick={onCommentClick}
onCopyLinkClick={onCopyLinkClick}
onBookmarkClick={onBookmarkClick}
onDownvoteClick={onDownvoteClick}
/>
</Container>
</div>
{children}
</FeedItemContainer>
);
});
Loading
Loading