diff --git a/packages/shared/src/components/modals/post/CreateSharedPostModal.tsx b/packages/shared/src/components/modals/post/CreateSharedPostModal.tsx index 13dc50f46b..dabd7b533e 100644 --- a/packages/shared/src/components/modals/post/CreateSharedPostModal.tsx +++ b/packages/shared/src/components/modals/post/CreateSharedPostModal.tsx @@ -131,6 +131,7 @@ export function CreateSharedPostModal({ currentPercentage={user?.profileCompletion?.percentage} requiredPercentage={requiredPercentage} description="Update your profile to share posts in squads." + profileCompletion={user?.profileCompletion} buttonSize={ButtonSize.Small} /> diff --git a/packages/shared/src/components/post/write/ProfileCompletionPostGate.spec.tsx b/packages/shared/src/components/post/write/ProfileCompletionPostGate.spec.tsx new file mode 100644 index 0000000000..259ba44b30 --- /dev/null +++ b/packages/shared/src/components/post/write/ProfileCompletionPostGate.spec.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { ProfileCompletionPostGate } from './ProfileCompletionPostGate'; + +describe('ProfileCompletionPostGate', () => { + it('should render missing profile items when profile completion is available', () => { + render( + , + ); + + expect( + screen.getByText('Add Profile image, Headline and Work experience.'), + ).toBeInTheDocument(); + expect( + screen.queryByText('You are 50% away from the 70% requirement.'), + ).not.toBeInTheDocument(); + }); + + it('should fall back to the percentage requirement message when profile completion is missing', () => { + render( + , + ); + + expect( + screen.getByText('You are 50% away from the 70% requirement.'), + ).toBeInTheDocument(); + }); + + it('should keep the profile CTA destination unchanged', () => { + render( + , + ); + + expect( + screen.getByRole('link', { name: 'Complete profile' }), + ).toHaveAttribute('href', '/settings/profile'); + }); +}); diff --git a/packages/shared/src/components/post/write/ProfileCompletionPostGate.tsx b/packages/shared/src/components/post/write/ProfileCompletionPostGate.tsx index 796af7ad60..1964bc6bc1 100644 --- a/packages/shared/src/components/post/write/ProfileCompletionPostGate.tsx +++ b/packages/shared/src/components/post/write/ProfileCompletionPostGate.tsx @@ -10,6 +10,11 @@ import { } from '../../typography/Typography'; import Link from '../../utilities/Link'; import { settingsUrl } from '../../../lib/constants'; +import type { ProfileCompletion } from '../../../lib/user'; +import { + formatCompletionDescription, + getCompletionItems, +} from '../../../lib/profileCompletion'; import { profileCompletionButtonBg, profileCompletionCardBg, @@ -21,6 +26,7 @@ interface ProfileCompletionPostGateProps { currentPercentage?: number; requiredPercentage?: number; description: string; + profileCompletion?: ProfileCompletion; compact?: boolean; buttonSize?: ButtonSize; } @@ -30,14 +36,22 @@ export function ProfileCompletionPostGate({ currentPercentage = 0, requiredPercentage = 0, description, + profileCompletion, compact = false, buttonSize = ButtonSize.Medium, }: ProfileCompletionPostGateProps): ReactElement { const normalizedCurrent = Math.max(0, Math.min(100, currentPercentage)); const normalizedRequired = Math.max(0, Math.min(100, requiredPercentage)); const completionGap = Math.max(0, normalizedRequired - normalizedCurrent); + const incompleteItems = profileCompletion + ? getCompletionItems(profileCompletion).filter((item) => !item.completed) + : []; + const missingItemsDescription = + incompleteItems.length > 0 + ? formatCompletionDescription(incompleteItems) + : null; const requirementMessage = - normalizedRequired > 0 + !missingItemsDescription && normalizedRequired > 0 ? `You are ${completionGap}% away from the ${normalizedRequired}% requirement.` : null; @@ -77,6 +91,14 @@ export function ProfileCompletionPostGate({ + {missingItemsDescription && ( + + {missingItemsDescription} + + )} {requirementMessage && ( { - 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 formatDescription = (incompleteItems: CompletionItem[]): string => { - if (incompleteItems.length === 0) { - return 'Profile completed!'; - } - - const labels = incompleteItems.map((item) => item.label); - const formattedList = - labels.length === 1 - ? labels[0] - : `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`; - - return `Add ${formattedList}.`; -}; - export const ProfileCompletion = ({ className, }: ProfileCompletionProps): ReactElement | null => { @@ -92,7 +42,7 @@ export const ProfileCompletion = ({ ); const description = useMemo( - () => formatDescription(incompleteItems), + () => formatCompletionDescription(incompleteItems), [incompleteItems], ); diff --git a/packages/shared/src/lib/profileCompletion.spec.ts b/packages/shared/src/lib/profileCompletion.spec.ts new file mode 100644 index 0000000000..ea2fbb0a8e --- /dev/null +++ b/packages/shared/src/lib/profileCompletion.spec.ts @@ -0,0 +1,60 @@ +import { + formatCompletionDescription, + getCompletionItems, +} from './profileCompletion'; +import type { ProfileCompletion } from './user'; + +const profileCompletion: ProfileCompletion = { + percentage: 20, + hasProfileImage: false, + hasHeadline: false, + hasExperienceLevel: true, + hasWork: false, + hasEducation: true, +}; + +describe('profileCompletion', () => { + it('should return completion items in the expected priority order', () => { + expect( + getCompletionItems(profileCompletion).map((item) => item.label), + ).toEqual([ + 'Profile image', + 'Headline', + 'Experience level', + 'Work experience', + 'Education', + ]); + }); + + it('should return only incomplete completion items', () => { + expect( + getCompletionItems(profileCompletion) + .filter((item) => !item.completed) + .map((item) => item.label), + ).toEqual(['Profile image', 'Headline', 'Work experience']); + }); + + it('should format a single incomplete item description', () => { + expect( + formatCompletionDescription([ + { + label: 'Headline', + completed: false, + redirectPath: '/settings/profile?field=bio', + }, + ]), + ).toBe('Add Headline.'); + }); + + it('should format a multi-item incomplete description', () => { + expect( + formatCompletionDescription( + getCompletionItems(profileCompletion).filter((item) => !item.completed), + ), + ).toBe('Add Profile image, Headline and Work experience.'); + }); + + it('should format the completed state description', () => { + expect(formatCompletionDescription([])).toBe('Profile completed!'); + }); +}); diff --git a/packages/shared/src/lib/profileCompletion.ts b/packages/shared/src/lib/profileCompletion.ts new file mode 100644 index 0000000000..704cc77647 --- /dev/null +++ b/packages/shared/src/lib/profileCompletion.ts @@ -0,0 +1,56 @@ +import type { ProfileCompletion } from './user'; +import { webappUrl } from './constants'; + +export type CompletionItem = { + label: string; + completed: boolean; + redirectPath: string; +}; + +export 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`, + }, + ]; +}; + +export const formatCompletionDescription = ( + incompleteItems: CompletionItem[], +): string => { + if (incompleteItems.length === 0) { + return 'Profile completed!'; + } + + const labels = incompleteItems.map((item) => item.label); + const formattedList = + labels.length === 1 + ? labels[0] + : `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`; + + return `Add ${formattedList}.`; +}; diff --git a/packages/webapp/pages/squads/create.tsx b/packages/webapp/pages/squads/create.tsx index 192c333399..733acceb2d 100644 --- a/packages/webapp/pages/squads/create.tsx +++ b/packages/webapp/pages/squads/create.tsx @@ -274,6 +274,7 @@ function CreatePost(): ReactElement { currentPercentage={user?.profileCompletion?.percentage} requiredPercentage={requiredPercentage} description="Add your profile details to keep post creation available." + profileCompletion={user?.profileCompletion} buttonSize={ButtonSize.Medium} />