From 2630ff8f2e7f9ee4d6e7cee6618dea7fb622e848 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Tue, 6 Jan 2026 16:46:06 +0100 Subject: [PATCH 1/2] feat: add profile completion card to feed with priority over brief card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new ProfileCompletionCard component that encourages users to complete their profile by showing their progress and the next incomplete section. The card displays in the first feed slot and takes priority over the brief card, using the existing ProfileCompletion logic and displaying a cabbage-themed design. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- packages/shared/src/components/Feed.tsx | 22 ++- .../cards/ProfileCompletionCard.tsx | 141 ++++++++++++++++++ 2 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 packages/shared/src/components/cards/ProfileCompletionCard.tsx diff --git a/packages/shared/src/components/Feed.tsx b/packages/shared/src/components/Feed.tsx index f3c2c4de8e..e20c249247 100644 --- a/packages/shared/src/components/Feed.tsx +++ b/packages/shared/src/components/Feed.tsx @@ -41,6 +41,7 @@ import { useFeedVotePost, useMutationSubscription, } from '../hooks'; +import { useProfileCompletionIndicator } from '../hooks/profile/useProfileCompletionIndicator'; import type { AllFeedPages } from '../lib/query'; import { OtherFeedPage, RequestKey } from '../lib/query'; @@ -129,6 +130,13 @@ const BriefCardFeed = dynamic( ), ); +const ProfileCompletionCard = dynamic( + () => + import( + /* webpackChunkName: "profileCompletionCard" */ './cards/ProfileCompletionCard' + ), +); + const calculateRow = (index: number, numCards: number): number => Math.floor(index / numCards); const calculateColumn = (index: number, numCards: number): number => @@ -203,6 +211,8 @@ export default function Feed({ shouldEvaluate: isMyFeed && hasNoBriefAction, }); const showBriefCard = isMyFeed && briefCardFeatureValue && hasNoBriefAction; + const { showIndicator: showProfileCompletionCard } = + useProfileCompletionIndicator(); const [getProducts] = useUpdateQuery(getProductsQueryOptions()); const { value: briefBannerPage } = useConditionalFeature({ @@ -527,10 +537,11 @@ export default function Feed({ const currentPageSize = pageSize ?? currentSettings.pageSize; const showPromoBanner = !!briefBannerPage; const columnsDiffWithPage = currentPageSize % virtualizedNumCards; + const showFirstSlotCard = showProfileCompletionCard || showBriefCard; const indexWhenShowingPromoBanner = currentPageSize * Number(briefBannerPage) - // number of items at that page columnsDiffWithPage * Number(briefBannerPage) - // cards let out of rows * page number - Number(showBriefCard); // if showing the brief card, we need to subtract 1 to the index + Number(showFirstSlotCard); // if showing a first slot card, we need to subtract 1 to the index return ( @@ -539,7 +550,14 @@ export default function Feed({ <>{emptyScreen} ) : ( <> - {showBriefCard && ( + {showProfileCompletionCard && ( + + )} + {showBriefCard && !showProfileCompletionCard && ( ; +}; + +const getCompletionItems = ( + completion: ProfileCompletion, +): 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 profileCompletionCardBorder = + '1px solid var(--theme-accent-cabbage-default)'; + +const profileCompletionCardBg = + 'linear-gradient(180deg, rgba(61, 179, 158, 0.16) 0%, rgba(61, 179, 158, 0.08) 50%, rgba(61, 179, 158, 0.04) 100%)'; + +const profileCompletionButtonBg = 'var(--theme-accent-cabbage-default)'; + +export const ProfileCompletionCard = ({ + className, +}: ProfileCompletionCardProps): 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 firstIncompleteItem = incompleteItems[0]; + const progress = profileCompletion?.percentage ?? 0; + const isCompleted = progress === 100; + + if (!profileCompletion || isCompleted || !firstIncompleteItem) { + return null; + } + + const nextSectionText = `Add your ${firstIncompleteItem.label.toLowerCase()} to improve your profile visibility.`; + + return ( +
+
+ + + Profile completion + + + {nextSectionText} + + +
+
+ ); +}; + +export default ProfileCompletionCard; From 98cfe73c524f37aa87cb41aed858de1dbf1e662f Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Tue, 6 Jan 2026 17:05:48 +0100 Subject: [PATCH 2/2] refactor: improve profile completion card styling and CTAs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use softer, less saturated brand purple colors for border and button - Replace generic text with benefit-focused copy for each profile section - Add section-specific CTAs that explain the value of completing each field 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- .../cards/ProfileCompletionCard.tsx | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/components/cards/ProfileCompletionCard.tsx b/packages/shared/src/components/cards/ProfileCompletionCard.tsx index ede70310e5..25af04bbae 100644 --- a/packages/shared/src/components/cards/ProfileCompletionCard.tsx +++ b/packages/shared/src/components/cards/ProfileCompletionCard.tsx @@ -16,6 +16,8 @@ type CompletionItem = { label: string; completed: boolean; redirectPath: string; + cta: string; + benefit: string; }; type ProfileCompletionCardProps = { @@ -33,37 +35,54 @@ const getCompletionItems = ( 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.', }, ]; }; +// Using softer, less saturated purple tones that align with the brand const profileCompletionCardBorder = - '1px solid var(--theme-accent-cabbage-default)'; + '1px solid color-mix(in srgb, var(--theme-accent-cabbage-subtler), transparent 50%)'; const profileCompletionCardBg = - 'linear-gradient(180deg, rgba(61, 179, 158, 0.16) 0%, rgba(61, 179, 158, 0.08) 50%, rgba(61, 179, 158, 0.04) 100%)'; + '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 = 'var(--theme-accent-cabbage-default)'; +const profileCompletionButtonBg = + 'color-mix(in srgb, var(--theme-accent-cabbage-default), transparent 20%)'; export const ProfileCompletionCard = ({ className, @@ -89,8 +108,6 @@ export const ProfileCompletionCard = ({ return null; } - const nextSectionText = `Add your ${firstIncompleteItem.label.toLowerCase()} to improve your profile visibility.`; - return (
- {nextSectionText} + {firstIncompleteItem.benefit}