diff --git a/packages/shared/src/components/cards/collection/CollectionGrid.tsx b/packages/shared/src/components/cards/collection/CollectionGrid.tsx index 51d4b05eed2..1f7ee126c6e 100644 --- a/packages/shared/src/components/cards/collection/CollectionGrid.tsx +++ b/packages/shared/src/components/cards/collection/CollectionGrid.tsx @@ -16,6 +16,8 @@ import PostMetadata from '../common/PostMetadata'; import { usePostImage } from '../../../hooks/post/usePostImage'; import CardOverlay from '../common/CardOverlay'; import PostTags from '../common/PostTags'; +import { isPostUpdated } from '../../../graphql/posts'; +import { TimeFormatType } from '../../../lib/dateFormat'; export const CollectionGrid = forwardRef(function CollectionCard( { @@ -35,14 +37,19 @@ export const CollectionGrid = forwardRef(function CollectionCard( ) { const { pinnedAt, trending } = post; const image = usePostImage(post); - const onPostCardClick = () => onPostClick(post); - const onPostCardAuxClick = () => onPostAuxClick(post); + const wasUpdated = isPostUpdated(post); + const onPostCardClick = () => onPostClick?.(post); + const onPostCardAuxClick = () => onPostAuxClick?.(post); return ( diff --git a/packages/shared/src/components/cards/collection/CollectionList.tsx b/packages/shared/src/components/cards/collection/CollectionList.tsx index ffbbf9085bd..900da81b08a 100644 --- a/packages/shared/src/components/cards/collection/CollectionList.tsx +++ b/packages/shared/src/components/cards/collection/CollectionList.tsx @@ -18,6 +18,8 @@ import { useTruncatedSummary, useViewSize, ViewSize } from '../../../hooks'; import PostTags from '../common/PostTags'; import { CardCoverList } from '../common/list/CardCover'; import { HIGH_PRIORITY_IMAGE_PROPS } from '../../image/Image'; +import { isPostUpdated } from '../../../graphql/posts'; +import { TimeFormatType } from '../../../lib/dateFormat'; export const CollectionList = forwardRef(function CollectionCard( { @@ -37,7 +39,8 @@ export const CollectionList = forwardRef(function CollectionCard( ) { const isMobile = useViewSize(ViewSize.MobileL); const image = usePostImage(post); - const { title } = useTruncatedSummary(post?.title); + const { title } = useTruncatedSummary(post?.title ?? ''); + const wasUpdated = isPostUpdated(post); const actionButtons = ( onPostClick(post), + onClick: () => onPostClick?.(post), href: post.commentsPermalink, }} bookmarked={post.bookmarked} > - + diff --git a/packages/shared/src/components/cards/common/PostMetadata.tsx b/packages/shared/src/components/cards/common/PostMetadata.tsx index 137e5e314b5..54b438d08fd 100644 --- a/packages/shared/src/components/cards/common/PostMetadata.tsx +++ b/packages/shared/src/components/cards/common/PostMetadata.tsx @@ -6,6 +6,7 @@ import { Separator } from './common'; import type { Post } from '../../../graphql/posts'; import { formatReadTime, DateFormat } from '../../utilities'; import { largeNumberFormat } from '../../../lib'; +import { pluralize } from '../../../lib/strings'; import { useFeedCardContext } from '../../../features/posts/FeedCardContext'; import { Tooltip } from '../../tooltip/Tooltip'; import type { PollMetadataProps } from './PollMetadata'; @@ -20,6 +21,9 @@ interface PostMetadataProps isVideoType?: boolean; domain?: ReactNode; pollMetadata?: PollMetadataProps; + numSources?: number; + dateLabel?: string; + dateType?: TimeFormatType; } export default function PostMetadata({ @@ -32,6 +36,9 @@ export default function PostMetadata({ isVideoType, domain, pollMetadata, + numSources, + dateLabel, + dateType = TimeFormatType.Post, }: PostMetadataProps): ReactElement { const hasUpvoteCount = typeof numUpvotes === 'number'; const upvoteCount = numUpvotes ?? 0; @@ -62,7 +69,13 @@ export default function PostMetadata({ !!createdAt && !boostedBy && { key: 'date', - node: , + node: ( + + ), }, showReadTime && { key: 'readTime', @@ -72,6 +85,15 @@ export default function PostMetadata({ ), }, + !!numSources && + numSources > 0 && { + key: 'sources', + node: ( + + {numSources} {pluralize('source', numSources)} + + ), + }, !!showReadTime && domain && { key: 'domain', node: domain }, hasUpvoteCount && upvoteCount > 0 && { @@ -87,7 +109,7 @@ export default function PostMetadata({ return (
diff --git a/packages/shared/src/components/cards/common/RaisedLabel.tsx b/packages/shared/src/components/cards/common/RaisedLabel.tsx index 3036a6253d1..611f4f7f45b 100644 --- a/packages/shared/src/components/cards/common/RaisedLabel.tsx +++ b/packages/shared/src/components/cards/common/RaisedLabel.tsx @@ -56,7 +56,7 @@ export function RaisedLabel({ )} > 0} + condition={!!description?.length} wrapper={(children) => ( {children} )} @@ -84,7 +84,7 @@ export function RaisedLabel({ )} > 0} + condition={!!description?.length} wrapper={(children) => ( {children} )} diff --git a/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx b/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx index 04562185537..5148d2df4e7 100644 --- a/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx +++ b/packages/shared/src/components/cards/common/list/FeedItemContainer.tsx @@ -47,12 +47,13 @@ function FeedItemContainer( ? RaisedLabelType.Pinned : RaisedLabelType.Hot; const description = - [RaisedLabelType.Hot].includes(raisedLabelType) && trending > 0 + raisedLabelType === RaisedLabelType.Hot && trending && trending > 0 ? `${trending} devs read it last hour` : undefined; const isFeedPreview = useFeedPreviewMode(); const showFlag = (!!pinnedAt || !!trending) && !isFeedPreview; - const showTypeLabel = !!adAttribution || !!type; + const typeLabelValue = adAttribution ?? type; + const showTypeLabel = !!typeLabelValue; const [focus, setFocus] = useState(false); return ( @@ -70,7 +71,7 @@ function FeedItemContainer( ...(highlightBookmarkedPost && { background: bookmarkProviderListBg }), }} > - {linkProps && ( + {linkProps?.href && ( - {showTypeLabel && ( + {typeLabelValue && ( )} diff --git a/packages/shared/src/components/cards/common/list/PostCardHeader.tsx b/packages/shared/src/components/cards/common/list/PostCardHeader.tsx index 73d9a06c22b..716f16f5edb 100644 --- a/packages/shared/src/components/cards/common/list/PostCardHeader.tsx +++ b/packages/shared/src/components/cards/common/list/PostCardHeader.tsx @@ -42,6 +42,10 @@ interface CardHeaderProps { topLabel?: PostMetadataProps['topLabel']; bottomLabel?: PostMetadataProps['bottomLabel']; dateFirst?: PostMetadataProps['dateFirst']; + createdAt?: PostMetadataProps['createdAt']; + dateLabel?: PostMetadataProps['dateLabel']; + numSources?: PostMetadataProps['numSources']; + dateType?: PostMetadataProps['dateType']; }; } @@ -113,11 +117,11 @@ export const PostCardHeader = ({ <> {showCTA && ( } - href={postLink} + href={postLink ?? ''} onClick={onReadArticleClick} openNewTab={openNewTab} /> diff --git a/packages/shared/src/components/cards/common/list/PostMetadata.tsx b/packages/shared/src/components/cards/common/list/PostMetadata.tsx index 242f56020fb..053a1ebb491 100644 --- a/packages/shared/src/components/cards/common/list/PostMetadata.tsx +++ b/packages/shared/src/components/cards/common/list/PostMetadata.tsx @@ -12,6 +12,7 @@ import { TypographyColor, } from '../../../typography/Typography'; import { useScrambler } from '../../../../hooks/useScrambler'; +import { pluralize } from '../../../../lib/strings'; export interface PostMetadataProps { className?: string; @@ -19,6 +20,9 @@ export interface PostMetadataProps { bottomLabel?: ReactElement | string; createdAt?: string; dateFirst?: boolean; + dateLabel?: string; + numSources?: number; + dateType?: TimeFormatType; } export default function PostMetadata({ @@ -27,11 +31,27 @@ export default function PostMetadata({ topLabel, bottomLabel, dateFirst, + dateLabel, + numSources, + dateType = TimeFormatType.Post, }: PostMetadataProps): ReactElement { const { boostedBy } = useFeedCardContext(); const promotedText = useScrambler( boostedBy ? `Promoted by @${boostedBy.username}` : undefined, ); + const hasSources = !!numSources && numSources > 0; + const dateNode = !!createdAt && ( + + ); + const sourcesNode = hasSources && ( + + {numSources} {pluralize('source', numSources)} + + ); return (
@@ -51,9 +71,7 @@ export default function PostMetadata({ {!!boostedBy && !!bottomLabel && } {dateFirst ? ( <> - {!!createdAt && ( - - )} + {dateNode} {!!createdAt && !!bottomLabel && } {bottomLabel} @@ -61,11 +79,13 @@ export default function PostMetadata({ <> {bottomLabel} {(!!bottomLabel || !!boostedBy) && !!createdAt && } - {!!createdAt && ( - - )} + {dateNode} )} + {(!!createdAt || !!bottomLabel || !!boostedBy) && sourcesNode && ( + + )} + {sourcesNode}
); diff --git a/packages/shared/src/components/post/collection/CollectionPostContent.tsx b/packages/shared/src/components/post/collection/CollectionPostContent.tsx index 60c969b443f..728702d0f3b 100644 --- a/packages/shared/src/components/post/collection/CollectionPostContent.tsx +++ b/packages/shared/src/components/post/collection/CollectionPostContent.tsx @@ -23,7 +23,8 @@ import { DateFormat } from '../../utilities'; import { withPostById } from '../withPostById'; import { PostTagList } from '../tags/PostTagList'; import { CollectionPostHeaderActions } from './CollectionPostHeaderActions'; -import type { Post } from '../../../graphql/posts'; +import { isPostUpdated, type Post } from '../../../graphql/posts'; +import { pluralize } from '../../../lib/strings'; type CollectionPostContentRawProps = Omit & { post: Post; @@ -53,7 +54,11 @@ const CollectionPostContentRaw = ({ origin, post, }); - const { updatedAt, contentHtml, image } = post; + const { createdAt, updatedAt, contentHtml, image, numCollectionSources } = + post; + const wasUpdated = isPostUpdated(post); + const dateToShow = wasUpdated ? updatedAt : createdAt; + const hasSources = !!numCollectionSources && numCollectionSources > 0; const { onCopyPostLink, onReadArticle } = engagementActions; const hasNavigation = !!onPreviousPost || !!onNextPost; @@ -130,7 +135,7 @@ const CollectionPostContentRaw = ({ >
-
+
- {!!updatedAt && ( -
- Last updated - + {!!dateToShow && ( +
+ + {hasSources && ( + <> + + + {numCollectionSources}{' '} + {pluralize('source', numCollectionSources)} + + + )}
)} {image && ( diff --git a/packages/shared/src/graphql/posts.ts b/packages/shared/src/graphql/posts.ts index 5871afd723e..96da9429755 100644 --- a/packages/shared/src/graphql/posts.ts +++ b/packages/shared/src/graphql/posts.ts @@ -92,6 +92,10 @@ export const isPostOrSharedPostTwitter = ( ): boolean => isSocialTwitterPost(post) || isSocialTwitterPost(post?.sharedPost as Post); +export const isPostUpdated = ( + post: Pick, +): boolean => !!post.updatedAt && post.updatedAt !== post.createdAt; + /** * For social:twitter quote posts, resolve to the top tweet (the post itself) * rather than the referenced/shared tweet. For all other post types, fall back diff --git a/packages/shared/src/lib/dateFormat.ts b/packages/shared/src/lib/dateFormat.ts index 1c319328e9a..1f73e214458 100644 --- a/packages/shared/src/lib/dateFormat.ts +++ b/packages/shared/src/lib/dateFormat.ts @@ -73,6 +73,7 @@ export const publishTimeLiveTimer: typeof publishTimeRelativeShort = ( export enum TimeFormatType { Post = 'post', + PostUpdated = 'postUpdated', Comment = 'comment', ReadHistory = 'readHistory', TopReaderBadge = 'topReaderBadge', @@ -114,6 +115,20 @@ export function postDateFormat( return date.toLocaleString('en-US', options); } +export function postUpdatedDateFormat( + value: Date | number | string, + now = new Date(), +): string { + const date = new Date(value); + + if (isSameDay(date, now) || isSameDay(date, subDays(now, 1))) { + const relative = publishTimeRelativeShort(value, now); + return relative === 'now' ? relative : `${relative} ago`; + } + + return postDateFormat(value, now); +} + export function commentDateFormat( value: Date | number | string, now = new Date(), @@ -287,6 +302,10 @@ export const formatDate = ({ value, type, now }: FormatDateProps): string => { return postDateFormat(date); } + if (type === TimeFormatType.PostUpdated) { + return postUpdatedDateFormat(date); + } + if (type === TimeFormatType.Comment) { return publishTimeRelativeShort(date); }