From db271332d46101f1efbc93d264c27061dd4edc00 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Mon, 23 Jun 2025 21:45:01 +0800 Subject: [PATCH 01/41] feat: support ad post --- .../src/components/FeedItemComponent.tsx | 162 +++++++++--------- packages/shared/src/hooks/useFeed.ts | 1 + 2 files changed, 84 insertions(+), 79 deletions(-) diff --git a/packages/shared/src/components/FeedItemComponent.tsx b/packages/shared/src/components/FeedItemComponent.tsx index 90cd6ec8b5d..f987b880f49 100644 --- a/packages/shared/src/components/FeedItemComponent.tsx +++ b/packages/shared/src/components/FeedItemComponent.tsx @@ -197,86 +197,90 @@ export default function FeedItemComponent({ postType: (item as PostItem).post?.type, }); - switch (item.type) { - case FeedItemType.Post: { - if ( - !!item.post.pinnedAt && - item.post.source?.currentMember?.flags?.collapsePinnedPosts - ) { - return null; - } - - return ( - { - toggleUpvote({ - payload: post, - origin, - opts: { - columns, - column, - row, - }, - }); - }} - onDownvoteClick={(post, origin = Origin.Feed) => { - toggleDownvote({ - payload: post, - origin, - opts: { - columns, - column, - row, - }, - }); - }} - onPostClick={(post) => onPostClick(post, index, row, column)} - onPostAuxClick={(post) => onPostClick(post, index, row, column, true)} - onReadArticleClick={() => - onReadArticleClick(item.post, index, row, column) - } - onShare={(post) => onShare(post, row, column)} - onBookmarkClick={(post, origin = Origin.Feed) => { - toggleBookmark({ - post, - origin, - opts: { - columns, - column, - row, - }, - }); - }} - openNewTab={openNewTab} - enableMenu={!!user} - onMenuClick={(event) => onMenuClick(event, index, row, column)} - onCopyLinkClick={(event, post) => - onCopyLinkClick(event, post, index, row, column) - } - menuOpened={postMenuIndex === index} - onCommentClick={(post) => onCommentClick(post, index, row, column)} - eagerLoadImage={row === 0 && column === 0} - > - {showCommentPopupId === item.post.id && ( - setShowCommentPopupId(null)} - onSubmit={(content) => - comment({ post: item.post, content, row, column, columns }) - } - loading={isSendingComment} - /> - )} - - ); + if ( + item.type === FeedItemType.Post || + (item.type === FeedItemType.Ad && item.post) + ) { + if ( + !!item.post.pinnedAt && + item.post.source?.currentMember?.flags?.collapsePinnedPosts + ) { + return null; } + + return ( + { + toggleUpvote({ + payload: post, + origin, + opts: { + columns, + column, + row, + }, + }); + }} + onDownvoteClick={(post, origin = Origin.Feed) => { + toggleDownvote({ + payload: post, + origin, + opts: { + columns, + column, + row, + }, + }); + }} + onPostClick={(post) => onPostClick(post, index, row, column)} + onPostAuxClick={(post) => onPostClick(post, index, row, column, true)} + onReadArticleClick={() => + onReadArticleClick(item.post, index, row, column) + } + onShare={(post) => onShare(post, row, column)} + onBookmarkClick={(post, origin = Origin.Feed) => { + toggleBookmark({ + post, + origin, + opts: { + columns, + column, + row, + }, + }); + }} + openNewTab={openNewTab} + enableMenu={!!user} + onMenuClick={(event) => onMenuClick(event, index, row, column)} + onCopyLinkClick={(event, post) => + onCopyLinkClick(event, post, index, row, column) + } + menuOpened={postMenuIndex === index} + onCommentClick={(post) => onCommentClick(post, index, row, column)} + eagerLoadImage={row === 0 && column === 0} + > + {showCommentPopupId === item.post.id && ( + setShowCommentPopupId(null)} + onSubmit={(content) => + comment({ post: item.post, content, row, column, columns }) + } + loading={isSendingComment} + /> + )} + + ); + } + + switch (item.type) { case FeedItemType.Ad: return ( { interface AdItem extends FeedItemBase { ad: Ad; + post?: Post; index: number; updatedAt: number; } From 9ebc4933611433627dc949adea38423a033cf6e3 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Mon, 23 Jun 2025 21:52:24 +0800 Subject: [PATCH 02/41] feat: context to avoid prop drilling --- .../src/components/FeedItemComponent.tsx | 141 +++++++++--------- .../src/features/boost/FeedCardContext.tsx | 12 ++ 2 files changed, 85 insertions(+), 68 deletions(-) create mode 100644 packages/shared/src/features/boost/FeedCardContext.tsx diff --git a/packages/shared/src/components/FeedItemComponent.tsx b/packages/shared/src/components/FeedItemComponent.tsx index f987b880f49..2276f27d8cc 100644 --- a/packages/shared/src/components/FeedItemComponent.tsx +++ b/packages/shared/src/components/FeedItemComponent.tsx @@ -32,6 +32,7 @@ import { CollectionGrid } from './cards/collection'; import type { UseBookmarkPost } from '../hooks/useBookmarkPost'; import { AdActions } from '../lib/ads'; import PlusGrid from './cards/plus/PlusGrid'; +import { FeedCardContext } from '../features/boost/FeedCardContext'; const CommentPopup = dynamic( () => @@ -208,75 +209,79 @@ export default function FeedItemComponent({ return null; } + const isBoostedReach = item.type === FeedItemType.Ad && !!item.post; + return ( - { - toggleUpvote({ - payload: post, - origin, - opts: { - columns, - column, - row, - }, - }); - }} - onDownvoteClick={(post, origin = Origin.Feed) => { - toggleDownvote({ - payload: post, - origin, - opts: { - columns, - column, - row, - }, - }); - }} - onPostClick={(post) => onPostClick(post, index, row, column)} - onPostAuxClick={(post) => onPostClick(post, index, row, column, true)} - onReadArticleClick={() => - onReadArticleClick(item.post, index, row, column) - } - onShare={(post) => onShare(post, row, column)} - onBookmarkClick={(post, origin = Origin.Feed) => { - toggleBookmark({ - post, - origin, - opts: { - columns, - column, - row, - }, - }); - }} - openNewTab={openNewTab} - enableMenu={!!user} - onMenuClick={(event) => onMenuClick(event, index, row, column)} - onCopyLinkClick={(event, post) => - onCopyLinkClick(event, post, index, row, column) - } - menuOpened={postMenuIndex === index} - onCommentClick={(post) => onCommentClick(post, index, row, column)} - eagerLoadImage={row === 0 && column === 0} - > - {showCommentPopupId === item.post.id && ( - setShowCommentPopupId(null)} - onSubmit={(content) => - comment({ post: item.post, content, row, column, columns }) - } - loading={isSendingComment} - /> - )} - + + { + toggleUpvote({ + payload: post, + origin, + opts: { + columns, + column, + row, + }, + }); + }} + onDownvoteClick={(post, origin = Origin.Feed) => { + toggleDownvote({ + payload: post, + origin, + opts: { + columns, + column, + row, + }, + }); + }} + onPostClick={(post) => onPostClick(post, index, row, column)} + onPostAuxClick={(post) => onPostClick(post, index, row, column, true)} + onReadArticleClick={() => + onReadArticleClick(item.post, index, row, column) + } + onShare={(post) => onShare(post, row, column)} + onBookmarkClick={(post, origin = Origin.Feed) => { + toggleBookmark({ + post, + origin, + opts: { + columns, + column, + row, + }, + }); + }} + openNewTab={openNewTab} + enableMenu={!!user} + onMenuClick={(event) => onMenuClick(event, index, row, column)} + onCopyLinkClick={(event, post) => + onCopyLinkClick(event, post, index, row, column) + } + menuOpened={postMenuIndex === index} + onCommentClick={(post) => onCommentClick(post, index, row, column)} + eagerLoadImage={row === 0 && column === 0} + > + {showCommentPopupId === item.post.id && ( + setShowCommentPopupId(null)} + onSubmit={(content) => + comment({ post: item.post, content, row, column, columns }) + } + loading={isSendingComment} + /> + )} + + ); } diff --git a/packages/shared/src/features/boost/FeedCardContext.tsx b/packages/shared/src/features/boost/FeedCardContext.tsx new file mode 100644 index 00000000000..21819a92943 --- /dev/null +++ b/packages/shared/src/features/boost/FeedCardContext.tsx @@ -0,0 +1,12 @@ +import { createContext, useContext } from 'react'; + +interface FeedCardContextData { + isBoostedReach: boolean; +} + +export const FeedCardContext = createContext({ + isBoostedReach: false, +}); + +export const useFeedCardContext = (): FeedCardContextData => + useContext(FeedCardContext); From 2d5561db5f4c9e2c86043be9bbcffa54ca089634 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Mon, 23 Jun 2025 22:09:04 +0800 Subject: [PATCH 03/41] feat: boosted label on card --- .../components/cards/common/PostMetadata.tsx | 4 ++ packages/shared/src/features/boost/utils.ts | 65 +++++++++++++++++++ packages/shared/src/hooks/useFeed.ts | 5 +- 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/components/cards/common/PostMetadata.tsx b/packages/shared/src/components/cards/common/PostMetadata.tsx index 22a1e991dc3..fa28aa9b29d 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, TruncateText, DateFormat } from '../../utilities'; import { largeNumberFormat } from '../../../lib'; +import { useFeedCardContext } from '../../../features/boost/FeedCardContext'; interface PostMetadataProps extends Pick { @@ -28,6 +29,7 @@ export default function PostMetadata({ }: PostMetadataProps): ReactElement { const timeActionContent = isVideoType ? 'watch' : 'read'; const showReadTime = isVideoType ? Number.isInteger(readTime) : !!readTime; + const { isBoostedReach } = useFeedCardContext(); return (
+ {isBoostedReach && Boosted} + {isBoostedReach && } {!!description && ( {description} )} diff --git a/packages/shared/src/features/boost/utils.ts b/packages/shared/src/features/boost/utils.ts index ec97622fa31..463493b07da 100644 --- a/packages/shared/src/features/boost/utils.ts +++ b/packages/shared/src/features/boost/utils.ts @@ -1,5 +1,6 @@ import { startOfDay } from 'date-fns'; import type { differenceInDays } from 'date-fns'; +import type { Post } from '../../graphql/posts'; export const getAbsoluteDifferenceInDays: typeof differenceInDays = ( date1, @@ -14,3 +15,67 @@ export const getAbsoluteDifferenceInDays: typeof differenceInDays = ( // Round down to the nearest whole number since we want full days return Math.floor(diffInDays); }; + +// TODO: delete once integration comes in +export const dummyAdBoostedPost = { + id: '6jpiRprsv', + title: "DevSecOps Isn't Optional Anymore", + image: + 'https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/7a96d7c6f453a82cddae62402ceb7a9b', + readTime: 4, + permalink: 'https://api.local.fylla.dev/r/6jpiRprsv', + commentsPermalink: + 'https://app.local.fylla.dev:5002/posts/devsecops-isn-t-optional-anymore-6jpirprsv', + createdAt: '2021-06-15T00:18:04.000Z', + commented: false, + bookmarked: false, + views: null, + numUpvotes: 0, + numComments: 0, + numAwards: 0, + summary: null, + bookmark: null, + author: null, + type: 'article', + tags: ['security', 'testing', 'devops'], + source: { + id: 'cshub', + handle: 'cshub', + name: 'Cyber Security Hub', + permalink: 'https://app.local.fylla.dev:5002/sources/cshub', + image: 'https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/cshub', + type: 'machine', + membersCount: 0, + description: null, + flags: { + totalUpvotes: 0, + }, + currentMember: null, + }, + userState: { + vote: 0, + flags: { + feedbackDismiss: null, + }, + awarded: false, + }, + slug: 'devsecops-isn-t-optional-anymore-6jpirprsv', + clickbaitTitleDetected: false, + language: 'en', + translation: { + title: null, + titleHtml: null, + smartTitle: null, + summary: null, + }, + sharedPost: null, + trending: null, + feedMeta: null, + collectionSources: [], + numCollectionSources: 0, + updatedAt: '2022-04-18T13:56:02.186Z', + contentHtml: null, + read: false, + upvoted: false, + downvoted: false, +} as unknown as Post; diff --git a/packages/shared/src/hooks/useFeed.ts b/packages/shared/src/hooks/useFeed.ts index 69c1bbea5e2..b7c80d72f83 100644 --- a/packages/shared/src/hooks/useFeed.ts +++ b/packages/shared/src/hooks/useFeed.ts @@ -27,6 +27,7 @@ import { cloudinaryPostImageCoverPlaceholder } from '../lib/image'; import { AD_PLACEHOLDER_SOURCE_ID } from '../lib/constants'; import { SharedFeedPage } from '../components/utilities'; import { useTranslation } from './translation/useTranslation'; +import { dummyAdBoostedPost } from '../features/boost/utils'; interface FeedItemBase { type: T; @@ -257,7 +258,9 @@ export default function useFeed( ad: nextAd, index: adPage, updatedAt: adsUpdatedAt, - }; + // TODO: remove this once integration comes in + post: index === 2 ? dummyAdBoostedPost : undefined, + } as AdItem; }, [ adsData, From 25441c6d331b57647c5851098d3b368fd0ff1e77 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Mon, 23 Jun 2025 23:38:40 +0800 Subject: [PATCH 04/41] feat: ads modal dashboard integration --- .../modals/post/boost/AdsDashboardModal.tsx | 33 ++-- .../src/features/boost/CampaignList.tsx | 14 +- .../src/features/boost/CampaignListItem.tsx | 18 ++- packages/shared/src/graphql/post/boost.ts | 124 +++++++++++++++ .../shared/src/hooks/post/usePostBoost.ts | 149 ++++++++---------- packages/shared/src/lib/query.ts | 1 + 6 files changed, 228 insertions(+), 111 deletions(-) create mode 100644 packages/shared/src/graphql/post/boost.ts diff --git a/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx b/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx index 2f4a373966d..14ea2e6acd2 100644 --- a/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx +++ b/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx @@ -1,18 +1,29 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useMemo } from 'react'; import { Modal } from '../../common/Modal'; import type { ModalProps } from '../../common/Modal'; import { CoreIcon } from '../../../icons'; import { IconSize } from '../../../Icon'; -import type { PostCampaign } from '../../../../hooks/post/usePostBoost'; import { usePostBoost } from '../../../../hooks/post/usePostBoost'; import { DataTile } from '../../../../features/boost/DataTile'; import { BoostHistoryLoading } from '../../../../features/boost/BoostHistoryLoading'; import { CampaignList } from '../../../../features/boost/CampaignList'; +import type { BoostedPostData } from '../../../../graphql/post/boost'; -export function AdsDashboardModal(props: ModalProps): ReactElement { - const { data, isLoading } = usePostBoost(); - const [campaign, setCampaign] = React.useState(null); +interface AdsDashboardModalProps extends ModalProps { + initialBoostedPost?: BoostedPostData; +} + +export function AdsDashboardModal({ + initialBoostedPost, + ...props +}: AdsDashboardModalProps): ReactElement { + const { data, isLoading, stats } = usePostBoost(); + const [boosted, setBoosted] = + React.useState(initialBoostedPost); + const list = useMemo(() => { + return data?.pages.flatMap((page) => page.edges.map((edge) => edge.node)); + }, [data]); return ( - Overview{!campaign && ' all time'} + Overview{!boosted && ' all time'}
} /> - - - + + +
Running ads {isLoading ? ( ) : ( - + )}
diff --git a/packages/shared/src/features/boost/CampaignList.tsx b/packages/shared/src/features/boost/CampaignList.tsx index c4b5396542d..52ec070105d 100644 --- a/packages/shared/src/features/boost/CampaignList.tsx +++ b/packages/shared/src/features/boost/CampaignList.tsx @@ -1,6 +1,5 @@ import type { ReactElement } from 'react'; import React from 'react'; -import type { PostCampaign } from '../../hooks/post/usePostBoost'; import { Typography, TypographyColor, @@ -9,10 +8,11 @@ import { import { IconSize } from '../../components/Icon'; import { BoostIcon } from '../../components/icons/Boost'; import { CampaignListItem } from './CampaignListItem'; +import type { BoostedPostData } from '../../graphql/post/boost'; interface CampaignListProps { - list: PostCampaign[]; - onClick?: (campaign: PostCampaign) => void; + list: BoostedPostData[]; + onClick?: (campaign: BoostedPostData) => void; } export function CampaignList({ @@ -44,11 +44,11 @@ export function CampaignList({ return (
- {list.map((campaign) => ( + {list.map((data) => ( onClick(campaign)} + key={data.campaign.campaignId} + data={data} + onClick={() => onClick(data)} /> ))}
diff --git a/packages/shared/src/features/boost/CampaignListItem.tsx b/packages/shared/src/features/boost/CampaignListItem.tsx index 42142ecd375..48cf480aa54 100644 --- a/packages/shared/src/features/boost/CampaignListItem.tsx +++ b/packages/shared/src/features/boost/CampaignListItem.tsx @@ -9,25 +9,27 @@ import { TypographyColor, TypographyTag, } from '../../components/typography/Typography'; -import type { PostCampaign } from '../../hooks/post/usePostBoost'; import { Image } from '../../components/image/Image'; import { getAbsoluteDifferenceInDays } from './utils'; +import type { BoostedPostData, PromotedPost } from '../../graphql/post/boost'; -const statusToColor: Record = { +const statusToColor: Record = { active: 'bg-action-upvote-active text-action-upvote-default', completed: 'bg-action-share-active text-action-share-default', cancelled: 'bg-action-downvote-active text-action-downvote-default', }; interface CampaignListItemProps { - campaign: PostCampaign; + data: BoostedPostData; onClick: MouseEventHandler; } export function CampaignListItem({ - campaign, + data, onClick, }: CampaignListItemProps): ReactElement { + const { campaign, post } = data; + const getCaption = () => { if (campaign.status === 'completed') { return 'Completed'; @@ -38,7 +40,7 @@ export function CampaignListItem({ } const remainingDays = getAbsoluteDifferenceInDays( - new Date(campaign.boostedUntil), + new Date(campaign.endedAt), new Date(), ); @@ -52,9 +54,9 @@ export function CampaignListItem({ className="flex w-full flex-row items-center gap-4" > - {campaign.image && ( + {post.image && ( - {campaign.title} + {post.title} { + engagements: number; +} + +interface CampaignBoostedPost extends Pick { + image: string; + permalink: string; +} + +export interface BoostedPostData { + campaign: PromotedPost; + post: CampaignBoostedPost; +} + +export interface BoostedPostConnection extends Connection { + stats?: BoostedPostStats; +} + +export const getBoostedPostCampaigns = async ({ + first, + after, +}: RequestQueryParams): Promise => { + const result = await gqlClient.request(BOOSTED_POST_CAMPAIGNS, { + first, + after, + }); + + return result.postCampaigns; +}; + +export const BOOSTED_POST_CAMPAIGN_BY_ID = gql` + query PostCampaignById($id: ID!) { + postCampaignById(id: $id) { + post { + id + title + image + shortId + permalink + } + campaign { + campaignId + postId + status + budget + currentBudget + impressions + clicks + } + } + } +`; + +export const getBoostedPostByCampaignId = async (id: string) => { + const result = await gqlClient.request(BOOSTED_POST_CAMPAIGN_BY_ID, { id }); + + return result.postCampaignById; +}; diff --git a/packages/shared/src/hooks/post/usePostBoost.ts b/packages/shared/src/hooks/post/usePostBoost.ts index 34c971c5f89..34f41867442 100644 --- a/packages/shared/src/hooks/post/usePostBoost.ts +++ b/packages/shared/src/hooks/post/usePostBoost.ts @@ -1,95 +1,74 @@ -import { useQuery } from '@tanstack/react-query'; - -export interface PostCampaign { - id: string; - title: string; - description?: string; - image?: string; // Optional image URL - cost: number; - views: number; - upvotes: number; - comments: number; - link: string; - boostedUntil?: Date; // Optional date when the boost ends - createdAt: Date; - status: 'completed' | 'cancelled' | 'active'; -} - -// interface PostCampaign { -// id: string; -// cost: number; -// boostedUntil?: Date; // Optional date when the boost ends -// createdAt: Date; -// status: 'completed' | 'cancelled' | 'active'; -// post: { -// permalink: string; -// title: string; -// content: string; -// titleHtml: string; -// contentHtml: string; -// image: string; -// numUpvotes: number; -// numComments: number; -// views: number; -// tags: string[]; -// createdAt: Date; -// readTime: number; -// author: { -// id: string; -// name: string; -// image: string; -// }; -// sharedPost: { -// id: string; -// title: string; -// content: string; -// image: string; -// }; -// }; -// } +import type { InfiniteData } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useRef } from 'react'; +import { useAuthContext } from '../../contexts/AuthContext'; +import type { + BoostedPostConnection, + BoostedPostStats, +} from '../../graphql/post/boost'; +import { getBoostedPostCampaigns } from '../../graphql/post/boost'; +import { + generateQueryKey, + RequestKey, + getNextPageParam, + StaleTime, +} from '../../lib/query'; interface UsePostBoost { - data?: PostCampaign[]; + stats: BoostedPostStats; + data?: InfiniteData; isLoading: boolean; } -const dummyData: PostCampaign[] = [ - { - id: '1', - title: 'Boost Your Post', - description: 'Get more visibility for your posts', - cost: 100, - views: 5000, - upvotes: 300, - comments: 50, - link: 'https://example.com/boost-your-post', - boostedUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // Boost lasts for 7 days - status: 'active', - image: 'https://example.com/image1.jpg', // Example image URL - createdAt: new Date(), // Current date - }, - { - id: '2', - title: 'Promote Your Content', - description: 'Reach a wider audience with our promotion tools', - cost: 200, - views: 10000, - upvotes: 600, - comments: 80, - link: 'https://example.com/promote-your-content', - boostedUntil: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), // Boost lasts for 5 days - status: 'completed', - image: 'https://example.com/image2.jpg', // Example image URL - createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), // Created 3 days ago - }, -]; +const FIRST_DEFAULT_VALUE = 20; export const usePostBoost = (): UsePostBoost => { - const { data, isPending, isFetched } = useQuery({ - queryKey: ['postBoost'], - // queryFn: async () => { - initialData: dummyData, + const { user } = useAuthContext(); + const statsRef = useRef({ + totalSpend: 0, + clicks: 0, + impressions: 0, + engagements: 0, + }); + const key = generateQueryKey(RequestKey.PostCampaigns, user, { + first: FIRST_DEFAULT_VALUE, + }); + const { + data: campaigns, + isPending, + isFetched, + } = useInfiniteQuery({ + queryKey: key, + queryFn: async ({ pageParam }) => { + const isFirstRequest = !pageParam; + const response = await getBoostedPostCampaigns({ + first: FIRST_DEFAULT_VALUE, + after: pageParam, + }); + + if (isFirstRequest) { + statsRef.current = response.stats; + } + + return response; + }, + initialPageParam: '', + getNextPageParam: (data, _, lastPageParam) => { + const nextPageparam = getNextPageParam(data?.pageInfo); + + if (lastPageParam === nextPageparam) { + return null; + } + + return getNextPageParam(data?.pageInfo); + }, + enabled: !!user, + staleTime: StaleTime.Default, }); - return { data, isLoading: isPending && !isFetched }; + return { + data: campaigns, + stats: statsRef.current, + isLoading: isPending && !isFetched, + }; }; diff --git a/packages/shared/src/lib/query.ts b/packages/shared/src/lib/query.ts index f76bd1b275d..b178092d1e1 100644 --- a/packages/shared/src/lib/query.ts +++ b/packages/shared/src/lib/query.ts @@ -202,6 +202,7 @@ export enum RequestKey { PriceMetadata = 'price_metadata', Products = 'products', Transactions = 'transactions', + PostCampaigns = 'post_campaigns', CheckCoresRole = 'check_cores_role', Awards = 'awards', PostBoostReach = 'postBoostReach', From 4bd2ddb2b8b33ccf575a8853ba44ab5e78ad4d81 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Tue, 24 Jun 2025 00:03:02 +0800 Subject: [PATCH 05/41] feat: post boost mutations modal --- .../modals/post/boost/BoostPostModal.tsx | 47 ++++------- packages/shared/src/graphql/post/boost.ts | 82 +++++++++++++++++++ .../src/hooks/post/usePostBoostMutations.ts | 71 ++++++++++++++++ 3 files changed, 167 insertions(+), 33 deletions(-) create mode 100644 packages/shared/src/hooks/post/usePostBoostMutations.ts diff --git a/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx b/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx index 3c6226ecc9c..471f0947e02 100644 --- a/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx @@ -1,7 +1,6 @@ import type { ReactElement } from 'react'; import dynamic from 'next/dynamic'; import React, { useState } from 'react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Modal } from '../../common/Modal'; import type { ModalProps } from '../../common/Modal'; import { @@ -14,7 +13,6 @@ import { useAuthContext } from '../../../../contexts/AuthContext'; import { CoreIcon, PlusIcon } from '../../../icons'; import type { Post } from '../../../../graphql/posts'; import { Image } from '../../../image/Image'; -import { generateQueryKey, RequestKey } from '../../../../lib/query'; import useDebounceFn from '../../../../hooks/useDebounceFn'; import { largeNumberFormat } from '../../../../lib'; import { IconSize } from '../../../Icon'; @@ -22,9 +20,8 @@ import { AdsDashboardModal } from './AdsDashboardModal'; import { Origin } from '../../../../lib/log'; import { BuyCoresModal } from '../../award/BuyCoresModal'; import { usePostImage } from '../../../../hooks/post/usePostImage'; -import type { TransactionCreated } from '../../../../graphql/njord'; -import { isNullOrUndefined } from '../../../../lib/func'; import { BoostPostSuccessModal } from './BoostPostSuccessModal'; +import { usePostBoostMutation } from '../../../../hooks/post/usePostBoostMutations'; const Slider = dynamic( () => import('../../../fields/Slider').then((mod) => mod.Slider), @@ -48,7 +45,7 @@ export function BoostPostModal({ post, ...props }: BoostPostModalProps): ReactElement { - const { user, updateUser } = useAuthContext(); + const { user } = useAuthContext(); const [activeScreen, setActiveScreen] = useState(SCREENS.FORM); const [coresPerDay, setCoresPerDay] = React.useState(5000); const [totalDays, setTotalDays] = React.useState(7); @@ -56,36 +53,16 @@ export function BoostPostModal({ coresPerDay, totalDays, }); - const { data } = useQuery<{ min: number; max: number }>({ - queryKey: generateQueryKey( - RequestKey.PostBoostReach, - user, - post.id, - queryProps.coresPerDay, - queryProps.totalDays, - ), - }); - const client = useQueryClient(); const [debounceSet] = useDebounceFn(setQueryProps, 220); const totalSpendInt = coresPerDay * totalDays; const totalSpend = largeNumberFormat(totalSpendInt); - const { mutateAsync: onBoost } = useMutation<{ - startPostBoost: TransactionCreated; - }>({ - onSuccess: (result) => { - setActiveScreen(SCREENS.SUCCESS); - const balance = result?.startPostBoost?.balance; - - if (!isNullOrUndefined(balance)) { - updateUser({ ...user, balance }); - } - - // invalidate wallet queries - client.invalidateQueries({ - queryKey: generateQueryKey(RequestKey.Transactions, user), - exact: false, - }); + const { estimatedReach, onBoostPost } = usePostBoostMutation({ + toEstimate: { + duration: queryProps.totalDays, + budget: queryProps.coresPerDay, + id: post.id, }, + onBoostSuccess: () => setActiveScreen(SCREENS.SUCCESS), }); const image = usePostImage(post); @@ -94,7 +71,11 @@ export function BoostPostModal({ return setActiveScreen(SCREENS.BUY_CORES); } - return onBoost(); + return onBoostPost({ + duration: totalDays, + budget: coresPerDay, + id: post.id, + }); }; if (activeScreen === SCREENS.BUY_CORES) { @@ -174,7 +155,7 @@ export function BoostPostModal({ Total spend - {data?.min ?? 0} - {data?.max ?? 0} + {estimatedReach?.min ?? 0} - {estimatedReach?.max ?? 0} { return result.postCampaignById; }; + +export const BOOST_ESTIMATED_REACH = gql` + query BoostEstimatedReach($postId: ID!, $duration: Int!, $budget: Int!) { + boostEstimatedReach(postId: $postId, duration: $duration, budget: $budget) { + estimatedReach { + min + max + } + } + } +`; + +export interface BoostPostProps { + id: string; + budget: number; + duration: number; +} + +export interface BoostEstimatedReach { + min: number; + max: number; +} + +export const getBoostEstimatedReach = async ({ + id, + budget, + duration, +}: BoostPostProps): Promise => { + const result = await gqlClient.request(BOOST_ESTIMATED_REACH, { + postId: id, + budget, + duration, + }); + + return result.estimatedReach; +}; + +export const START_POST_BOOST = gql` + mutation StartPostBoost($postId: ID!, $duration: Int!, $budget: Int!) { + startPostBoost(postId: $postId, duration: $duration, budget: $budget) { + transactionId + balance { + amount + } + } + } +`; + +export const startPostBoost = async ({ + id, + budget, + duration, +}: BoostPostProps): Promise<{ + transactionId: string; + balance: { amount: number }; +}> => { + const result = await gqlClient.request(START_POST_BOOST, { + postId: id, + budget, + duration, + }); + + return result.startPostBoost; +}; + +export const CANCEL_POST_BOOST = gql` + mutation CancelPostBoost($postId: ID!) { + cancelPostBoost(postId: $postId) { + _ + } + } +`; + +export const cancelPostBoost = async ( + id: string, +): Promise => { + const result = await gqlClient.request(CANCEL_POST_BOOST, { + postId: id, + }); + + return result.cancelPostBoost; +}; diff --git a/packages/shared/src/hooks/post/usePostBoostMutations.ts b/packages/shared/src/hooks/post/usePostBoostMutations.ts new file mode 100644 index 00000000000..29d224d53bb --- /dev/null +++ b/packages/shared/src/hooks/post/usePostBoostMutations.ts @@ -0,0 +1,71 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { + BoostEstimatedReach, + BoostPostProps, +} from '../../graphql/post/boost'; +import { + getBoostEstimatedReach, + startPostBoost, + cancelPostBoost, +} from '../../graphql/post/boost'; +import { generateQueryKey, RequestKey } from '../../lib/query'; +import { useAuthContext } from '../../contexts/AuthContext'; +import { isNullOrUndefined } from '../../lib/func'; + +interface UsePostBoostMutationProps { + toEstimate?: BoostPostProps; + onBoostSuccess?: () => void; + onCancelSuccess?: () => void; +} + +interface UsePostBoostMutation { + estimatedReach: BoostEstimatedReach; + onBoostPost: typeof startPostBoost; + onCancelBoost: typeof cancelPostBoost; +} + +export const usePostBoostMutation = ({ + toEstimate, + onBoostSuccess, + onCancelSuccess, +}: UsePostBoostMutationProps): UsePostBoostMutation => { + const client = useQueryClient(); + const { user, updateUser } = useAuthContext(); + const { data: estimatedReach } = useQuery({ + queryKey: generateQueryKey( + RequestKey.PostCampaigns, + user, + 'estimate', + toEstimate, + ), + queryFn: () => getBoostEstimatedReach(toEstimate), + enabled: !!toEstimate, + }); + + const { mutateAsync: onBoostPost } = useMutation({ + mutationFn: startPostBoost, + onSuccess: (data) => { + if (data.transactionId) { + const balance = data?.balance; + + if (!isNullOrUndefined(balance)) { + updateUser({ ...user, balance }); + } + + client.invalidateQueries({ + queryKey: generateQueryKey(RequestKey.Transactions, user), + exact: false, + }); + + onBoostSuccess?.(); + } + }, + }); + + const { mutateAsync: onCancelBoost } = useMutation({ + mutationFn: cancelPostBoost, + onSuccess: onCancelSuccess, + }); + + return { estimatedReach, onBoostPost, onCancelBoost }; +}; From e314ab846ee92946b2c1f30e656c6b68234a3280 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Tue, 24 Jun 2025 19:28:02 +0800 Subject: [PATCH 06/41] fix: min max default value and max edge case --- .../src/components/modals/post/boost/BoostPostModal.tsx | 5 ++++- packages/shared/src/hooks/post/usePostBoostMutations.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx b/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx index 471f0947e02..6c56c088072 100644 --- a/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx @@ -103,6 +103,9 @@ export function BoostPostModal({ return ; } + // just to avoid any edge case where the min, for some reason is greater than max + const maxReach = Math.max(estimatedReach.min, estimatedReach.max); + return ( - {estimatedReach?.min ?? 0} - {estimatedReach?.max ?? 0} + {estimatedReach.min} - {maxReach} getBoostEstimatedReach(toEstimate), enabled: !!toEstimate, + initialData: { min: 0, max: 0 }, }); const { mutateAsync: onBoostPost } = useMutation({ From 378b8a0d10f3749df819c76418a5a2ffc48a62c7 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Tue, 24 Jun 2025 20:08:11 +0800 Subject: [PATCH 07/41] feat: boosted list view --- .../modals/post/boost/AdsDashboardModal.tsx | 13 +++++- .../post/boost/BoostedPostViewModal.tsx | 37 ++++++++++++++++ .../shared/src/features/boost/BoostStatus.tsx | 0 .../src/features/boost/CampaignListItem.tsx | 25 +++++++---- .../src/features/boost/CampaignListView.tsx | 44 +++++++++---------- packages/shared/src/graphql/post/boost.ts | 1 + 6 files changed, 88 insertions(+), 32 deletions(-) create mode 100644 packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx create mode 100644 packages/shared/src/features/boost/BoostStatus.tsx diff --git a/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx b/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx index 14ea2e6acd2..603ceae7fac 100644 --- a/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx +++ b/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx @@ -9,6 +9,7 @@ import { DataTile } from '../../../../features/boost/DataTile'; import { BoostHistoryLoading } from '../../../../features/boost/BoostHistoryLoading'; import { CampaignList } from '../../../../features/boost/CampaignList'; import type { BoostedPostData } from '../../../../graphql/post/boost'; +import { BoostedPostViewModal } from './BoostedPostViewModal'; interface AdsDashboardModalProps extends ModalProps { initialBoostedPost?: BoostedPostData; @@ -25,6 +26,16 @@ export function AdsDashboardModal({ return data?.pages.flatMap((page) => page.edges.map((edge) => edge.node)); }, [data]); + if (boosted) { + return ( + setBoosted(null)} + /> + ); + } + return ( - Overview{!boosted && ' all time'} + Overview all time
+ + + + Overview + + {capitalize(data.campaign.status)} + + + + + + ); +} diff --git a/packages/shared/src/features/boost/BoostStatus.tsx b/packages/shared/src/features/boost/BoostStatus.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/shared/src/features/boost/CampaignListItem.tsx b/packages/shared/src/features/boost/CampaignListItem.tsx index 48cf480aa54..c5af3cf8505 100644 --- a/packages/shared/src/features/boost/CampaignListItem.tsx +++ b/packages/shared/src/features/boost/CampaignListItem.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames'; -import type { MouseEventHandler, ReactElement } from 'react'; +import type { MouseEventHandler, PropsWithChildren, ReactElement } from 'react'; import React from 'react'; import { IconSize, iconSizeToClassName } from '../../components/Icon'; import { ArrowIcon } from '../../components/icons'; @@ -19,6 +19,21 @@ const statusToColor: Record = { cancelled: 'bg-action-downvote-active text-action-downvote-default', }; +export const BoostStatus = ({ + children, + status, +}: PropsWithChildren<{ + status: PromotedPost['status']; +}>) => ( + + {children} + +); + interface CampaignListItemProps { data: BoostedPostData; onClick: MouseEventHandler; @@ -71,13 +86,7 @@ export function CampaignListItem({ {post.title} - - {getCaption()} - + {getCaption()} ); diff --git a/packages/shared/src/features/boost/CampaignListView.tsx b/packages/shared/src/features/boost/CampaignListView.tsx index ef858c68483..bd144fa23f8 100644 --- a/packages/shared/src/features/boost/CampaignListView.tsx +++ b/packages/shared/src/features/boost/CampaignListView.tsx @@ -5,7 +5,6 @@ import { TypographyColor, TypographyType, } from '../../components/typography/Typography'; -import type { PostCampaign } from '../../hooks/post/usePostBoost'; import { Image } from '../../components/image/Image'; import { Button, @@ -18,34 +17,33 @@ import { DataTile } from './DataTile'; import { BeforeIcon } from '../../components/icons/Before'; import { ProgressBar } from '../../components/fields/ProgressBar'; import { getAbsoluteDifferenceInDays } from './utils'; +import type { BoostedPostData } from '../../graphql/post/boost'; interface CampaignListViewProps { - campaign: PostCampaign; + data: BoostedPostData; } export function CampaignListView({ - campaign, + data, }: CampaignListViewProps): ReactElement { + const { campaign, post } = data; const date = useMemo(() => { + const totalDays = getAbsoluteDifferenceInDays( + campaign.endedAt, + campaign.startedAt, + ); + const getEndsIn = () => { if (campaign.status === 'active') { - return getAbsoluteDifferenceInDays(campaign.boostedUntil, new Date()); + return getAbsoluteDifferenceInDays(campaign.endedAt, new Date()); } - return getAbsoluteDifferenceInDays( - campaign.boostedUntil, - campaign.createdAt, - ); + return totalDays; }; - const totalDays = getAbsoluteDifferenceInDays( - campaign.boostedUntil, - campaign.createdAt, - ); - return { endsIn: getEndsIn(), - startedIn: getAbsoluteDifferenceInDays(new Date(), campaign.createdAt), + startedIn: getAbsoluteDifferenceInDays(new Date(), campaign.startedAt), totalDays, }; }, [campaign]); @@ -58,10 +56,10 @@ export function CampaignListView({ type={TypographyType.Callout} className="line-clamp-3 flex-1" > - {campaign.title} + {post.title} - +
@@ -82,21 +80,21 @@ export function CampaignListView({
} /> - - - + + +
Summary - {campaign.cost} | {date.totalDays}{' '} - days + {campaign.budget} |{' '} + {date.totalDays} days
diff --git a/packages/shared/src/hooks/post/usePostBoostMutations.ts b/packages/shared/src/hooks/post/usePostBoostMutations.ts index 8c3dbc3b55e..cdf159a55ee 100644 --- a/packages/shared/src/hooks/post/usePostBoostMutations.ts +++ b/packages/shared/src/hooks/post/usePostBoostMutations.ts @@ -28,7 +28,7 @@ export const usePostBoostMutation = ({ toEstimate, onBoostSuccess, onCancelSuccess, -}: UsePostBoostMutationProps): UsePostBoostMutation => { +}: UsePostBoostMutationProps = {}): UsePostBoostMutation => { const client = useQueryClient(); const { user, updateUser } = useAuthContext(); const { data: estimatedReach } = useQuery({ From 9738ee733ee5ed05463fcb9b31776c57aa16f398 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 25 Jun 2025 01:50:29 +0800 Subject: [PATCH 11/41] fix: success modal ui --- .../post/boost/BoostPostSuccessModal.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/modals/post/boost/BoostPostSuccessModal.tsx b/packages/shared/src/components/modals/post/boost/BoostPostSuccessModal.tsx index b999a2c32ee..d46257264de 100644 --- a/packages/shared/src/components/modals/post/boost/BoostPostSuccessModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostPostSuccessModal.tsx @@ -10,6 +10,7 @@ import { import type { ModalProps } from '../../common/Modal'; import { Modal } from '../../common/Modal'; import { Image } from '../../../image/Image'; +import { ModalClose } from '../../common/ModalClose'; export function BoostPostSuccessModal({ onBackToDashboard, @@ -23,15 +24,19 @@ export function BoostPostSuccessModal({ size={Modal.Size.Small} isDrawerOnMobile > - - -
- + +
+ + +
+
+ Post boosted successfully! Your post is now being promoted and will start reaching more developers shortly. You can track its performance anytime from the @@ -46,6 +51,14 @@ export function BoostPostSuccessModal({ > Ads dashboard + ); From 20a239bf22a7f178bf027ab15c329f8b597b18b3 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 25 Jun 2025 12:06:09 +0800 Subject: [PATCH 12/41] fix: dependency issue and campaign id --- .../modals/post/boost/AdsDashboardModal.tsx | 18 +++++++++++++----- .../modals/post/boost/BoostPostModal.tsx | 11 ++++------- .../src/components/post/PostHeaderActions.tsx | 2 +- packages/shared/src/graphql/fragments.ts | 4 ++++ packages/shared/src/graphql/posts.ts | 2 +- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx b/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx index 4846c968b82..9070f455c99 100644 --- a/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx +++ b/packages/shared/src/components/modals/post/boost/AdsDashboardModal.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Modal } from '../../common/Modal'; import type { ModalProps } from '../../common/Modal'; import { CoreIcon } from '../../../icons'; @@ -10,9 +10,10 @@ import { BoostHistoryLoading } from '../../../../features/boost/BoostHistoryLoad import { CampaignList } from '../../../../features/boost/CampaignList'; import type { BoostedPostData } from '../../../../graphql/post/boost'; import { BoostedPostViewModal } from './BoostedPostViewModal'; -import { BoostPostModal } from './BoostPostModal'; import usePostById from '../../../../hooks/usePostById'; import type { Post } from '../../../../graphql/posts'; +import { useLazyModal } from '../../../../hooks/useLazyModal'; +import { LazyModal } from '../../common/types'; interface AdsDashboardModalProps extends ModalProps { initialBoostedPost?: BoostedPostData; @@ -22,6 +23,7 @@ export function AdsDashboardModal({ initialBoostedPost, ...props }: AdsDashboardModalProps): ReactElement { + const { openModal } = useLazyModal(); const { data, isLoading, stats } = usePostBoost(); const [toBoost, setToBoost] = useState(); const { post } = usePostById({ id: toBoost }); @@ -30,8 +32,14 @@ export function AdsDashboardModal({ return data?.pages.flatMap((page) => page.edges.map((edge) => edge.node)); }, [data]); - if (post) { - return ; + useEffect(() => { + if (post) { + openModal({ type: LazyModal.BoostPost, props: { post } }); + } + }, [openModal, post]); + + if (toBoost) { + return null; } if (boosted) { @@ -53,7 +61,7 @@ export function AdsDashboardModal({ kind={Modal.Kind.FixedCenter} size={Modal.Size.Small} > - + Overview all time
diff --git a/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx b/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx index 6c56c088072..5ec0ebd2d34 100644 --- a/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx @@ -16,12 +16,13 @@ import { Image } from '../../../image/Image'; import useDebounceFn from '../../../../hooks/useDebounceFn'; import { largeNumberFormat } from '../../../../lib'; import { IconSize } from '../../../Icon'; -import { AdsDashboardModal } from './AdsDashboardModal'; import { Origin } from '../../../../lib/log'; import { BuyCoresModal } from '../../award/BuyCoresModal'; import { usePostImage } from '../../../../hooks/post/usePostImage'; import { BoostPostSuccessModal } from './BoostPostSuccessModal'; import { usePostBoostMutation } from '../../../../hooks/post/usePostBoostMutations'; +import { useLazyModal } from '../../../../hooks/useLazyModal'; +import { LazyModal } from '../../common/types'; const Slider = dynamic( () => import('../../../fields/Slider').then((mod) => mod.Slider), @@ -35,7 +36,6 @@ interface BoostPostModalProps extends ModalProps { const SCREENS = { FORM: 'FORM', BUY_CORES: 'BUY_CORES', - DASHBOARD: 'DASHBOARD', SUCCESS: 'SUCCESS', } as const; @@ -46,6 +46,7 @@ export function BoostPostModal({ ...props }: BoostPostModalProps): ReactElement { const { user } = useAuthContext(); + const { openModal } = useLazyModal(); const [activeScreen, setActiveScreen] = useState(SCREENS.FORM); const [coresPerDay, setCoresPerDay] = React.useState(5000); const [totalDays, setTotalDays] = React.useState(7); @@ -94,15 +95,11 @@ export function BoostPostModal({ return ( setActiveScreen(SCREENS.DASHBOARD)} + onBackToDashboard={() => openModal({ type: LazyModal.AdsDashboard })} /> ); } - if (activeScreen === SCREENS.DASHBOARD) { - return ; - } - // just to avoid any edge case where the min, for some reason is greater than max const maxReach = Math.max(estimatedReach.min, estimatedReach.max); diff --git a/packages/shared/src/components/post/PostHeaderActions.tsx b/packages/shared/src/components/post/PostHeaderActions.tsx index f3ee96a8b03..9706c55fa69 100644 --- a/packages/shared/src/components/post/PostHeaderActions.tsx +++ b/packages/shared/src/components/post/PostHeaderActions.tsx @@ -87,7 +87,7 @@ export function PostHeaderActions({ return ( {!isInternalReadType(post) && !!onReadArticle && } - {!post.flags?.boosted && canBoost && } + {!post.flags?.campaignId && canBoost && } {isCollection && } Date: Wed, 25 Jun 2025 13:01:44 +0800 Subject: [PATCH 13/41] fix: options menu management --- .../shared/src/components/PostOptionsMenu.tsx | 18 ++++++++++++++++-- .../shared/src/components/modals/common.tsx | 8 ++++++++ .../src/components/modals/common/types.ts | 1 + .../modals/post/boost/BoostedPostViewModal.tsx | 6 ++++-- packages/shared/src/graphql/post/boost.ts | 4 +++- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index f3a675d575a..47bba4b201f 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -76,6 +76,7 @@ import { settingsUrl, webappUrl } from '../lib/constants'; import { SharedFeedPage } from './utilities'; import useCustomDefaultFeed from '../hooks/feed/useCustomDefaultFeed'; import { BoostIcon } from './icons/Boost'; +import { getBoostedPostByCampaignId } from '../graphql/post/boost'; const ContextMenu = dynamic( () => import(/* webpackChunkName: "contextMenu" */ './fields/ContextMenu'), @@ -378,11 +379,24 @@ export default function PostOptionsMenu({ }); }; + const onManageBoost = async () => { + try { + const data = await getBoostedPostByCampaignId(post.id); + + openModal({ + type: LazyModal.BoostedPostView, + props: { data }, + }); + } catch (err) { + displayToast('Error fetching campaign'); + } + }; + if (canBoost) { postOptions.push({ - icon: , + icon: , label: 'Boost post', - action: onBoostPost, + action: post?.flags?.campaignId ? onManageBoost : onBoostPost, }); } diff --git a/packages/shared/src/components/modals/common.tsx b/packages/shared/src/components/modals/common.tsx index be0881962b9..4e621a3436d 100644 --- a/packages/shared/src/components/modals/common.tsx +++ b/packages/shared/src/components/modals/common.tsx @@ -289,6 +289,13 @@ const BoostPostModal = dynamic( ), ); +const BoostedPostViewModal = dynamic( + () => + import( + /* webpackChunkName: "boostedPostViewModal" */ './post/boost/BoostedPostViewModal' + ), +); + const OrganizationInviteMemberModal = dynamic(() => import( /* webpackChunkName: "inviteMemberModal" */ '../../features/organizations/components/InviteMemberModal' @@ -349,6 +356,7 @@ export const modals = { [LazyModal.ListAwards]: ListAwardsModal, [LazyModal.AdsDashboard]: AdsDashboardModal, [LazyModal.BoostPost]: BoostPostModal, + [LazyModal.BoostedPostView]: BoostedPostViewModal, [LazyModal.OrganizationInviteMember]: OrganizationInviteMemberModal, [LazyModal.OrganizationManageSeats]: OrganizationManageSeatsModal, }; diff --git a/packages/shared/src/components/modals/common/types.ts b/packages/shared/src/components/modals/common/types.ts index 5d7aa2ed626..eb3576c7195 100644 --- a/packages/shared/src/components/modals/common/types.ts +++ b/packages/shared/src/components/modals/common/types.ts @@ -72,6 +72,7 @@ export enum LazyModal { ListAwards = 'listAwards', AdsDashboard = 'adsDashboard', BoostPost = 'boostPost', + BoostedPostView = 'boostedPostView', OrganizationInviteMember = 'organizationInviteMember', OrganizationManageSeats = 'organizationManageSeats', } diff --git a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx index 6812f995796..be52dbd2f64 100644 --- a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx @@ -11,8 +11,8 @@ import type { Post } from '../../../../graphql/posts'; interface BoostedPostViewModalProps extends ModalProps { data: BoostedPostData; - isLoading: boolean; - onBoostAgain: (id: Post['id']) => void; + isLoading?: boolean; + onBoostAgain?: (id: Post['id']) => void; } export function BoostedPostViewModal({ @@ -55,3 +55,5 @@ export function BoostedPostViewModal({ ); } + +export default BoostedPostViewModal; diff --git a/packages/shared/src/graphql/post/boost.ts b/packages/shared/src/graphql/post/boost.ts index f0922c73a18..fc8da8999d0 100644 --- a/packages/shared/src/graphql/post/boost.ts +++ b/packages/shared/src/graphql/post/boost.ts @@ -118,7 +118,9 @@ export const BOOSTED_POST_CAMPAIGN_BY_ID = gql` } `; -export const getBoostedPostByCampaignId = async (id: string) => { +export const getBoostedPostByCampaignId = async ( + id: string, +): Promise => { const result = await gqlClient.request(BOOSTED_POST_CAMPAIGN_BY_ID, { id }); return result.postCampaignById; From cb243463836b6a3ce356d69923f7104e2749258e Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 25 Jun 2025 20:12:27 +0800 Subject: [PATCH 14/41] fix: missing engagements property --- packages/shared/src/graphql/post/boost.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/shared/src/graphql/post/boost.ts b/packages/shared/src/graphql/post/boost.ts index fc8da8999d0..957753679af 100644 --- a/packages/shared/src/graphql/post/boost.ts +++ b/packages/shared/src/graphql/post/boost.ts @@ -21,6 +21,7 @@ export const BOOSTED_POST_CAMPAIGNS = gql` image shortId permalink + engagements } campaign { campaignId @@ -104,6 +105,7 @@ export const BOOSTED_POST_CAMPAIGN_BY_ID = gql` image shortId permalink + engagements } campaign { campaignId From 68a48070e0452c39927b2f4a5bd641c3677a78ae Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 25 Jun 2025 20:18:46 +0800 Subject: [PATCH 15/41] fix: usage of ref --- .../shared/src/hooks/post/usePostBoost.ts | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/shared/src/hooks/post/usePostBoost.ts b/packages/shared/src/hooks/post/usePostBoost.ts index 34f41867442..246d3e53ae3 100644 --- a/packages/shared/src/hooks/post/usePostBoost.ts +++ b/packages/shared/src/hooks/post/usePostBoost.ts @@ -1,6 +1,5 @@ import type { InfiniteData } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query'; -import { useRef } from 'react'; import { useAuthContext } from '../../contexts/AuthContext'; import type { BoostedPostConnection, @@ -21,15 +20,15 @@ interface UsePostBoost { } const FIRST_DEFAULT_VALUE = 20; +const defaultStats = { + totalSpend: 0, + clicks: 0, + impressions: 0, + engagements: 0, +}; export const usePostBoost = (): UsePostBoost => { const { user } = useAuthContext(); - const statsRef = useRef({ - totalSpend: 0, - clicks: 0, - impressions: 0, - engagements: 0, - }); const key = generateQueryKey(RequestKey.PostCampaigns, user, { first: FIRST_DEFAULT_VALUE, }); @@ -39,19 +38,11 @@ export const usePostBoost = (): UsePostBoost => { isFetched, } = useInfiniteQuery({ queryKey: key, - queryFn: async ({ pageParam }) => { - const isFirstRequest = !pageParam; - const response = await getBoostedPostCampaigns({ + queryFn: ({ pageParam }) => + getBoostedPostCampaigns({ first: FIRST_DEFAULT_VALUE, after: pageParam, - }); - - if (isFirstRequest) { - statsRef.current = response.stats; - } - - return response; - }, + }), initialPageParam: '', getNextPageParam: (data, _, lastPageParam) => { const nextPageparam = getNextPageParam(data?.pageInfo); @@ -68,7 +59,7 @@ export const usePostBoost = (): UsePostBoost => { return { data: campaigns, - stats: statsRef.current, + stats: campaigns?.pages?.[0]?.stats ?? defaultStats, isLoading: isPending && !isFetched, }; }; From b8131655d1dd70a9da90d5c3a18897fdd9c0bd58 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 25 Jun 2025 20:24:14 +0800 Subject: [PATCH 16/41] feat: common hook for when transaction errors --- .../modals/award/GiveAwardModal.tsx | 43 +++---------------- .../src/hooks/post/usePostBoostMutations.ts | 2 + .../shared/src/hooks/useTransactionError.ts | 39 +++++++++++++++++ 3 files changed, 46 insertions(+), 38 deletions(-) create mode 100644 packages/shared/src/hooks/useTransactionError.ts diff --git a/packages/shared/src/components/modals/award/GiveAwardModal.tsx b/packages/shared/src/components/modals/award/GiveAwardModal.tsx index e163fcb961d..7dcaf802025 100644 --- a/packages/shared/src/components/modals/award/GiveAwardModal.tsx +++ b/packages/shared/src/components/modals/award/GiveAwardModal.tsx @@ -27,24 +27,14 @@ import { } from '../../../contexts/GiveAwardModalContext'; import { Justify } from '../../utilities'; import MarkdownInput from '../../fields/MarkdownInput'; -import { useToastNotification, useViewSize, ViewSize } from '../../../hooks'; +import { useViewSize, ViewSize } from '../../../hooks'; import { LazyModal, ModalKind } from '../common/types'; import { IconSize } from '../../Icon'; import { BuyCreditsButton } from '../../credit/BuyCreditsButton'; import { BuyCoresModal } from './BuyCoresModal'; import type { Product } from '../../../graphql/njord'; -import { - award, - getProductsQueryOptions, - UserTransactionStatus, -} from '../../../graphql/njord'; -import { labels, largeNumberFormat } from '../../../lib'; -import type { - ApiErrorResult, - ApiResponseError, - ApiUserTransactionErrorExtension, -} from '../../../graphql/common'; -import { ApiError } from '../../../graphql/common'; +import { award, getProductsQueryOptions } from '../../../graphql/njord'; +import { largeNumberFormat } from '../../../lib'; import { useAuthContext } from '../../../contexts/AuthContext'; import { Origin } from '../../../lib/log'; import type { Post } from '../../../graphql/posts'; @@ -53,6 +43,7 @@ import { formatCoresCurrency } from '../../../lib/utils'; import { useCanPurchaseCores } from '../../../hooks/useCoresFeature'; import { AnimatedAward } from '../../AnimatedAward'; import { useLazyModal } from '../../../hooks/useLazyModal'; +import { useTransactionError } from '../../../hooks/useTransactionError'; const AwardItem = ({ item, @@ -272,7 +263,6 @@ const CommentScreen = () => { const { setActiveStep, type, entity, product, flags, logAwardEvent } = useGiveAwardModalContext(); const isMobile = useViewSize(ViewSize.MobileL); - const { displayToast } = useToastNotification(); const [note, setNote] = useState(''); const { mutate: awardMutation, isPending } = useMutation({ @@ -289,30 +279,7 @@ const CommentScreen = () => { setActiveStep({ screen: AWARD_SCREENS.SUCCESS, product }); }, - onError: async (data: ApiErrorResult) => { - if ( - data.response.errors?.[0]?.extensions?.code === - ApiError.BalanceTransactionError - ) { - const errorExtensions = data.response - .errors[0] as ApiResponseError; - - if ( - errorExtensions.extensions.status === - UserTransactionStatus.InsufficientFunds && - errorExtensions.extensions.balance - ) { - await updateUser({ - ...user, - balance: errorExtensions.extensions.balance, - }); - } - } - - displayToast( - data?.response?.errors?.[0]?.message || labels.error.generic, - ); - }, + onError: useTransactionError(), }); const onAwardClick = useCallback(() => { diff --git a/packages/shared/src/hooks/post/usePostBoostMutations.ts b/packages/shared/src/hooks/post/usePostBoostMutations.ts index cdf159a55ee..1174d6b94bd 100644 --- a/packages/shared/src/hooks/post/usePostBoostMutations.ts +++ b/packages/shared/src/hooks/post/usePostBoostMutations.ts @@ -11,6 +11,7 @@ import { import { generateQueryKey, RequestKey } from '../../lib/query'; import { useAuthContext } from '../../contexts/AuthContext'; import { isNullOrUndefined } from '../../lib/func'; +import { useTransactionError } from '../useTransactionError'; interface UsePostBoostMutationProps { toEstimate?: BoostPostProps; @@ -61,6 +62,7 @@ export const usePostBoostMutation = ({ onBoostSuccess?.(); } }, + onError: useTransactionError(), }); const { mutateAsync: onCancelBoost } = useMutation({ diff --git a/packages/shared/src/hooks/useTransactionError.ts b/packages/shared/src/hooks/useTransactionError.ts new file mode 100644 index 00000000000..285d22e9a77 --- /dev/null +++ b/packages/shared/src/hooks/useTransactionError.ts @@ -0,0 +1,39 @@ +import user from '../../__tests__/fixture/loggedUser'; +import { useAuthContext } from '../contexts/AuthContext'; +import type { + ApiErrorResult, + ApiResponseError, + ApiUserTransactionErrorExtension, +} from '../graphql/common'; +import { ApiError } from '../graphql/common'; +import { UserTransactionStatus } from '../graphql/njord'; +import { labels } from '../lib'; +import { useToastNotification } from './useToastNotification'; + +export const useTransactionError = () => { + const { displayToast } = useToastNotification(); + const { updateUser } = useAuthContext(); + + return async (data: ApiErrorResult) => { + if ( + data.response.errors?.[0]?.extensions?.code === + ApiError.BalanceTransactionError + ) { + const errorExtensions = data.response + .errors[0] as ApiResponseError; + + if ( + errorExtensions.extensions.status === + UserTransactionStatus.InsufficientFunds && + errorExtensions.extensions.balance + ) { + await updateUser({ + ...user, + balance: errorExtensions.extensions.balance, + }); + } + } + + displayToast(data?.response?.errors?.[0]?.message || labels.error.generic); + }; +}; From e5ba7cfb59619ec19370bcb135754e79731a7482 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Fri, 27 Jun 2025 17:32:18 +0800 Subject: [PATCH 17/41] chore: delete unused file --- packages/shared/src/features/boost/BoostStatus.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/shared/src/features/boost/BoostStatus.tsx diff --git a/packages/shared/src/features/boost/BoostStatus.tsx b/packages/shared/src/features/boost/BoostStatus.tsx deleted file mode 100644 index e69de29bb2d..00000000000 From 968ccb93e2180025bb0954892ff7147aa32e92d5 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Fri, 27 Jun 2025 18:22:58 +0800 Subject: [PATCH 18/41] refactor: upper level context wrapiing --- packages/shared/src/components/Feed.tsx | 60 ++++---- .../src/components/FeedItemComponent.tsx | 141 +++++++++--------- 2 files changed, 102 insertions(+), 99 deletions(-) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index 18f8e7a42cc..60ee2135e22 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -48,6 +48,8 @@ import { useFeedContentPreferenceMutationSubscription } from './feeds/useFeedCon import { useFeedBookmarkPost } from '../hooks/bookmark/useFeedBookmarkPost'; import type { AdActions } from '../lib/ads'; import usePlusEntry from '../hooks/usePlusEntry'; +import { FeedCardContext } from '../features/boost/FeedCardContext'; +import { FeedItemType } from './cards/common/common'; const FeedErrorScreen = dynamic( () => import(/* webpackChunkName: "feedErrorScreen" */ './FeedErrorScreen'), @@ -432,33 +434,39 @@ export default function Feed({ ) : ( <> {items.map((item, index) => ( - + value={{ + isBoostedReach: item.type === FeedItemType.Ad && !!item.post, + }} + > + + ))} {!isFetching && !isInitialLoading && !isHorizontal && ( diff --git a/packages/shared/src/components/FeedItemComponent.tsx b/packages/shared/src/components/FeedItemComponent.tsx index 2276f27d8cc..f987b880f49 100644 --- a/packages/shared/src/components/FeedItemComponent.tsx +++ b/packages/shared/src/components/FeedItemComponent.tsx @@ -32,7 +32,6 @@ import { CollectionGrid } from './cards/collection'; import type { UseBookmarkPost } from '../hooks/useBookmarkPost'; import { AdActions } from '../lib/ads'; import PlusGrid from './cards/plus/PlusGrid'; -import { FeedCardContext } from '../features/boost/FeedCardContext'; const CommentPopup = dynamic( () => @@ -209,79 +208,75 @@ export default function FeedItemComponent({ return null; } - const isBoostedReach = item.type === FeedItemType.Ad && !!item.post; - return ( - - { - toggleUpvote({ - payload: post, - origin, - opts: { - columns, - column, - row, - }, - }); - }} - onDownvoteClick={(post, origin = Origin.Feed) => { - toggleDownvote({ - payload: post, - origin, - opts: { - columns, - column, - row, - }, - }); - }} - onPostClick={(post) => onPostClick(post, index, row, column)} - onPostAuxClick={(post) => onPostClick(post, index, row, column, true)} - onReadArticleClick={() => - onReadArticleClick(item.post, index, row, column) - } - onShare={(post) => onShare(post, row, column)} - onBookmarkClick={(post, origin = Origin.Feed) => { - toggleBookmark({ - post, - origin, - opts: { - columns, - column, - row, - }, - }); - }} - openNewTab={openNewTab} - enableMenu={!!user} - onMenuClick={(event) => onMenuClick(event, index, row, column)} - onCopyLinkClick={(event, post) => - onCopyLinkClick(event, post, index, row, column) - } - menuOpened={postMenuIndex === index} - onCommentClick={(post) => onCommentClick(post, index, row, column)} - eagerLoadImage={row === 0 && column === 0} - > - {showCommentPopupId === item.post.id && ( - setShowCommentPopupId(null)} - onSubmit={(content) => - comment({ post: item.post, content, row, column, columns }) - } - loading={isSendingComment} - /> - )} - - + { + toggleUpvote({ + payload: post, + origin, + opts: { + columns, + column, + row, + }, + }); + }} + onDownvoteClick={(post, origin = Origin.Feed) => { + toggleDownvote({ + payload: post, + origin, + opts: { + columns, + column, + row, + }, + }); + }} + onPostClick={(post) => onPostClick(post, index, row, column)} + onPostAuxClick={(post) => onPostClick(post, index, row, column, true)} + onReadArticleClick={() => + onReadArticleClick(item.post, index, row, column) + } + onShare={(post) => onShare(post, row, column)} + onBookmarkClick={(post, origin = Origin.Feed) => { + toggleBookmark({ + post, + origin, + opts: { + columns, + column, + row, + }, + }); + }} + openNewTab={openNewTab} + enableMenu={!!user} + onMenuClick={(event) => onMenuClick(event, index, row, column)} + onCopyLinkClick={(event, post) => + onCopyLinkClick(event, post, index, row, column) + } + menuOpened={postMenuIndex === index} + onCommentClick={(post) => onCommentClick(post, index, row, column)} + eagerLoadImage={row === 0 && column === 0} + > + {showCommentPopupId === item.post.id && ( + setShowCommentPopupId(null)} + onSubmit={(content) => + comment({ post: item.post, content, row, column, columns }) + } + loading={isSendingComment} + /> + )} + ); } From 26f5c71369dd7814941035c0d730f0880c5116b9 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Fri, 27 Jun 2025 19:25:18 +0800 Subject: [PATCH 19/41] refactor: usage of context util --- packages/shared/src/components/Feed.tsx | 10 ++++------ .../src/features/boost/FeedCardContext.tsx | 18 ++++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index 60ee2135e22..7c3b8787459 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -48,8 +48,8 @@ import { useFeedContentPreferenceMutationSubscription } from './feeds/useFeedCon import { useFeedBookmarkPost } from '../hooks/bookmark/useFeedBookmarkPost'; import type { AdActions } from '../lib/ads'; import usePlusEntry from '../hooks/usePlusEntry'; -import { FeedCardContext } from '../features/boost/FeedCardContext'; import { FeedItemType } from './cards/common/common'; +import { FeedCardContextProvider } from '../features/boost/FeedCardContext'; const FeedErrorScreen = dynamic( () => import(/* webpackChunkName: "feedErrorScreen" */ './FeedErrorScreen'), @@ -434,11 +434,9 @@ export default function Feed({ ) : ( <> {items.map((item, index) => ( - ({ onAdAction={onAdAction} onReadArticleClick={onReadArticleClick} /> - + ))} {!isFetching && !isInitialLoading && !isHorizontal && ( diff --git a/packages/shared/src/features/boost/FeedCardContext.tsx b/packages/shared/src/features/boost/FeedCardContext.tsx index 21819a92943..c7b9e40fb1a 100644 --- a/packages/shared/src/features/boost/FeedCardContext.tsx +++ b/packages/shared/src/features/boost/FeedCardContext.tsx @@ -1,12 +1,14 @@ -import { createContext, useContext } from 'react'; +import { createContextProvider } from '@kickass-coderz/react'; +import type { PropsWithChildren } from 'react'; -interface FeedCardContextData { +type FeedCardContextData = { isBoostedReach: boolean; -} +}; -export const FeedCardContext = createContext({ - isBoostedReach: false, -}); +const [FeedCardContextProvider, useFeedCardContext] = createContextProvider( + ({ isBoostedReach }: PropsWithChildren) => ({ + isBoostedReach, + }), +); -export const useFeedCardContext = (): FeedCardContextData => - useContext(FeedCardContext); +export { FeedCardContextProvider, useFeedCardContext }; From 5f65bbb525ebc41ed058b9b3d754887bdeaff7b9 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Fri, 27 Jun 2025 20:27:17 +0800 Subject: [PATCH 20/41] refactor: fetch campaign modals --- .../shared/src/components/modals/common.tsx | 16 +++-- .../src/components/modals/common/types.ts | 1 + .../post/boost/BoostedPostViewModal.tsx | 72 ++++++++++++++++++- .../src/features/boost/CampaignListView.tsx | 45 +++++++++--- .../src/features/posts/PostOptionButton.tsx | 17 ++--- 5 files changed, 122 insertions(+), 29 deletions(-) diff --git a/packages/shared/src/components/modals/common.tsx b/packages/shared/src/components/modals/common.tsx index 4e621a3436d..4dfaaf84e7b 100644 --- a/packages/shared/src/components/modals/common.tsx +++ b/packages/shared/src/components/modals/common.tsx @@ -289,11 +289,16 @@ const BoostPostModal = dynamic( ), ); -const BoostedPostViewModal = dynamic( - () => - import( - /* webpackChunkName: "boostedPostViewModal" */ './post/boost/BoostedPostViewModal' - ), +const BoostedPostViewModal = dynamic(() => + import( + /* webpackChunkName: "boostedPostViewModal" */ './post/boost/BoostedPostViewModal' + ).then((mod) => mod.BoostedPostViewModal), +); + +const FetchBoostedPostViewModal = dynamic(() => + import( + /* webpackChunkName: "fetchBoostedPostViewModal" */ './post/boost/BoostedPostViewModal' + ).then((mod) => mod.FetchBoostedViewModal), ); const OrganizationInviteMemberModal = dynamic(() => @@ -357,6 +362,7 @@ export const modals = { [LazyModal.AdsDashboard]: AdsDashboardModal, [LazyModal.BoostPost]: BoostPostModal, [LazyModal.BoostedPostView]: BoostedPostViewModal, + [LazyModal.FetchBoostedPostView]: FetchBoostedPostViewModal, [LazyModal.OrganizationInviteMember]: OrganizationInviteMemberModal, [LazyModal.OrganizationManageSeats]: OrganizationManageSeatsModal, }; diff --git a/packages/shared/src/components/modals/common/types.ts b/packages/shared/src/components/modals/common/types.ts index eb3576c7195..7b724ed972b 100644 --- a/packages/shared/src/components/modals/common/types.ts +++ b/packages/shared/src/components/modals/common/types.ts @@ -73,6 +73,7 @@ export enum LazyModal { AdsDashboard = 'adsDashboard', BoostPost = 'boostPost', BoostedPostView = 'boostedPostView', + FetchBoostedPostView = 'fetchBoostedPostView', OrganizationInviteMember = 'organizationInviteMember', OrganizationManageSeats = 'organizationManageSeats', } diff --git a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx index be52dbd2f64..29037354ed7 100644 --- a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx @@ -1,13 +1,20 @@ import type { ReactElement } from 'react'; import React from 'react'; +import { useQuery } from '@tanstack/react-query'; import { BoostStatus } from '../../../../features/boost/CampaignListItem'; -import { CampaignListView } from '../../../../features/boost/CampaignListView'; +import { + CampaignListView, + CampaignStatsGrid, +} from '../../../../features/boost/CampaignListView'; import { capitalize } from '../../../../lib/strings'; import type { ModalProps } from '../../common/Modal'; import { Modal } from '../../common/Modal'; import type { BoostedPostData } from '../../../../graphql/post/boost'; +import { getBoostedPostByCampaignId } from '../../../../graphql/post/boost'; import { usePostBoostMutation } from '../../../../hooks/post/usePostBoostMutations'; import type { Post } from '../../../../graphql/posts'; +import { generateQueryKey, RequestKey, StaleTime } from '../../../../lib/query'; +import { useAuthContext } from '../../../../contexts/AuthContext'; interface BoostedPostViewModalProps extends ModalProps { data: BoostedPostData; @@ -56,4 +63,65 @@ export function BoostedPostViewModal({ ); } -export default BoostedPostViewModal; +export function FetchBoostedViewModal({ + campaignId, + ...props +}: Omit & { + campaignId: string; +}): ReactElement { + const { user } = useAuthContext(); + const { data, isLoading } = useQuery({ + queryKey: generateQueryKey(RequestKey.PostCampaigns, user, campaignId), + queryFn: () => getBoostedPostByCampaignId(campaignId), + staleTime: StaleTime.Default, + enabled: !!campaignId && !!user, + }); + + if (isLoading) { + return ( + + +
+ + Fetching data, please hold... + + +
+
+ ); + } + + if (!data) { + return ( + + + + No campaign found + + + + ); + } + + return ; +} diff --git a/packages/shared/src/features/boost/CampaignListView.tsx b/packages/shared/src/features/boost/CampaignListView.tsx index 20e703fde17..dc5482e446c 100644 --- a/packages/shared/src/features/boost/CampaignListView.tsx +++ b/packages/shared/src/features/boost/CampaignListView.tsx @@ -1,5 +1,6 @@ import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; +import classNames from 'classnames'; import { Typography, TypographyColor, @@ -25,6 +26,33 @@ interface CampaignListViewProps { onBoostClick: () => void; } +interface CampaignStatsGridProps { + impressions: number; + engagements: number; + clicks: number; + cores: number; + className?: string; +} + +export const CampaignStatsGrid = ({ + className, + cores, + clicks, + engagements, + impressions, +}: CampaignStatsGridProps) => ( +
+ } + /> + + + +
+); + export function CampaignListView({ data, isLoading, @@ -82,16 +110,13 @@ export function CampaignListView({ Ends in {date.endsIn} days -
- } - /> - - - -
+
diff --git a/packages/shared/src/features/posts/PostOptionButton.tsx b/packages/shared/src/features/posts/PostOptionButton.tsx index f4872a3f530..d8cf36373b6 100644 --- a/packages/shared/src/features/posts/PostOptionButton.tsx +++ b/packages/shared/src/features/posts/PostOptionButton.tsx @@ -97,7 +97,6 @@ import type { PromptOptions } from '../../hooks/usePrompt'; import { usePrompt } from '../../hooks/usePrompt'; import type { PostItem } from '../../hooks/useFeed'; import { BoostIcon } from '../../components/icons/Boost'; -import { getBoostedPostByCampaignId } from '../../graphql/post/boost'; const getBlockLabel = ( name: string, @@ -155,7 +154,7 @@ const PostOptionButtonContent = ({ const isModerator = user?.roles?.includes(Roles.Moderator); const isCustomFeed = feedQueryKey?.[0] === 'custom'; const customFeedId = isCustomFeed ? (feedQueryKey?.[2] as string) : undefined; - const post = loadedPost ?? initialPost; + const post = loadedPost ?? (initialPost as Post); const { isPlus, logSubscriptionEvent } = usePlusSubscription(); const { feedSettings, advancedSettings, checkSettingsEnabledState } = useFeedSettings({ @@ -439,16 +438,10 @@ const PostOptionButtonContent = ({ }; const onManageBoost = async () => { - try { - const data = await getBoostedPostByCampaignId(post.id); - - openModal({ - type: LazyModal.BoostedPostView, - props: { data }, - }); - } catch (err) { - displayToast('Error fetching campaign'); - } + openModal({ + type: LazyModal.FetchBoostedPostView, + props: { campaignId: post.flags.campaignId }, + }); }; if (canBoost) { From f3c0422d9276076d41fe19b80b079d8425f927fe Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Fri, 27 Jun 2025 21:35:25 +0800 Subject: [PATCH 21/41] chore: remove dummy --- packages/shared/src/features/boost/utils.ts | 65 --------------------- packages/shared/src/hooks/useFeed.ts | 2 - 2 files changed, 67 deletions(-) diff --git a/packages/shared/src/features/boost/utils.ts b/packages/shared/src/features/boost/utils.ts index 463493b07da..ec97622fa31 100644 --- a/packages/shared/src/features/boost/utils.ts +++ b/packages/shared/src/features/boost/utils.ts @@ -1,6 +1,5 @@ import { startOfDay } from 'date-fns'; import type { differenceInDays } from 'date-fns'; -import type { Post } from '../../graphql/posts'; export const getAbsoluteDifferenceInDays: typeof differenceInDays = ( date1, @@ -15,67 +14,3 @@ export const getAbsoluteDifferenceInDays: typeof differenceInDays = ( // Round down to the nearest whole number since we want full days return Math.floor(diffInDays); }; - -// TODO: delete once integration comes in -export const dummyAdBoostedPost = { - id: '6jpiRprsv', - title: "DevSecOps Isn't Optional Anymore", - image: - 'https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/7a96d7c6f453a82cddae62402ceb7a9b', - readTime: 4, - permalink: 'https://api.local.fylla.dev/r/6jpiRprsv', - commentsPermalink: - 'https://app.local.fylla.dev:5002/posts/devsecops-isn-t-optional-anymore-6jpirprsv', - createdAt: '2021-06-15T00:18:04.000Z', - commented: false, - bookmarked: false, - views: null, - numUpvotes: 0, - numComments: 0, - numAwards: 0, - summary: null, - bookmark: null, - author: null, - type: 'article', - tags: ['security', 'testing', 'devops'], - source: { - id: 'cshub', - handle: 'cshub', - name: 'Cyber Security Hub', - permalink: 'https://app.local.fylla.dev:5002/sources/cshub', - image: 'https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/cshub', - type: 'machine', - membersCount: 0, - description: null, - flags: { - totalUpvotes: 0, - }, - currentMember: null, - }, - userState: { - vote: 0, - flags: { - feedbackDismiss: null, - }, - awarded: false, - }, - slug: 'devsecops-isn-t-optional-anymore-6jpirprsv', - clickbaitTitleDetected: false, - language: 'en', - translation: { - title: null, - titleHtml: null, - smartTitle: null, - summary: null, - }, - sharedPost: null, - trending: null, - feedMeta: null, - collectionSources: [], - numCollectionSources: 0, - updatedAt: '2022-04-18T13:56:02.186Z', - contentHtml: null, - read: false, - upvoted: false, - downvoted: false, -} as unknown as Post; diff --git a/packages/shared/src/hooks/useFeed.ts b/packages/shared/src/hooks/useFeed.ts index b7c80d72f83..817ea84a3eb 100644 --- a/packages/shared/src/hooks/useFeed.ts +++ b/packages/shared/src/hooks/useFeed.ts @@ -27,7 +27,6 @@ import { cloudinaryPostImageCoverPlaceholder } from '../lib/image'; import { AD_PLACEHOLDER_SOURCE_ID } from '../lib/constants'; import { SharedFeedPage } from '../components/utilities'; import { useTranslation } from './translation/useTranslation'; -import { dummyAdBoostedPost } from '../features/boost/utils'; interface FeedItemBase { type: T; @@ -259,7 +258,6 @@ export default function useFeed( index: adPage, updatedAt: adsUpdatedAt, // TODO: remove this once integration comes in - post: index === 2 ? dummyAdBoostedPost : undefined, } as AdItem; }, [ From 720efbd41f048de670751ba7b69e8fd321f5e3dc Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Mon, 30 Jun 2025 15:21:26 +0800 Subject: [PATCH 22/41] revert: createContextProvider to createContext --- packages/shared/src/components/Feed.tsx | 10 ++++++---- .../shared/src/features/boost/FeedCardContext.tsx | 13 +++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index 7c3b8787459..448b91cf971 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -49,7 +49,7 @@ import { useFeedBookmarkPost } from '../hooks/bookmark/useFeedBookmarkPost'; import type { AdActions } from '../lib/ads'; import usePlusEntry from '../hooks/usePlusEntry'; import { FeedItemType } from './cards/common/common'; -import { FeedCardContextProvider } from '../features/boost/FeedCardContext'; +import { FeedCardContext } from '../features/boost/FeedCardContext'; const FeedErrorScreen = dynamic( () => import(/* webpackChunkName: "feedErrorScreen" */ './FeedErrorScreen'), @@ -434,9 +434,11 @@ export default function Feed({ ) : ( <> {items.map((item, index) => ( - ({ onAdAction={onAdAction} onReadArticleClick={onReadArticleClick} /> - + ))} {!isFetching && !isInitialLoading && !isHorizontal && ( diff --git a/packages/shared/src/features/boost/FeedCardContext.tsx b/packages/shared/src/features/boost/FeedCardContext.tsx index c7b9e40fb1a..890afb87444 100644 --- a/packages/shared/src/features/boost/FeedCardContext.tsx +++ b/packages/shared/src/features/boost/FeedCardContext.tsx @@ -1,14 +1,11 @@ -import { createContextProvider } from '@kickass-coderz/react'; -import type { PropsWithChildren } from 'react'; +import { createContext, useContext } from 'react'; type FeedCardContextData = { isBoostedReach: boolean; }; -const [FeedCardContextProvider, useFeedCardContext] = createContextProvider( - ({ isBoostedReach }: PropsWithChildren) => ({ - isBoostedReach, - }), -); +export const FeedCardContext = createContext({ + isBoostedReach: false, +}); -export { FeedCardContextProvider, useFeedCardContext }; +export const useFeedCardContext = () => useContext(FeedCardContext); From 3885bbbc21d070b6ccd6a7d2aaf21de2f49c592c Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Mon, 30 Jun 2025 16:44:13 +0800 Subject: [PATCH 23/41] refactor: unnecessary check --- packages/shared/src/graphql/posts.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/graphql/posts.ts b/packages/shared/src/graphql/posts.ts index 9debdfb1ff0..16eb21ae67f 100644 --- a/packages/shared/src/graphql/posts.ts +++ b/packages/shared/src/graphql/posts.ts @@ -15,10 +15,7 @@ import { import type { Bookmark, BookmarkFolder } from './bookmarks'; import type { SourcePostModeration } from './squads'; import type { FeaturedAward } from './njord'; -import { - useCanPurchaseCores, - useHasAccessToCores, -} from '../hooks/useCoresFeature'; +import { useCanPurchaseCores } from '../hooks/useCoresFeature'; import { useAuthContext } from '../contexts/AuthContext'; export const ACCEPTED_TYPES = 'image/png,image/jpeg'; @@ -949,9 +946,8 @@ export const checkCanBoostByUser = (post: Post, userId: string) => export const useCanBoostPost = (post: Post) => { const { user } = useAuthContext(); - const hasAccess = useHasAccessToCores(); const canBuy = useCanPurchaseCores(); - const canBoost = hasAccess && canBuy && checkCanBoostByUser(post, user?.id); + const canBoost = canBuy && checkCanBoostByUser(post, user?.id); return { canBoost }; }; From c980dc84b53a2a3278b9ef1619a20224aea6f6d0 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Mon, 30 Jun 2025 21:22:56 +0800 Subject: [PATCH 24/41] fix: estimated reach query --- packages/shared/src/graphql/post/boost.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/graphql/post/boost.ts b/packages/shared/src/graphql/post/boost.ts index 957753679af..448cabf4516 100644 --- a/packages/shared/src/graphql/post/boost.ts +++ b/packages/shared/src/graphql/post/boost.ts @@ -131,10 +131,8 @@ export const getBoostedPostByCampaignId = async ( export const BOOST_ESTIMATED_REACH = gql` query BoostEstimatedReach($postId: ID!, $duration: Int!, $budget: Int!) { boostEstimatedReach(postId: $postId, duration: $duration, budget: $budget) { - estimatedReach { - min - max - } + min + max } } `; @@ -161,7 +159,7 @@ export const getBoostEstimatedReach = async ({ duration, }); - return result.estimatedReach; + return result.boostEstimatedReach; }; export const START_POST_BOOST = gql` From 194928156668b43b4bdf6292d0ec612046762c91 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 01:08:44 +0800 Subject: [PATCH 25/41] fix: icons --- .../src/components/icons/Before/filled.svg | 21 ++++++------------- .../src/components/icons/Before/index.tsx | 3 ++- .../src/components/icons/Before/outlined.svg | 8 +++++++ 3 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 packages/shared/src/components/icons/Before/outlined.svg diff --git a/packages/shared/src/components/icons/Before/filled.svg b/packages/shared/src/components/icons/Before/filled.svg index d735ae44941..b084898869e 100644 --- a/packages/shared/src/components/icons/Before/filled.svg +++ b/packages/shared/src/components/icons/Before/filled.svg @@ -1,17 +1,8 @@ - - - - - - - - - - - - - - - + + + + + + diff --git a/packages/shared/src/components/icons/Before/index.tsx b/packages/shared/src/components/icons/Before/index.tsx index d226e1f5db3..358a402abf0 100644 --- a/packages/shared/src/components/icons/Before/index.tsx +++ b/packages/shared/src/components/icons/Before/index.tsx @@ -3,7 +3,8 @@ import React from 'react'; import type { IconProps } from '../../Icon'; import Icon from '../../Icon'; import FilledIcon from './filled.svg'; +import OutlinedIcon from './outlined.svg'; export const BeforeIcon = (props: IconProps): ReactElement => ( - + ); diff --git a/packages/shared/src/components/icons/Before/outlined.svg b/packages/shared/src/components/icons/Before/outlined.svg new file mode 100644 index 00000000000..d1853b4ebe7 --- /dev/null +++ b/packages/shared/src/components/icons/Before/outlined.svg @@ -0,0 +1,8 @@ + + + + + + + + From c401160e0435d17cd8ed8e94daeba9aa26992a4e Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 01:09:56 +0800 Subject: [PATCH 26/41] fix: status and queries --- .../post/boost/BoostedPostViewModal.tsx | 2 +- .../src/features/boost/CampaignListItem.tsx | 5 ++- .../src/features/boost/CampaignListView.tsx | 31 ++++++++++--------- .../src/features/posts/PostOptionButton.tsx | 2 +- packages/shared/src/graphql/post/boost.ts | 8 +++-- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx index 29037354ed7..ec1dc241a2a 100644 --- a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx @@ -31,7 +31,7 @@ export function BoostedPostViewModal({ const { onCancelBoost } = usePostBoostMutation({}); const handleBoostClick = () => { - if (data.campaign.status === 'active') { + if (data.campaign.status === 'ACTIVE') { return onCancelBoost(data.campaign.campaignId); } diff --git a/packages/shared/src/features/boost/CampaignListItem.tsx b/packages/shared/src/features/boost/CampaignListItem.tsx index c5af3cf8505..3d33373c6ba 100644 --- a/packages/shared/src/features/boost/CampaignListItem.tsx +++ b/packages/shared/src/features/boost/CampaignListItem.tsx @@ -28,7 +28,10 @@ export const BoostStatus = ({ {children} diff --git a/packages/shared/src/features/boost/CampaignListView.tsx b/packages/shared/src/features/boost/CampaignListView.tsx index dc5482e446c..9d9a2cb54dc 100644 --- a/packages/shared/src/features/boost/CampaignListView.tsx +++ b/packages/shared/src/features/boost/CampaignListView.tsx @@ -60,14 +60,13 @@ export function CampaignListView({ }: CampaignListViewProps): ReactElement { const { campaign, post } = data; const date = useMemo(() => { - const totalDays = getAbsoluteDifferenceInDays( - campaign.endedAt, - campaign.startedAt, - ); + const startedAt = new Date(campaign.startedAt); + const endedAt = new Date(campaign.endedAt); + const totalDays = getAbsoluteDifferenceInDays(endedAt, startedAt); const getEndsIn = () => { - if (campaign.status === 'active') { - return getAbsoluteDifferenceInDays(campaign.endedAt, new Date()); + if (campaign.status === 'ACTIVE') { + return getAbsoluteDifferenceInDays(endedAt, new Date()); } return totalDays; @@ -75,14 +74,14 @@ export function CampaignListView({ return { endsIn: getEndsIn(), - startedIn: getAbsoluteDifferenceInDays(new Date(), campaign.startedAt), + startedIn: getAbsoluteDifferenceInDays(new Date(), startedAt), totalDays, }; }, [campaign]); return ( -
-
+
+
- +
- + } + color={campaign.status === 'ACTIVE' && ButtonColor.Ketchup} + icon={campaign.status !== 'ACTIVE' && } onClick={onBoostClick} disabled={isLoading} loading={isLoading} > - {campaign.status === 'active' ? 'Stop campaign' : 'Boost again'} + {campaign.status === 'ACTIVE' ? 'Stop campaign' : 'Boost again'}
); diff --git a/packages/shared/src/features/posts/PostOptionButton.tsx b/packages/shared/src/features/posts/PostOptionButton.tsx index 9dbf8c301fd..2b1eba23dd9 100644 --- a/packages/shared/src/features/posts/PostOptionButton.tsx +++ b/packages/shared/src/features/posts/PostOptionButton.tsx @@ -446,7 +446,7 @@ const PostOptionButtonContent = ({ if (canBoost) { postOptions.push({ icon: , - label: 'Boost post', + label: post?.flags?.campaignId ? 'Manage ad' : 'Boost post', action: post?.flags?.campaignId ? onManageBoost : onBoostPost, }); } diff --git a/packages/shared/src/graphql/post/boost.ts b/packages/shared/src/graphql/post/boost.ts index 448cabf4516..40a0f949c69 100644 --- a/packages/shared/src/graphql/post/boost.ts +++ b/packages/shared/src/graphql/post/boost.ts @@ -31,6 +31,8 @@ export const BOOSTED_POST_CAMPAIGNS = gql` currentBudget impressions clicks + startedAt + endedAt } } } @@ -47,11 +49,11 @@ export const BOOSTED_POST_CAMPAIGNS = gql` export interface PromotedPost { campaignId: string; postId: string; - status: 'completed' | 'cancelled' | 'active'; + status: 'COMPLETED' | 'CANCELED' | 'ACTIVE'; budget: number; currentBudget: number; startedAt: Date; - endedAt?: Date; + endedAt: Date; impressions: number; clicks: number; } @@ -115,6 +117,8 @@ export const BOOSTED_POST_CAMPAIGN_BY_ID = gql` currentBudget impressions clicks + startedAt + endedAt } } } From e30be3b456645c664a6f0c09965e7d778d536f97 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 01:10:45 +0800 Subject: [PATCH 27/41] refactor: feed context to use partial post for future proofing --- packages/shared/src/features/boost/FeedCardContext.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/shared/src/features/boost/FeedCardContext.tsx b/packages/shared/src/features/boost/FeedCardContext.tsx index 890afb87444..b552ddc033c 100644 --- a/packages/shared/src/features/boost/FeedCardContext.tsx +++ b/packages/shared/src/features/boost/FeedCardContext.tsx @@ -1,11 +1,14 @@ import { createContext, useContext } from 'react'; +import type { Post } from '../../graphql/posts'; type FeedCardContextData = { isBoostedReach: boolean; + post: Partial; }; export const FeedCardContext = createContext({ isBoostedReach: false, + post: {}, }); export const useFeedCardContext = () => useContext(FeedCardContext); From 409ebedec905512fc18f51a53a962726e221d67c Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 01:19:18 +0800 Subject: [PATCH 28/41] fix: status mapping --- .../shared/src/features/boost/CampaignListItem.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/features/boost/CampaignListItem.tsx b/packages/shared/src/features/boost/CampaignListItem.tsx index 3d33373c6ba..79bec6f4a25 100644 --- a/packages/shared/src/features/boost/CampaignListItem.tsx +++ b/packages/shared/src/features/boost/CampaignListItem.tsx @@ -14,9 +14,9 @@ import { getAbsoluteDifferenceInDays } from './utils'; import type { BoostedPostData, PromotedPost } from '../../graphql/post/boost'; const statusToColor: Record = { - active: 'bg-action-upvote-active text-action-upvote-default', - completed: 'bg-action-share-active text-action-share-default', - cancelled: 'bg-action-downvote-active text-action-downvote-default', + ACTIVE: 'bg-action-upvote-active text-action-upvote-default', + COMPLETED: 'bg-action-share-active text-action-share-default', + CANCELED: 'bg-action-downvote-active text-action-downvote-default', }; export const BoostStatus = ({ @@ -49,11 +49,11 @@ export function CampaignListItem({ const { campaign, post } = data; const getCaption = () => { - if (campaign.status === 'completed') { + if (campaign.status === 'COMPLETED') { return 'Completed'; } - if (campaign.status === 'cancelled') { + if (campaign.status === 'CANCELED') { return 'Cancelled'; } From 4ca0f6c2cde23b94a806a13cdfa7a38c3ffe9819 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 01:21:45 +0800 Subject: [PATCH 29/41] fix: build --- packages/shared/src/features/boost/FeedCardContext.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/shared/src/features/boost/FeedCardContext.tsx b/packages/shared/src/features/boost/FeedCardContext.tsx index b552ddc033c..c705faf09d9 100644 --- a/packages/shared/src/features/boost/FeedCardContext.tsx +++ b/packages/shared/src/features/boost/FeedCardContext.tsx @@ -3,12 +3,11 @@ import type { Post } from '../../graphql/posts'; type FeedCardContextData = { isBoostedReach: boolean; - post: Partial; + post?: Post; }; export const FeedCardContext = createContext({ isBoostedReach: false, - post: {}, }); export const useFeedCardContext = () => useContext(FeedCardContext); From 790de2e47c54401b46fa17a96b978c168b1fc9f0 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 01:38:05 +0800 Subject: [PATCH 30/41] fix: canceling a post boost --- .../src/components/modals/post/boost/BoostedPostViewModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx index ec1dc241a2a..feb156c9d59 100644 --- a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx @@ -32,7 +32,7 @@ export function BoostedPostViewModal({ const handleBoostClick = () => { if (data.campaign.status === 'ACTIVE') { - return onCancelBoost(data.campaign.campaignId); + return onCancelBoost(data.post.id); } return onBoostAgain(data.post.id); From b5ae7ef8dada6bda60d35f6fdbe573f467d8f6b9 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 01:43:38 +0800 Subject: [PATCH 31/41] fix: canceled process flow --- packages/shared/src/features/boost/CampaignListItem.tsx | 4 ++-- packages/shared/src/graphql/post/boost.ts | 2 +- packages/shared/src/hooks/post/usePostBoostMutations.ts | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/features/boost/CampaignListItem.tsx b/packages/shared/src/features/boost/CampaignListItem.tsx index 79bec6f4a25..efbe8fe8ac1 100644 --- a/packages/shared/src/features/boost/CampaignListItem.tsx +++ b/packages/shared/src/features/boost/CampaignListItem.tsx @@ -16,7 +16,7 @@ import type { BoostedPostData, PromotedPost } from '../../graphql/post/boost'; const statusToColor: Record = { ACTIVE: 'bg-action-upvote-active text-action-upvote-default', COMPLETED: 'bg-action-share-active text-action-share-default', - CANCELED: 'bg-action-downvote-active text-action-downvote-default', + CANCELLED: 'bg-action-downvote-active text-action-downvote-default', }; export const BoostStatus = ({ @@ -53,7 +53,7 @@ export function CampaignListItem({ return 'Completed'; } - if (campaign.status === 'CANCELED') { + if (campaign.status === 'CANCELLED') { return 'Cancelled'; } diff --git a/packages/shared/src/graphql/post/boost.ts b/packages/shared/src/graphql/post/boost.ts index 40a0f949c69..39a0dfa215c 100644 --- a/packages/shared/src/graphql/post/boost.ts +++ b/packages/shared/src/graphql/post/boost.ts @@ -49,7 +49,7 @@ export const BOOSTED_POST_CAMPAIGNS = gql` export interface PromotedPost { campaignId: string; postId: string; - status: 'COMPLETED' | 'CANCELED' | 'ACTIVE'; + status: 'COMPLETED' | 'CANCELLED' | 'ACTIVE'; budget: number; currentBudget: number; startedAt: Date; diff --git a/packages/shared/src/hooks/post/usePostBoostMutations.ts b/packages/shared/src/hooks/post/usePostBoostMutations.ts index 1174d6b94bd..cf2af629cb6 100644 --- a/packages/shared/src/hooks/post/usePostBoostMutations.ts +++ b/packages/shared/src/hooks/post/usePostBoostMutations.ts @@ -67,7 +67,11 @@ export const usePostBoostMutation = ({ const { mutateAsync: onCancelBoost } = useMutation({ mutationFn: cancelPostBoost, - onSuccess: onCancelSuccess, + onSuccess: async () => { + await client.invalidateQueries({ queryKey: [RequestKey.PostCampaigns] }); + + onCancelSuccess?.(); + }, }); return { estimatedReach, onBoostPost, onCancelBoost }; From 77181b0db34423a43631471c1ccba71866e60be5 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 01:45:20 +0800 Subject: [PATCH 32/41] fix: copy --- .../shared/src/components/modals/post/boost/BoostPostModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx b/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx index 5ec0ebd2d34..ef32185d9d9 100644 --- a/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostPostModal.tsx @@ -161,7 +161,7 @@ export function BoostPostModal({ type={TypographyType.Callout} color={TypographyColor.Tertiary} > - Estimated reach + Potential reach
From 23869740b7a4d03f3273d14d91de26663f7e1346 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 02:02:22 +0800 Subject: [PATCH 33/41] fix: stale state management --- .../post/boost/BoostedPostViewModal.tsx | 6 +++-- .../src/hooks/post/usePostBoostMutations.ts | 22 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx index feb156c9d59..51a47a81c2a 100644 --- a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx @@ -28,7 +28,9 @@ export function BoostedPostViewModal({ onBoostAgain, ...props }: BoostedPostViewModalProps): ReactElement { - const { onCancelBoost } = usePostBoostMutation({}); + const { onCancelBoost, isLoadingCancel } = usePostBoostMutation({ + onCancelSuccess: () => props.onRequestClose(null), + }); const handleBoostClick = () => { if (data.campaign.status === 'ACTIVE') { @@ -55,7 +57,7 @@ export function BoostedPostViewModal({ diff --git a/packages/shared/src/hooks/post/usePostBoostMutations.ts b/packages/shared/src/hooks/post/usePostBoostMutations.ts index cf2af629cb6..ee82deb32b8 100644 --- a/packages/shared/src/hooks/post/usePostBoostMutations.ts +++ b/packages/shared/src/hooks/post/usePostBoostMutations.ts @@ -12,6 +12,7 @@ import { generateQueryKey, RequestKey } from '../../lib/query'; import { useAuthContext } from '../../contexts/AuthContext'; import { isNullOrUndefined } from '../../lib/func'; import { useTransactionError } from '../useTransactionError'; +import { useToastNotification } from '../useToastNotification'; interface UsePostBoostMutationProps { toEstimate?: BoostPostProps; @@ -23,6 +24,7 @@ interface UsePostBoostMutation { estimatedReach: BoostEstimatedReach; onBoostPost: typeof startPostBoost; onCancelBoost: typeof cancelPostBoost; + isLoadingCancel: boolean; } export const usePostBoostMutation = ({ @@ -31,8 +33,9 @@ export const usePostBoostMutation = ({ onCancelSuccess, }: UsePostBoostMutationProps = {}): UsePostBoostMutation => { const client = useQueryClient(); + const { displayToast } = useToastNotification(); const { user, updateUser } = useAuthContext(); - const { data: estimatedReach } = useQuery({ + const { data: estimatedReach, isPending } = useQuery({ queryKey: generateQueryKey( RequestKey.PostCampaigns, user, @@ -58,6 +61,10 @@ export const usePostBoostMutation = ({ queryKey: generateQueryKey(RequestKey.Transactions, user), exact: false, }); + client.invalidateQueries({ + queryKey: generateQueryKey(RequestKey.PostCampaigns, user), + exact: false, + }); onBoostSuccess?.(); } @@ -68,11 +75,20 @@ export const usePostBoostMutation = ({ const { mutateAsync: onCancelBoost } = useMutation({ mutationFn: cancelPostBoost, onSuccess: async () => { - await client.invalidateQueries({ queryKey: [RequestKey.PostCampaigns] }); + await client.invalidateQueries({ + queryKey: generateQueryKey(RequestKey.PostCampaigns, user), + exact: false, + }); + displayToast('Post boost canceled!'); onCancelSuccess?.(); }, }); - return { estimatedReach, onBoostPost, onCancelBoost }; + return { + estimatedReach, + onBoostPost, + onCancelBoost, + isLoadingCancel: isPending, + }; }; From 1ff2b10a1f73f10cc26516227df64901f456d226 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 02:12:53 +0800 Subject: [PATCH 34/41] fix: button flicker --- packages/shared/src/components/post/PostHeaderActions.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/components/post/PostHeaderActions.tsx b/packages/shared/src/components/post/PostHeaderActions.tsx index 3417f762cb3..7edd3b98d6a 100644 --- a/packages/shared/src/components/post/PostHeaderActions.tsx +++ b/packages/shared/src/components/post/PostHeaderActions.tsx @@ -1,6 +1,7 @@ import type { ReactElement } from 'react'; import React, { useCallback, useContext } from 'react'; import classNames from 'classnames'; +import { useQuery } from '@tanstack/react-query'; import { OpenLinkIcon } from '../icons'; import { getReadPostButtonText, @@ -18,6 +19,7 @@ import { CollectionSubscribeButton } from './collection/CollectionSubscribeButto import { useViewSizeClient, ViewSize } from '../../hooks'; import { BoostPostButton } from '../../features/boost/BoostPostButton'; import { Tooltip } from '../tooltip/Tooltip'; +import { getPostByIdKey } from '../../lib/query'; const Container = classed('div', 'flex flex-row items-center'); @@ -31,6 +33,7 @@ export function PostHeaderActions({ isFixedNavigation, ...props }: PostHeaderActionsProps): ReactElement { + const { isSuccess } = useQuery({ queryKey: getPostByIdKey(post?.id) }); const { openNewTab } = useContext(SettingsContext); const isLaptop = useViewSizeClient(ViewSize.Laptop); const isMobile = useViewSizeClient(ViewSize.MobileXL); @@ -73,7 +76,9 @@ export function PostHeaderActions({ return ( {!isInternalReadType(post) && !!onReadArticle && } - {!post.flags?.campaignId && canBoost && } + {!post.flags?.campaignId && canBoost && isSuccess && ( + + )} {isCollection && } Date: Wed, 2 Jul 2025 02:15:11 +0800 Subject: [PATCH 35/41] fix: loading state --- .../post/boost/BoostedPostViewModal.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx index 51a47a81c2a..c453201ae26 100644 --- a/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx +++ b/packages/shared/src/components/modals/post/boost/BoostedPostViewModal.tsx @@ -88,7 +88,7 @@ export function FetchBoostedViewModal({ size={Modal.Size.Small} > -
+
Fetching data, please hold... @@ -112,15 +112,17 @@ export function FetchBoostedViewModal({ size={Modal.Size.Small} > - - No campaign found - - +
+ + No campaign found + + +
); } From d5759538bfd1e481fe713763c505eac78f6ceeb0 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 03:39:32 +0800 Subject: [PATCH 36/41] fix: test --- packages/shared/src/hooks/useScrollRestoration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/hooks/useScrollRestoration.ts b/packages/shared/src/hooks/useScrollRestoration.ts index 0b8b5dd5f37..76260f5199a 100644 --- a/packages/shared/src/hooks/useScrollRestoration.ts +++ b/packages/shared/src/hooks/useScrollRestoration.ts @@ -22,7 +22,7 @@ export const useScrollRestoration = (): void => { useEffect(() => { const scrollPosition = scrollPositions[pathname] || 0; - window.scrollTo(0, scrollPosition); + globalThis?.window?.scrollTo(0, scrollPosition); }, [pathname]); }; From c975da2c06412fcbe13212f7372568f84a3c5861 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 03:40:32 +0800 Subject: [PATCH 37/41] Revert "fix: test" This reverts commit d5759538bfd1e481fe713763c505eac78f6ceeb0. --- packages/shared/src/hooks/useScrollRestoration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/hooks/useScrollRestoration.ts b/packages/shared/src/hooks/useScrollRestoration.ts index 76260f5199a..0b8b5dd5f37 100644 --- a/packages/shared/src/hooks/useScrollRestoration.ts +++ b/packages/shared/src/hooks/useScrollRestoration.ts @@ -22,7 +22,7 @@ export const useScrollRestoration = (): void => { useEffect(() => { const scrollPosition = scrollPositions[pathname] || 0; - globalThis?.window?.scrollTo(0, scrollPosition); + window.scrollTo(0, scrollPosition); }, [pathname]); }; From fb5631efdd9098caa34d9be0801da5db5ece56d0 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 16:43:51 +0800 Subject: [PATCH 38/41] fix: tests --- .../shared/src/components/post/PostHeaderActions.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/post/PostHeaderActions.tsx b/packages/shared/src/components/post/PostHeaderActions.tsx index 7edd3b98d6a..4d6aff71df5 100644 --- a/packages/shared/src/components/post/PostHeaderActions.tsx +++ b/packages/shared/src/components/post/PostHeaderActions.tsx @@ -1,7 +1,7 @@ import type { ReactElement } from 'react'; import React, { useCallback, useContext } from 'react'; import classNames from 'classnames'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { OpenLinkIcon } from '../icons'; import { getReadPostButtonText, @@ -33,7 +33,13 @@ export function PostHeaderActions({ isFixedNavigation, ...props }: PostHeaderActionsProps): ReactElement { - const { isSuccess } = useQuery({ queryKey: getPostByIdKey(post?.id) }); + const key = getPostByIdKey(post?.id); + const client = useQueryClient(); + const { isSuccess } = useQuery({ + queryKey: key, + queryFn: () => client.getQueryData(key), + staleTime: Infinity, + }); const { openNewTab } = useContext(SettingsContext); const isLaptop = useViewSizeClient(ViewSize.Laptop); const isMobile = useViewSizeClient(ViewSize.MobileXL); From 975342ec0d4d99abf357e68a91d933cd9106ed1c Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 20:38:21 +0800 Subject: [PATCH 39/41] fix: pr comments --- packages/shared/src/components/Feed.tsx | 2 +- packages/shared/src/components/cards/common/PostMetadata.tsx | 2 +- .../shared/src/features/{boost => posts}/FeedCardContext.tsx | 0 packages/shared/src/hooks/useFeed.ts | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) rename packages/shared/src/features/{boost => posts}/FeedCardContext.tsx (100%) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index 448b91cf971..2299755b0b4 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -49,7 +49,7 @@ import { useFeedBookmarkPost } from '../hooks/bookmark/useFeedBookmarkPost'; import type { AdActions } from '../lib/ads'; import usePlusEntry from '../hooks/usePlusEntry'; import { FeedItemType } from './cards/common/common'; -import { FeedCardContext } from '../features/boost/FeedCardContext'; +import { FeedCardContext } from '../features/posts/FeedCardContext'; const FeedErrorScreen = dynamic( () => import(/* webpackChunkName: "feedErrorScreen" */ './FeedErrorScreen'), diff --git a/packages/shared/src/components/cards/common/PostMetadata.tsx b/packages/shared/src/components/cards/common/PostMetadata.tsx index fa28aa9b29d..f0798a51945 100644 --- a/packages/shared/src/components/cards/common/PostMetadata.tsx +++ b/packages/shared/src/components/cards/common/PostMetadata.tsx @@ -6,7 +6,7 @@ import { Separator } from './common'; import type { Post } from '../../../graphql/posts'; import { formatReadTime, TruncateText, DateFormat } from '../../utilities'; import { largeNumberFormat } from '../../../lib'; -import { useFeedCardContext } from '../../../features/boost/FeedCardContext'; +import { useFeedCardContext } from '../../../features/posts/FeedCardContext'; interface PostMetadataProps extends Pick { diff --git a/packages/shared/src/features/boost/FeedCardContext.tsx b/packages/shared/src/features/posts/FeedCardContext.tsx similarity index 100% rename from packages/shared/src/features/boost/FeedCardContext.tsx rename to packages/shared/src/features/posts/FeedCardContext.tsx diff --git a/packages/shared/src/hooks/useFeed.ts b/packages/shared/src/hooks/useFeed.ts index 817ea84a3eb..63f00169d1a 100644 --- a/packages/shared/src/hooks/useFeed.ts +++ b/packages/shared/src/hooks/useFeed.ts @@ -257,7 +257,6 @@ export default function useFeed( ad: nextAd, index: adPage, updatedAt: adsUpdatedAt, - // TODO: remove this once integration comes in } as AdItem; }, [ From 54849ab26a3e0a157276bc0d8ab1349db3439f06 Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 21:35:23 +0800 Subject: [PATCH 40/41] refactor: unused property --- packages/shared/src/features/posts/FeedCardContext.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/features/posts/FeedCardContext.tsx b/packages/shared/src/features/posts/FeedCardContext.tsx index c705faf09d9..92c41cffac8 100644 --- a/packages/shared/src/features/posts/FeedCardContext.tsx +++ b/packages/shared/src/features/posts/FeedCardContext.tsx @@ -1,10 +1,8 @@ import { createContext, useContext } from 'react'; -import type { Post } from '../../graphql/posts'; -type FeedCardContextData = { +interface FeedCardContextData { isBoostedReach: boolean; - post?: Post; -}; +} export const FeedCardContext = createContext({ isBoostedReach: false, From 803cac7797b904f264142b85d2f7269f7d7734ad Mon Sep 17 00:00:00 2001 From: Lee Hansel Solevilla Date: Wed, 2 Jul 2025 21:40:55 +0800 Subject: [PATCH 41/41] refactor: better naming --- packages/shared/src/components/Feed.tsx | 2 +- .../shared/src/components/cards/common/PostMetadata.tsx | 6 +++--- packages/shared/src/features/posts/FeedCardContext.tsx | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index 2299755b0b4..17533799383 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -437,7 +437,7 @@ export default function Feed({ - {isBoostedReach && Boosted} - {isBoostedReach && } + {isBoostedAdPost && Boosted} + {isBoostedAdPost && } {!!description && ( {description} )} diff --git a/packages/shared/src/features/posts/FeedCardContext.tsx b/packages/shared/src/features/posts/FeedCardContext.tsx index 92c41cffac8..d06dae26a69 100644 --- a/packages/shared/src/features/posts/FeedCardContext.tsx +++ b/packages/shared/src/features/posts/FeedCardContext.tsx @@ -1,11 +1,12 @@ import { createContext, useContext } from 'react'; interface FeedCardContextData { - isBoostedReach: boolean; + // a boosted post can surface organically, and we want to show the boosted label only if the post surfaced as an ad + isBoostedAdPost: boolean; } export const FeedCardContext = createContext({ - isBoostedReach: false, + isBoostedAdPost: false, }); export const useFeedCardContext = () => useContext(FeedCardContext);