-
Notifications
You must be signed in to change notification settings - Fork 290
feat: profile completion card #5254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+314
−4
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
2630ff8
feat: add profile completion card to feed with priority over brief card
AmarTrebinjac 98cfe73
refactor: improve profile completion card styling and CTAs
AmarTrebinjac 098388b
refactor: improve profile completion card with dismiss and all remain…
AmarTrebinjac 5746845
fix: use TypographyTag.Span enum instead of string literal
AmarTrebinjac 1069243
fix: prevent brief card from showing when profile card is dismissed
AmarTrebinjac 1b6d860
refactor: restore brand purple color scheme for profile completion card
AmarTrebinjac 1fba15f
Merge branch 'main' into profile-card-v2
AmarTrebinjac f166df5
Merge branch 'main' into profile-card-v2
AmarTrebinjac e9c9da1
feat: add feature flag for profile completion card
AmarTrebinjac 772acdb
fix: prevent experiment double-exposure and layout shift for profile …
AmarTrebinjac f2e084f
feat: add analytics logging to profile completion card
AmarTrebinjac 10a5b49
fix: use LogEvent.Click for dismiss action
AmarTrebinjac e7b7527
chore: remove comments from profile completion card
AmarTrebinjac 9c4901c
chore: remove remaining comments from profile card feature
AmarTrebinjac 6dd2064
Merge branch 'main' into profile-card-v2
AmarTrebinjac 17812e8
refactor: move profile completion card styles to custom.ts
AmarTrebinjac 8fc61c4
Merge branch 'main' into profile-card-v2
AmarTrebinjac da51aa7
Merge branch 'main' into profile-card-v2
AmarTrebinjac b191663
Merge branch 'main' into profile-card-v2
AmarTrebinjac File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
217 changes: 217 additions & 0 deletions
217
packages/shared/src/components/cards/ProfileCompletionCard.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| import React, { useCallback, useEffect, useMemo, useRef } from 'react'; | ||
| import type { ReactElement } from 'react'; | ||
| import classNames from 'classnames'; | ||
| import { | ||
| Typography, | ||
| TypographyColor, | ||
| TypographyTag, | ||
| TypographyType, | ||
| } from '../typography/Typography'; | ||
| import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; | ||
| import ProgressCircle from '../ProgressCircle'; | ||
| import CloseButton from '../CloseButton'; | ||
| import { useAuthContext } from '../../contexts/AuthContext'; | ||
| import { useLogContext } from '../../contexts/LogContext'; | ||
| import { useActions } from '../../hooks'; | ||
| import { ActionType } from '../../graphql/actions'; | ||
| import type { ProfileCompletion } from '../../lib/user'; | ||
| import { webappUrl } from '../../lib/constants'; | ||
| import { LogEvent, TargetType } from '../../lib/log'; | ||
| import { | ||
| profileCompletionCardBorder, | ||
| profileCompletionCardBg, | ||
| profileCompletionButtonBg, | ||
| } from '../../styles/custom'; | ||
|
|
||
| type CompletionItem = { | ||
| label: string; | ||
| completed: boolean; | ||
| redirectPath: string; | ||
| cta: string; | ||
| benefit: string; | ||
| }; | ||
|
|
||
| type ProfileCompletionCardProps = { | ||
| className?: Partial<{ | ||
| container: string; | ||
| card: string; | ||
| }>; | ||
| }; | ||
|
|
||
| const getCompletionItems = ( | ||
| completion: ProfileCompletion, | ||
| ): CompletionItem[] => { | ||
| return [ | ||
| { | ||
| label: 'Profile image', | ||
| completed: completion.hasProfileImage, | ||
| redirectPath: `${webappUrl}settings/profile`, | ||
| cta: 'Add profile image', | ||
| benefit: | ||
| 'Stand out in comments and discussions. Profiles with photos get more engagement.', | ||
| }, | ||
| { | ||
| label: 'Headline', | ||
| completed: completion.hasHeadline, | ||
| redirectPath: `${webappUrl}settings/profile?field=bio`, | ||
| cta: 'Write your headline', | ||
| benefit: | ||
| 'Tell the community who you are. A good headline helps others connect with you.', | ||
| }, | ||
| { | ||
| label: 'Experience level', | ||
| completed: completion.hasExperienceLevel, | ||
| redirectPath: `${webappUrl}settings/profile?field=experienceLevel`, | ||
| cta: 'Set experience level', | ||
| benefit: | ||
| 'Get personalized content recommendations based on where you are in your career.', | ||
| }, | ||
| { | ||
| label: 'Work experience', | ||
| completed: completion.hasWork, | ||
| redirectPath: `${webappUrl}settings/profile/experience/work`, | ||
| cta: 'Add work experience', | ||
| benefit: | ||
| 'Showcase your background and unlock opportunities from companies looking for talent like you.', | ||
| }, | ||
| { | ||
| label: 'Education', | ||
| completed: completion.hasEducation, | ||
| redirectPath: `${webappUrl}settings/profile/experience/education`, | ||
| cta: 'Add education', | ||
| benefit: | ||
| 'Complete your story. Education helps others understand your journey.', | ||
| }, | ||
| ]; | ||
| }; | ||
|
|
||
| export const ProfileCompletionCard = ({ | ||
| className, | ||
| }: ProfileCompletionCardProps): ReactElement | null => { | ||
| const { user } = useAuthContext(); | ||
| const { logEvent } = useLogContext(); | ||
| const { checkHasCompleted, completeAction, isActionsFetched } = useActions(); | ||
| const profileCompletion = user?.profileCompletion; | ||
| const isImpressionTracked = useRef(false); | ||
|
|
||
| const items = useMemo( | ||
| () => (profileCompletion ? getCompletionItems(profileCompletion) : []), | ||
| [profileCompletion], | ||
| ); | ||
|
|
||
| const incompleteItems = useMemo( | ||
| () => items.filter((item) => !item.completed), | ||
| [items], | ||
| ); | ||
|
|
||
| const firstIncompleteItem = incompleteItems[0]; | ||
| const progress = profileCompletion?.percentage ?? 0; | ||
| const isCompleted = progress === 100; | ||
| const isDismissed = | ||
| isActionsFetched && checkHasCompleted(ActionType.ProfileCompletionCard); | ||
|
|
||
| const shouldShow = | ||
| profileCompletion && !isCompleted && firstIncompleteItem && !isDismissed; | ||
|
|
||
| useEffect(() => { | ||
| if (!shouldShow || isImpressionTracked.current) { | ||
| return; | ||
| } | ||
|
|
||
| logEvent({ | ||
| event_name: LogEvent.Impression, | ||
| target_type: TargetType.ProfileCompletionCard, | ||
| }); | ||
| isImpressionTracked.current = true; | ||
| }, [shouldShow, logEvent]); | ||
|
|
||
| const handleCtaClick = useCallback(() => { | ||
| logEvent({ | ||
| event_name: LogEvent.Click, | ||
| target_type: TargetType.ProfileCompletionCard, | ||
| target_id: 'cta', | ||
| }); | ||
| }, [logEvent]); | ||
|
|
||
| const handleDismiss = useCallback(() => { | ||
| logEvent({ | ||
| event_name: LogEvent.Click, | ||
| target_type: TargetType.ProfileCompletionCard, | ||
| target_id: 'dismiss', | ||
| }); | ||
| completeAction(ActionType.ProfileCompletionCard); | ||
| }, [logEvent, completeAction]); | ||
|
|
||
| if (!shouldShow) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className={classNames('flex flex-1 p-2 laptop:p-0', className?.container)} | ||
| > | ||
| <div | ||
| style={{ | ||
| border: profileCompletionCardBorder, | ||
| background: profileCompletionCardBg, | ||
| }} | ||
| className={classNames( | ||
| 'relative flex flex-1 flex-col gap-4 rounded-16 px-6 py-4', | ||
| 'backdrop-blur-3xl', | ||
| className?.card, | ||
| )} | ||
| > | ||
| <CloseButton | ||
| className="absolute right-2 top-2" | ||
| size={ButtonSize.XSmall} | ||
| onClick={handleDismiss} | ||
| /> | ||
| <ProgressCircle progress={progress} size={48} showPercentage /> | ||
| <Typography | ||
| type={TypographyType.Title2} | ||
| color={TypographyColor.Primary} | ||
| bold | ||
| > | ||
| Complete your profile | ||
| </Typography> | ||
| <div className="flex flex-col gap-2"> | ||
| <Typography | ||
| type={TypographyType.Callout} | ||
| color={TypographyColor.Tertiary} | ||
| > | ||
| Finish setting up your profile to get the most out of daily.dev: | ||
| </Typography> | ||
| <ul className="flex list-inside list-disc flex-col gap-1"> | ||
| {incompleteItems.map((item) => ( | ||
| <li key={item.label}> | ||
| <Typography | ||
| tag={TypographyTag.Span} | ||
| type={TypographyType.Callout} | ||
| color={TypographyColor.Secondary} | ||
| > | ||
| {item.label} | ||
| </Typography> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </div> | ||
| <Button | ||
| style={{ | ||
| background: profileCompletionButtonBg, | ||
| }} | ||
| className="mt-auto w-full" | ||
| tag="a" | ||
| href={firstIncompleteItem.redirectPath} | ||
| type="button" | ||
| variant={ButtonVariant.Primary} | ||
| size={ButtonSize.Small} | ||
| onClick={handleCtaClick} | ||
| > | ||
| {firstIncompleteItem.cta} | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default ProfileCompletionCard; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
packages/shared/src/hooks/profile/useProfileCompletionCard.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { useAuthContext } from '../../contexts/AuthContext'; | ||
| import { useConditionalFeature } from '../useConditionalFeature'; | ||
| import { profileCompletionCardFeature } from '../../lib/featureManagement'; | ||
| import { useActions } from '../useActions'; | ||
| import { ActionType } from '../../graphql/actions'; | ||
|
|
||
| interface UseProfileCompletionCard { | ||
| showProfileCompletionCard: boolean; | ||
| isDismissed: boolean; | ||
| isLoading: boolean; | ||
| } | ||
|
|
||
| interface UseProfileCompletionCardProps { | ||
| isMyFeed: boolean; | ||
| } | ||
|
|
||
| export const useProfileCompletionCard = ({ | ||
| isMyFeed, | ||
| }: UseProfileCompletionCardProps): UseProfileCompletionCard => { | ||
| const { user } = useAuthContext(); | ||
| const { checkHasCompleted, isActionsFetched } = useActions(); | ||
|
|
||
| const profileCompletion = user?.profileCompletion; | ||
| const isCompleted = (profileCompletion?.percentage ?? 100) === 100; | ||
| const isDismissed = | ||
| isActionsFetched && checkHasCompleted(ActionType.ProfileCompletionCard); | ||
|
|
||
| const hasNotDismissed = | ||
| isActionsFetched && !checkHasCompleted(ActionType.ProfileCompletionCard); | ||
| const shouldEvaluate = isMyFeed && !isCompleted && hasNotDismissed; | ||
|
|
||
| const { value: featureEnabled, isLoading: isFeatureLoading } = | ||
| useConditionalFeature({ | ||
| feature: profileCompletionCardFeature, | ||
| shouldEvaluate, | ||
| }); | ||
|
|
||
| const couldPotentiallyShow = isMyFeed && !isCompleted && !!profileCompletion; | ||
| const isLoading = | ||
| couldPotentiallyShow && (!isActionsFetched || isFeatureLoading); | ||
|
|
||
| return { | ||
| showProfileCompletionCard: | ||
| shouldEvaluate && featureEnabled && !!profileCompletion, | ||
| isDismissed, | ||
| isLoading, | ||
| }; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there so much calculations in the useProfileCompletionCard hook and also here?