diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index f3c2c4de8e..43cecb993c 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -64,6 +64,7 @@ import { getProductsQueryOptions } from '../graphql/njord'; import { useUpdateQuery } from '../hooks/useUpdateQuery'; import { BriefBannerFeed } from './cards/brief/BriefBanner/BriefBannerFeed'; import { ActionType } from '../graphql/actions'; +import { useProfileCompletionIndicator } from '../hooks/profile/useProfileCompletionIndicator'; const FeedErrorScreen = dynamic( () => import(/* webpackChunkName: "feedErrorScreen" */ './FeedErrorScreen'), @@ -204,6 +205,8 @@ export default function Feed({ }); const showBriefCard = isMyFeed && briefCardFeatureValue && hasNoBriefAction; const [getProducts] = useUpdateQuery(getProductsQueryOptions()); + const { showIndicator: showProfileCompletion } = + useProfileCompletionIndicator(); const { value: briefBannerPage } = useConditionalFeature({ feature: briefFeedEntrypointPage, @@ -239,6 +242,7 @@ export default function Feed({ disableAds, adPostLength: isSquadFeed ? 2 : undefined, showAcquisitionForm, + showProfileCompletion, ...(showMarketingCta && { marketingCta }), ...(plusEntryFeed && { plusEntry: plusEntryFeed }), feedName, diff --git a/packages/shared/src/components/FeedItemComponent.tsx b/packages/shared/src/components/FeedItemComponent.tsx index ee3baa8292..713abf8dde 100644 --- a/packages/shared/src/components/FeedItemComponent.tsx +++ b/packages/shared/src/components/FeedItemComponent.tsx @@ -45,6 +45,7 @@ import { MarketingCtaBriefing } from './marketingCta/MarketingCtaBriefing'; import { MarketingCtaYearInReview } from './marketingCta/MarketingCtaYearInReview'; import PollGrid from './cards/poll/PollGrid'; import { PollList } from './cards/poll/PollList'; +import { ProfileCompletionGrid } from './cards/profileCompletion/ProfileCompletionGrid'; export type FeedItemComponentProps = { item: FeedItem; @@ -141,6 +142,7 @@ const getTags = ({ AcquisitionFormTag: useListCards ? AcquisitionFormList : AcquisitionFormGrid, + ProfileCompletionTag: ProfileCompletionGrid, }; }; @@ -242,6 +244,7 @@ function FeedItemComponent({ MarketingCtaTag, PlusGridTag, AcquisitionFormTag, + ProfileCompletionTag, } = getTags({ isListFeedLayout: shouldUseListFeedLayout, shouldUseListMode, @@ -379,6 +382,8 @@ function FeedItemComponent({ ); case FeedItemType.PlusEntry: return ; + case FeedItemType.ProfileCompletion: + return ; default: return ; } diff --git a/packages/shared/src/components/cards/common/common.tsx b/packages/shared/src/components/cards/common/common.tsx index 3c134cb5fc..67f322a99c 100644 --- a/packages/shared/src/components/cards/common/common.tsx +++ b/packages/shared/src/components/cards/common/common.tsx @@ -79,4 +79,5 @@ export enum FeedItemType { Placeholder = 'placeholder', UserAcquisition = 'userAcquisition', MarketingCta = 'marketingCta', + ProfileCompletion = 'profileCompletion', } diff --git a/packages/shared/src/components/cards/profileCompletion/ProfileCompletionGrid.tsx b/packages/shared/src/components/cards/profileCompletion/ProfileCompletionGrid.tsx new file mode 100644 index 0000000000..34ad0b1033 --- /dev/null +++ b/packages/shared/src/components/cards/profileCompletion/ProfileCompletionGrid.tsx @@ -0,0 +1,127 @@ +import type { ReactElement } from 'react'; +import React, { useMemo } from 'react'; +import ProgressCircle from '../../ProgressCircle'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../../typography/Typography'; +import type { ProfileCompletion as ProfileCompletionData } from '../../../lib/user'; +import { webappUrl } from '../../../lib/constants'; +import { useAuthContext } from '../../../contexts/AuthContext'; +import { Button, ButtonSize, ButtonVariant } from '../../buttons/Button'; + +type CompletionItem = { + label: string; + completed: boolean; + redirectPath: string; +}; + +const getCompletionItems = ( + completion: ProfileCompletionData, +): CompletionItem[] => { + return [ + { + label: 'Profile image', + completed: completion.hasProfileImage, + redirectPath: `${webappUrl}settings/profile`, + }, + { + label: 'Headline', + completed: completion.hasHeadline, + redirectPath: `${webappUrl}settings/profile?field=bio`, + }, + { + label: 'Experience level', + completed: completion.hasExperienceLevel, + redirectPath: `${webappUrl}settings/profile?field=experienceLevel`, + }, + { + label: 'Work experience', + completed: completion.hasWork, + redirectPath: `${webappUrl}settings/profile/experience/work`, + }, + { + label: 'Education', + completed: completion.hasEducation, + redirectPath: `${webappUrl}settings/profile/experience/education`, + }, + ]; +}; + +const formatNextStep = (incompleteItems: CompletionItem[]): string => { + if (incompleteItems.length === 0) { + return 'Your profile is complete!'; + } + + const labels = incompleteItems.map((item) => item.label.toLowerCase()); + const formattedList = + labels.length === 1 + ? labels[0] + : `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`; + + return `Add ${formattedList}.`; +}; + +export const ProfileCompletionGrid = (): ReactElement | null => { + const { user } = useAuthContext(); + const profileCompletion = user?.profileCompletion; + + const items = useMemo( + () => (profileCompletion ? getCompletionItems(profileCompletion) : []), + [profileCompletion], + ); + + const incompleteItems = useMemo( + () => items.filter((item) => !item.completed), + [items], + ); + + const nextStep = useMemo( + () => formatNextStep(incompleteItems), + [incompleteItems], + ); + + const firstIncompleteItem = incompleteItems[0]; + const redirectPath = firstIncompleteItem?.redirectPath; + + const progress = profileCompletion?.percentage ?? 0; + + if (!profileCompletion || !redirectPath) { + return null; + } + + return ( +
+ + + Profile completion + + + {nextStep} + + +
+ ); +}; diff --git a/packages/shared/src/hooks/useFeed.ts b/packages/shared/src/hooks/useFeed.ts index e82c76dca9..590a2031c1 100644 --- a/packages/shared/src/hooks/useFeed.ts +++ b/packages/shared/src/hooks/useFeed.ts @@ -69,6 +69,7 @@ export type FeedItem = | MarketingCtaItem | FeedItemBase | FeedItemBase + | FeedItemBase | PlusEntryItem; export const isBoostedPostAd = (item: FeedItem): item is AdPostItem => @@ -98,6 +99,7 @@ type UseFeedSettingParams = { adPostLength?: number; disableAds?: boolean; showAcquisitionForm?: boolean; + showProfileCompletion?: boolean; marketingCta?: MarketingCta; plusEntry?: MarketingCta; feedName?: string; @@ -325,6 +327,8 @@ export default function useFeed( }); } else if (withFirstIndex(settings.showAcquisitionForm)) { acc.push({ type: FeedItemType.UserAcquisition }); + } else if (withFirstIndex(settings.showProfileCompletion)) { + acc.push({ type: FeedItemType.ProfileCompletion }); } else { acc.push(adItem); } @@ -371,6 +375,7 @@ export default function useFeed( feedQuery.dataUpdatedAt, settings.marketingCta, settings.showAcquisitionForm, + settings.showProfileCompletion, placeholdersPerPage, getAd, settings.plusEntry,