Skip to content

Commit 06e7e7b

Browse files
AmarTrebinjacrebelchrissshanzel
authored
feat: polls (#4902)
Co-authored-by: Chris Bongers <chrisbongers@gmail.com> Co-authored-by: Lee Hansel Solevilla <sshanzel@yahoo.com> Co-authored-by: Lee Hansel Solevilla <13744167+sshanzel@users.noreply.github.com>
1 parent 7f179ca commit 06e7e7b

66 files changed

Lines changed: 1979 additions & 251 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/shared/src/components/Feed.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ const BriefPostModal = dynamic(
117117
import(/* webpackChunkName: "briefPostModal" */ './modals/BriefPostModal'),
118118
);
119119

120+
const PollPostModal = dynamic(
121+
() =>
122+
import(/* webpackChunkName: "pollPostModal" */ './modals/PollPostModal'),
123+
);
124+
120125
const BriefCardFeed = dynamic(
121126
() =>
122127
import(
@@ -137,6 +142,7 @@ export const PostModalMap: Record<PostType, typeof ArticlePostModal> = {
137142
[PostType.VideoYouTube]: ArticlePostModal,
138143
[PostType.Collection]: CollectionPostModal,
139144
[PostType.Brief]: BriefPostModal,
145+
[PostType.Poll]: PollPostModal,
140146
};
141147

142148
export default function Feed<T>({

packages/shared/src/components/FeedItemComponent.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import { adLogEvent, feedLogExtra } from '../lib/feed';
4242
import { useLogContext } from '../contexts/LogContext';
4343
import { MarketingCtaVariant } from './marketingCta/common';
4444
import { MarketingCtaBriefing } from './marketingCta/MarketingCtaBriefing';
45+
import PollGrid from './cards/poll/PollGrid';
46+
import { PollList } from './cards/poll/PollList';
4547

4648
export type FeedItemComponentProps = {
4749
item: FeedItem;
@@ -100,6 +102,7 @@ const PostTypeToTagCard: Record<PostType, FunctionComponent> = {
100102
[PostType.VideoYouTube]: ArticleGrid,
101103
[PostType.Collection]: CollectionGrid,
102104
[PostType.Brief]: BriefCard,
105+
[PostType.Poll]: PollGrid,
103106
};
104107

105108
const PostTypeToTagList: Record<PostType, FunctionComponent> = {
@@ -110,6 +113,7 @@ const PostTypeToTagList: Record<PostType, FunctionComponent> = {
110113
[PostType.VideoYouTube]: ArticleList,
111114
[PostType.Collection]: CollectionList,
112115
[PostType.Brief]: BriefCard,
116+
[PostType.Poll]: PollList,
113117
};
114118

115119
type GetTagsProps = {

packages/shared/src/components/Pill.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import React from 'react';
33
import classNames from 'classnames';
44

55
export enum PillSize {
6+
XSmall = 'xsmall',
67
Small = 'small',
78
Medium = 'medium',
89
}
910

1011
const pillSizeToClassName: Record<PillSize, string> = {
12+
[PillSize.XSmall]: 'font-bold typo-caption2 rounded-4 px-1',
1113
[PillSize.Small]: 'font-bold typo-footnote rounded-8 p-1 px-2',
1214
[PillSize.Medium]: 'font-bold typo-caption1 rounded-10 p-2',
1315
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from 'react';
2+
import isAfter from 'date-fns/isAfter';
3+
import {
4+
Typography,
5+
TypographyColor,
6+
TypographyTag,
7+
TypographyType,
8+
} from '../../typography/Typography';
9+
import { Separator } from './common';
10+
import type { Post } from '../../../graphql/posts';
11+
import { largeNumberFormat } from '../../../lib';
12+
13+
const MIN_VOTES_REQUIRED = 10;
14+
15+
export type PollMetadataProps = Pick<Post, 'endsAt' | 'numPollVotes'> & {
16+
isAuthor: boolean;
17+
};
18+
19+
const PollMetadata = ({
20+
endsAt,
21+
isAuthor,
22+
numPollVotes,
23+
}: PollMetadataProps) => {
24+
const shouldShowVotes = numPollVotes > MIN_VOTES_REQUIRED || isAuthor;
25+
const pollHasEnded = endsAt && isAfter(new Date(), new Date(endsAt));
26+
27+
return (
28+
<>
29+
{pollHasEnded && (
30+
<>
31+
<Typography tag={TypographyTag.Span} type={TypographyType.Footnote}>
32+
Voting ended
33+
</Typography>
34+
<Separator />
35+
</>
36+
)}
37+
<>
38+
<Typography
39+
tag={TypographyTag.Span}
40+
type={TypographyType.Footnote}
41+
color={TypographyColor.StatusSuccess}
42+
>
43+
{shouldShowVotes ? 'Voting open' : 'New poll'}
44+
</Typography>
45+
<Separator />
46+
</>
47+
{shouldShowVotes && (
48+
<>
49+
<Typography tag={TypographyTag.Span} type={TypographyType.Footnote}>
50+
{largeNumberFormat(numPollVotes)}{' '}
51+
{pollHasEnded ? 'total votes' : 'votes'}
52+
</Typography>
53+
<Separator />
54+
</>
55+
)}
56+
</>
57+
);
58+
};
59+
60+
export default PollMetadata;

packages/shared/src/components/cards/common/PostMetadata.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { formatReadTime, TruncateText, DateFormat } from '../../utilities';
88
import { largeNumberFormat } from '../../../lib';
99
import { useFeedCardContext } from '../../../features/posts/FeedCardContext';
1010
import { Tooltip } from '../../tooltip/Tooltip';
11+
import type { PollMetadataProps } from './PollMetadata';
12+
import PollMetadata from './PollMetadata';
1113

1214
interface PostMetadataProps
1315
extends Pick<Post, 'createdAt' | 'readTime' | 'numUpvotes'> {
@@ -16,6 +18,7 @@ interface PostMetadataProps
1618
children?: ReactNode;
1719
isVideoType?: boolean;
1820
domain?: ReactNode;
21+
pollMetadata?: PollMetadataProps;
1922
}
2023

2124
export default function PostMetadata({
@@ -27,6 +30,7 @@ export default function PostMetadata({
2730
description,
2831
isVideoType,
2932
domain,
33+
pollMetadata,
3034
}: PostMetadataProps): ReactElement {
3135
const timeActionContent = isVideoType ? 'watch' : 'read';
3236
const showReadTime = isVideoType ? Number.isInteger(readTime) : !!readTime;
@@ -49,6 +53,7 @@ export default function PostMetadata({
4953
<Separator />
5054
)}
5155
{!!description && description}
56+
{pollMetadata && <PollMetadata {...pollMetadata} />}
5257
{!!createdAt && !!description && !boostedBy && <Separator />}
5358
{!!createdAt && !boostedBy && (
5459
<DateFormat date={createdAt} type={TimeFormatType.Post} />
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { Ref } from 'react';
2+
import React, { forwardRef, useState } from 'react';
3+
import FeedItemContainer from '../common/FeedItemContainer';
4+
import {
5+
CardTextContainer,
6+
CardTitle,
7+
getPostClassNames,
8+
} from '../common/Card';
9+
import { SquadPostCardHeader } from '../common/SquadPostCardHeader';
10+
import type { PostCardProps } from '../common/common';
11+
import { Container } from '../common/common';
12+
import ActionButtons from '../ActionsButtons';
13+
import PollOptions from './PollOptions';
14+
import PostMetadata from '../common/PostMetadata';
15+
import { useAuthContext } from '../../../contexts/AuthContext';
16+
import usePoll from '../../../hooks/usePoll';
17+
import CardOverlay from '../common/CardOverlay';
18+
import { useSmartTitle } from '../../../hooks/post/useSmartTitle';
19+
20+
const PollGrid = forwardRef(function PollCard(
21+
{
22+
post,
23+
onPostClick,
24+
onPostAuxClick,
25+
onUpvoteClick,
26+
onDownvoteClick,
27+
onCommentClick,
28+
onBookmarkClick,
29+
onCopyLinkClick,
30+
domProps = {},
31+
}: PostCardProps,
32+
ref: Ref<HTMLElement>,
33+
) {
34+
const { user } = useAuthContext();
35+
const { onVote, isCastingVote } = usePoll({ post });
36+
const [shouldAnimateResults, setShouldAnimateResults] = useState(false);
37+
38+
const handleVote = (optionId: string, text: string) => {
39+
if (!isCastingVote) {
40+
onVote(optionId, text);
41+
setShouldAnimateResults(true);
42+
}
43+
};
44+
const { title } = useSmartTitle(post);
45+
46+
const { pinnedAt, trending, pollOptions, endsAt, numPollVotes, source } =
47+
post;
48+
49+
return (
50+
<FeedItemContainer
51+
ref={ref}
52+
domProps={{
53+
...domProps,
54+
className: getPostClassNames(post, domProps?.className, 'min-h-card'),
55+
}}
56+
flagProps={{ pinnedAt, trending }}
57+
>
58+
<CardOverlay
59+
post={post}
60+
onPostCardAuxClick={() => onPostAuxClick(post)}
61+
onPostCardClick={() => onPostClick(post)}
62+
/>
63+
<SquadPostCardHeader
64+
post={post}
65+
enableSourceHeader={source.type === 'squad'}
66+
/>
67+
<CardTextContainer>
68+
<CardTitle>{title}</CardTitle>
69+
</CardTextContainer>
70+
<Container className="justify-end gap-2">
71+
<PostMetadata
72+
createdAt={post.createdAt}
73+
className="mx-4"
74+
pollMetadata={{
75+
endsAt,
76+
isAuthor: user?.id === post.author?.id,
77+
numPollVotes,
78+
}}
79+
/>
80+
<PollOptions
81+
className={{
82+
container: 'px-2',
83+
}}
84+
options={pollOptions}
85+
onClick={handleVote}
86+
userVote={post?.userState?.pollOption?.id}
87+
numPollVotes={numPollVotes || 0}
88+
endsAt={endsAt}
89+
shouldAnimateResults={shouldAnimateResults}
90+
/>
91+
<ActionButtons
92+
post={post}
93+
onUpvoteClick={onUpvoteClick}
94+
onCommentClick={onCommentClick}
95+
onCopyLinkClick={onCopyLinkClick}
96+
onBookmarkClick={onBookmarkClick}
97+
onDownvoteClick={onDownvoteClick}
98+
/>
99+
</Container>
100+
</FeedItemContainer>
101+
);
102+
});
103+
104+
export default PollGrid;

0 commit comments

Comments
 (0)