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}
/>