-
Notifications
You must be signed in to change notification settings - Fork 291
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
Changes from 15 commits
2630ff8
98cfe73
098388b
5746845
1069243
1b6d860
1fba15f
f166df5
e9c9da1
772acdb
f2e084f
10a5b49
e7b7527
9c4901c
6dd2064
17812e8
8fc61c4
da51aa7
b191663
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| 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'; | ||
|
|
||
| type CompletionItem = { | ||
| label: string; | ||
| completed: boolean; | ||
| redirectPath: string; | ||
| cta: string; | ||
| benefit: string; | ||
| }; | ||
|
|
||
| type ProfileCompletionCardProps = { | ||
| className?: Partial<{ | ||
| container: string; | ||
| card: string; | ||
| }>; | ||
| }; | ||
|
|
||
| const profileCompletionCardBorder = | ||
| '1px solid color-mix(in srgb, var(--theme-accent-cabbage-subtler), transparent 50%)'; | ||
|
|
||
| const profileCompletionCardBg = | ||
| 'linear-gradient(180deg, color-mix(in srgb, var(--theme-accent-cabbage-bolder), transparent 92%) 0%, color-mix(in srgb, var(--theme-accent-cabbage-bolder), transparent 96%) 100%)'; | ||
|
||
|
|
||
| const profileCompletionButtonBg = | ||
| 'color-mix(in srgb, var(--theme-accent-cabbage-default), transparent 20%)'; | ||
|
||
|
|
||
| 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; | ||
|
Comment on lines
+107
to
+114
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
|
|
||
| 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; | ||
| 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, | ||
| }; | ||
| }; |
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.
nit: why is this not tailwind?