From aa4b5c90221a9646b3ba375c2913ffe3aa89d4a4 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 13 Jan 2026 14:50:57 +0200 Subject: [PATCH 01/18] PM-3357 - profile completeness messaging --- .../ProfileCompleteness.module.scss | 6 +++++ .../ProfileCompleteness.tsx | 22 ++++++++++++++++++- .../data-providers/useProfileCompleteness.ts | 2 ++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.module.scss b/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.module.scss index 5273eb6a6..68ed3d77e 100644 --- a/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.module.scss +++ b/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.module.scss @@ -18,6 +18,12 @@ font-weight: 900; } + small { + font-weight: 400; + font-size: 20px; + line-height: 24px; + } + @include ltemd { font-size: 24px; line-height: 28px; diff --git a/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.tsx b/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.tsx index 10d87cdb4..b71e2ba60 100644 --- a/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.tsx +++ b/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.tsx @@ -1,4 +1,5 @@ -import { FC, useEffect } from 'react' +import { FC, useEffect, useMemo } from 'react' +import { startCase } from 'lodash'; import { useProfileCompleteness, UserProfile } from '~/libs/core' @@ -21,10 +22,29 @@ const ProfileCompleteness: FC = props => { useEffect(() => { completeness?.mutate() }, [props.profile]) + const [count, incompleteEntries] = useMemo(() => { + const fields = Object.entries(completeness.entries) + .filter(([, value]) => !value) + .map(([key]) => startCase(key)); + + if (fields.length === 2) { + return [2, fields.join(' and ')]; + } + + if (fields.length >= 2) { + fields[fields.length - 1] = `and ${fields[fields.length - 1]}`; + } + + return [fields.length, fields.join(', ')] + }, [completeness]); + return hideCompletenessMeter ? <> : (
Profile: {`${completed}% Complete`} +
+ Only {incompleteEntries} left to fill. Please add {count === 1 ? 'it' : 'them'} to make your profile more discoverable. +
) } diff --git a/src/libs/core/lib/profile/data-providers/useProfileCompleteness.ts b/src/libs/core/lib/profile/data-providers/useProfileCompleteness.ts index ed6e4809b..23a2fe1c8 100644 --- a/src/libs/core/lib/profile/data-providers/useProfileCompleteness.ts +++ b/src/libs/core/lib/profile/data-providers/useProfileCompleteness.ts @@ -3,6 +3,7 @@ import useSWR, { KeyedMutator, SWRResponse } from 'swr' import { getProfileUrl } from '../profile-functions' export function useProfileCompleteness(memberHandle?: string): { + entries: {[key: string]: boolean}, isLoading: boolean, mutate: KeyedMutator, percent: number | undefined, @@ -13,6 +14,7 @@ export function useProfileCompleteness(memberHandle?: string): { const percentComplete = data?.data?.percentComplete return { + entries: data?.data ?? {}, isLoading: percentComplete === undefined, mutate, percent: (percentComplete ?? 0) * 100, From c0ee28c08a6e542e022270319b70bd536550f846 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 13 Jan 2026 15:38:51 +0200 Subject: [PATCH 02/18] lint fixes --- .../ProfileCompleteness.tsx | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.tsx b/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.tsx index b71e2ba60..4be2ec0a9 100644 --- a/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.tsx +++ b/src/apps/profiles/src/member-profile/profile-completeness/ProfileCompleteness.tsx @@ -1,5 +1,5 @@ import { FC, useEffect, useMemo } from 'react' -import { startCase } from 'lodash'; +import { startCase } from 'lodash' import { useProfileCompleteness, UserProfile } from '~/libs/core' @@ -25,25 +25,33 @@ const ProfileCompleteness: FC = props => { const [count, incompleteEntries] = useMemo(() => { const fields = Object.entries(completeness.entries) .filter(([, value]) => !value) - .map(([key]) => startCase(key)); + .map(([key]) => startCase(key)) - if (fields.length === 2) { - return [2, fields.join(' and ')]; - } + if (fields.length === 2) { + return [2, fields.join(' and ')] + } - if (fields.length >= 2) { - fields[fields.length - 1] = `and ${fields[fields.length - 1]}`; - } + if (fields.length >= 2) { + fields[fields.length - 1] = `and ${fields[fields.length - 1]}` + } return [fields.length, fields.join(', ')] - }, [completeness]); + }, [completeness.entries]) return hideCompletenessMeter ? <> : (
Profile: {`${completed}% Complete`}
- Only {incompleteEntries} left to fill. Please add {count === 1 ? 'it' : 'them'} to make your profile more discoverable. + + Only + {incompleteEntries} + {' '} + left to fill. Please add + {count === 1 ? 'it' : 'them'} + {' '} + to make your profile more discoverable. +
) From 6cf84b518b12b365f05320011696133fccb08c01 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 14 Jan 2026 00:01:08 +0530 Subject: [PATCH 03/18] feat: added download button in profile page --- .../page-layout/ProfilePageLayout.module.scss | 57 ++++++++ .../page-layout/ProfilePageLayout.tsx | 123 +++++++++++++----- .../lib/profile/profile-functions/index.ts | 1 + .../profile-store/profile-xhr.store.ts | 6 +- .../profile-functions/profile.functions.ts | 13 ++ 5 files changed, 166 insertions(+), 34 deletions(-) diff --git a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.module.scss b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.module.scss index 5e25fef6e..1a9a667e9 100644 --- a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.module.scss +++ b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.module.scss @@ -7,12 +7,69 @@ height: 100%; .profileHeaderWrap { + position: relative; background: url('../../lib/assets/profile-header-bg.png') no-repeat right top / auto, linear-gradient(#0d83c5, #0e89d5); @include ltelg { background: url('../../lib/assets/profile-header-bg-mobile.png') no-repeat right top /100% 100%; } + .downloadButtonWrap { + position: absolute; + top: $sp-4; + right: calc((100% - #{$xxl-min}) / 2); + max-width: $xxl-min; + width: 100%; + display: flex; + justify-content: flex-end; + padding-right: $sp-8; + z-index: 10; + pointer-events: none; + + @include ltexl { + right: $sp-8; + } + + @include ltemd { + padding-right: $sp-6; + right: $sp-6; + } + + @include ltesm { + padding-right: $sp-4; + right: $sp-4; + } + + @include ltelg { + position: absolute; + top: $sp-4; + right: $sp-4; + left: auto; + max-width: none; + width: auto; + padding: 0; + pointer-events: auto; + } + + > * { + pointer-events: auto; + } + } + + .downloadButton { + color: $tc-white; + padding: $sp-2 $sp-4; + border-radius: 4px; + font-weight: $font-weight-bold; + font-family: $font-roboto; + font-size: 16px; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } + .profileHeaderContent { padding: 0; max-height: 260px; diff --git a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx index 290cd1cd8..6a9cd7ee6 100644 --- a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx +++ b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx @@ -1,6 +1,6 @@ -import { FC } from 'react' +import { Dispatch, FC, SetStateAction, useState } from 'react' -import { UserProfile } from '~/libs/core' +import { UserProfile, downloadProfileAsync } from '~/libs/core' import { Button, ContentLayout, IconSolid, PageTitle } from '~/libs/ui' // import { MemberTCActivityInfo } from '../tc-activity' @@ -26,35 +26,91 @@ interface ProfilePageLayoutProps { handleBackBtn: () => void } -const ProfilePageLayout: FC = (props: ProfilePageLayoutProps) => ( -
- - {`${props.profile.handle} | Community Profile | Topcoder`} - -
- - {props.isTalentSearch && ( -
-
- )} - -
-
-
+const ProfilePageLayout: FC = (props: ProfilePageLayoutProps) => { + function canDownloadProfile(authProfile: UserProfile | undefined, profile: UserProfile): boolean { + if (!authProfile) { + return false + } + // Check if user is viewing their own profile + if (authProfile.handle === profile.handle) { + return true + } + // Check if user has admin roles + const adminRoles = ['administrator', 'admin'] + if (authProfile.roles?.some(role => adminRoles.includes(role.toLowerCase()))) { + return true + } + // Check if user has PM or Talent Manager roles + const allowedRoles = ['Project Manager', 'Talent Manager'] + if (authProfile.roles?.some(role => allowedRoles.some(allowed => role.toLowerCase() === allowed.toLowerCase()))) { + return true + } + return false + } + + const canDownload: boolean = canDownloadProfile(props.authProfile, props.profile) + + const [isDownloading, setIsDownloading]: [boolean, Dispatch>] + = useState(false) + + async function handleDownloadProfile(): Promise { + if (isDownloading) { + return + } + setIsDownloading(true) + try { + await downloadProfileAsync(props.profile.handle) + } catch (error) { + // Error handling - could show a toast notification here + console.error('Failed to download profile:', error) + } finally { + setIsDownloading(false) + } + } + + return ( +
+ + {`${props.profile.handle} | Community Profile | Topcoder`} + +
+ { + canDownload && ( +
+
+ ) + } + + {props.isTalentSearch && ( +
+
+ )} + +
+
+
= (props: ProfilePageLayoutP -
-) +
+ ) +} export default ProfilePageLayout diff --git a/src/libs/core/lib/profile/profile-functions/index.ts b/src/libs/core/lib/profile/profile-functions/index.ts index ed8ec1d6c..e93fa1c4e 100644 --- a/src/libs/core/lib/profile/profile-functions/index.ts +++ b/src/libs/core/lib/profile/profile-functions/index.ts @@ -14,6 +14,7 @@ export { modifyTracksAsync, updateMemberProfileAsync, updateMemberPhotoAsync, + downloadProfileAsync, updateOrCreateMemberTraitsAsync, updateDeleteOrCreateMemberTraitAsync, } from './profile.functions' diff --git a/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts b/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts index 452f8c88f..733b37eb8 100644 --- a/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts +++ b/src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts @@ -1,4 +1,4 @@ -import { xhrDeleteAsync, xhrGetAsync, xhrPatchAsync, xhrPostAsync, xhrPutAsync } from '../../../xhr' +import { xhrDeleteAsync, xhrGetAsync, xhrGetBlobAsync, xhrPatchAsync, xhrPostAsync, xhrPutAsync } from '../../../xhr' import { CountryLookup } from '../../country-lookup.model' import { EditNameRequest } from '../../edit-name-request.model' import { ModifyTracksRequest } from '../../modify-tracks.request' @@ -126,3 +126,7 @@ export async function updateMemberPhoto(handle: string, payload: FormData): Prom }, }) } + +export async function downloadProfile(handle: string): Promise { + return xhrGetBlobAsync(`${profileUrl(handle)}/profileDownload`) +} diff --git a/src/libs/core/lib/profile/profile-functions/profile.functions.ts b/src/libs/core/lib/profile/profile-functions/profile.functions.ts index 581723f3a..2337c771f 100644 --- a/src/libs/core/lib/profile/profile-functions/profile.functions.ts +++ b/src/libs/core/lib/profile/profile-functions/profile.functions.ts @@ -16,6 +16,7 @@ import { getMemberStats, getVerification, profileStoreGet, profileStorePatchName import { createMemberTraits, deleteMemberTrait, + downloadProfile, getCountryLookup, modifyTracks, updateMemberEmailPreferences, @@ -143,6 +144,18 @@ export async function updateMemberPhotoAsync(handle: string, payload: FormData): return updateMemberPhoto(handle, payload) } +export async function downloadProfileAsync(handle: string): Promise { + const blob = await downloadProfile(handle) + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', `profile-${handle}.pdf`) + document.body.appendChild(link) + link.click() + link.parentNode?.removeChild(link) + window.URL.revokeObjectURL(url) +} + export async function updateOrCreateMemberTraitsAsync( handle: string, traits: UserTraits[], From 905c256c2010c8cc932e7dd419dad461ebe4ce3d Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 14 Jan 2026 00:20:35 +0530 Subject: [PATCH 04/18] fix: lint --- .../page-layout/ProfilePageLayout.tsx | 117 ++++++++++-------- 1 file changed, 63 insertions(+), 54 deletions(-) diff --git a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx index 6a9cd7ee6..d395e480c 100644 --- a/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx +++ b/src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx @@ -1,6 +1,6 @@ import { Dispatch, FC, SetStateAction, useState } from 'react' -import { UserProfile, downloadProfileAsync } from '~/libs/core' +import { downloadProfileAsync, UserProfile } from '~/libs/core' import { Button, ContentLayout, IconSolid, PageTitle } from '~/libs/ui' // import { MemberTCActivityInfo } from '../tc-activity' @@ -31,20 +31,28 @@ const ProfilePageLayout: FC = (props: ProfilePageLayoutP if (!authProfile) { return false } + // Check if user is viewing their own profile if (authProfile.handle === profile.handle) { return true } + // Check if user has admin roles const adminRoles = ['administrator', 'admin'] if (authProfile.roles?.some(role => adminRoles.includes(role.toLowerCase()))) { return true } + // Check if user has PM or Talent Manager roles const allowedRoles = ['Project Manager', 'Talent Manager'] - if (authProfile.roles?.some(role => allowedRoles.some(allowed => role.toLowerCase() === allowed.toLowerCase()))) { + if (authProfile + .roles?.some( + role => allowedRoles.some(allowed => role.toLowerCase() === allowed.toLowerCase()), + ) + ) { return true } + return false } @@ -57,6 +65,7 @@ const ProfilePageLayout: FC = (props: ProfilePageLayoutP if (isDownloading) { return } + setIsDownloading(true) try { await downloadProfileAsync(props.profile.handle) @@ -80,7 +89,7 @@ const ProfilePageLayout: FC = (props: ProfilePageLayoutP
) } diff --git a/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts b/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts index d7c225254..bad3ebea7 100644 --- a/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts +++ b/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts @@ -43,3 +43,23 @@ export async function updateMemberSkills( skills, }) } + +/** + * Fetch skills by their IDs + * @param skillIds Array of skill UUIDs + * @returns Promise with array of UserSkill objects + */ +export async function fetchSkillsByIds(skillIds: string[]): Promise { + if (!skillIds || skillIds.length === 0) { + return Promise.resolve([]) + } + + try { + const skillPromises = skillIds.map(skillId => xhrGetAsync(`${baseUrl}/skills/${skillId}`) + .catch(() => undefined)) + const results = await Promise.all(skillPromises) + return results.filter((skill): skill is UserSkill => (skill !== null || skill !== undefined)) + } catch { + return [] + } +} From b53024641d2b3e0d727878403dd7ae387274927d Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 15 Jan 2026 15:12:00 +1100 Subject: [PATCH 08/18] Minor tweaks to legend view based on user feedback --- src/apps/calendar/src/lib/utils/calendar.utils.ts | 1 - .../src/pages/team-calendar/TeamCalendarPage.module.scss | 3 +++ src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/apps/calendar/src/lib/utils/calendar.utils.ts b/src/apps/calendar/src/lib/utils/calendar.utils.ts index b9a90153f..defb7683b 100644 --- a/src/apps/calendar/src/lib/utils/calendar.utils.ts +++ b/src/apps/calendar/src/lib/utils/calendar.utils.ts @@ -22,7 +22,6 @@ export const legendStatusOrder: LeaveStatus[] = [ LeaveStatus.AVAILABLE, LeaveStatus.LEAVE, LeaveStatus.HOLIDAY, - LeaveStatus.WIPRO_HOLIDAY, LeaveStatus.WEEKEND, ] diff --git a/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.module.scss b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.module.scss index 4735ddd7f..110c4f957 100644 --- a/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.module.scss +++ b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.module.scss @@ -17,6 +17,9 @@ .navigation { display: flex; + flex-direction: column; + align-items: center; + gap: 8px; justify-content: center; margin: 14px 0 10px; } diff --git a/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx index ab50c6d28..4e3ebc76b 100644 --- a/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx +++ b/src/apps/calendar/src/pages/team-calendar/TeamCalendarPage.tsx @@ -1,7 +1,7 @@ import { addMonths, endOfMonth, startOfMonth, subMonths } from 'date-fns' import { FC, useCallback, useEffect, useMemo, useState } from 'react' -import { MonthNavigation, TeamCalendar } from '../../lib/components' +import { CalendarLegend, MonthNavigation, TeamCalendar } from '../../lib/components' import { useFetchTeamLeave } from '../../lib/hooks' import styles from './TeamCalendarPage.module.scss' @@ -57,6 +57,7 @@ const TeamCalendarPage: FC = () => { onNextMonth={handleNextMonth} onPrevMonth={handlePrevMonth} /> +
From fa871ac802d05ba587d1bc59080e608f2f44fe3e Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 15 Jan 2026 15:54:30 +1100 Subject: [PATCH 09/18] Legend wording --- src/apps/calendar/src/lib/utils/calendar.utils.ts | 2 +- .../src/pages/personal-calendar/PersonalCalendarPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/calendar/src/lib/utils/calendar.utils.ts b/src/apps/calendar/src/lib/utils/calendar.utils.ts index defb7683b..365a52f29 100644 --- a/src/apps/calendar/src/lib/utils/calendar.utils.ts +++ b/src/apps/calendar/src/lib/utils/calendar.utils.ts @@ -12,7 +12,7 @@ const statusColorMap: Record = { const statusLabelMap: Record = { [LeaveStatus.LEAVE]: 'Leave', - [LeaveStatus.HOLIDAY]: 'Personal Holiday', + [LeaveStatus.HOLIDAY]: 'Holiday', [LeaveStatus.WIPRO_HOLIDAY]: 'Wipro Holiday', [LeaveStatus.WEEKEND]: 'Weekend', [LeaveStatus.AVAILABLE]: 'Available', diff --git a/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx index fe2ab14b9..ac3d57220 100644 --- a/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx +++ b/src/apps/calendar/src/pages/personal-calendar/PersonalCalendarPage.tsx @@ -152,7 +152,7 @@ const PersonalCalendarPage: FC = () => { onClick={handleSetAsHoliday} disabled={!selectedDates.size || isUpdating} > - Set as Personal Holiday + Set as Holiday
) } From 17c42577c062bb781a425c240dfc4c4f1b54c308 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 15 Jan 2026 17:57:39 +0530 Subject: [PATCH 13/18] removed console log --- .../ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.tsx b/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.tsx index a15fd5098..a23d08e6c 100644 --- a/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.tsx +++ b/src/apps/profiles/src/member-profile/work-expirence/ModifyWorkExpirenceModal/ModifyWorkExpirenceModal.tsx @@ -358,7 +358,6 @@ const ModifyWorkExpirenceModal: FC = (props: Modi } } - console.log(formValues, 'formValues') return ( Date: Thu, 15 Jan 2026 18:02:21 +0530 Subject: [PATCH 14/18] fix: review comments --- src/apps/profiles/src/config/constants.ts | 7 +++++++ src/apps/profiles/src/lib/helpers.ts | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/apps/profiles/src/config/constants.ts b/src/apps/profiles/src/config/constants.ts index ec7f7dce4..98616ebfd 100644 --- a/src/apps/profiles/src/config/constants.ts +++ b/src/apps/profiles/src/config/constants.ts @@ -26,3 +26,10 @@ export enum profileEditModes { // (removed) CES Survey/Userflow integrations export const MAX_PRINCIPAL_SKILLS_COUNT = 10 + +export enum ADMIN_ROLES_ENUM { + ADMINISTRATOR = 'administrator', + ADMIN = 'admin', +} + +export const ADMIN_ROLES = [ADMIN_ROLES_ENUM.ADMINISTRATOR, ADMIN_ROLES_ENUM.ADMIN] diff --git a/src/apps/profiles/src/lib/helpers.ts b/src/apps/profiles/src/lib/helpers.ts index cb0e6c157..4be3c6825 100644 --- a/src/apps/profiles/src/lib/helpers.ts +++ b/src/apps/profiles/src/lib/helpers.ts @@ -1,5 +1,6 @@ /* eslint-disable complexity */ import { UserProfile } from '~/libs/core' +import { ADMIN_ROLES, ADMIN_ROLES_ENUM } from '../config' declare global { interface Window { tcUniNav: any } @@ -143,8 +144,7 @@ export function canDownloadProfile(authProfile: UserProfile | undefined, profile } // Check if user has admin roles - const adminRoles = ['administrator', 'admin'] - if (authProfile.roles?.some(role => adminRoles.includes(role.toLowerCase()))) { + if (authProfile.roles?.some(role => ADMIN_ROLES.includes(role.toLowerCase() as ADMIN_ROLES_ENUM))) { return true } From c3ca9bc5523e98e095b80cf41c72aa18fbdf7200 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 15 Jan 2026 18:07:53 +0530 Subject: [PATCH 15/18] fix: review comments --- src/apps/profiles/src/config/constants.ts | 9 +++------ src/apps/profiles/src/lib/helpers.ts | 6 +++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/apps/profiles/src/config/constants.ts b/src/apps/profiles/src/config/constants.ts index 98616ebfd..ddf1b7f1b 100644 --- a/src/apps/profiles/src/config/constants.ts +++ b/src/apps/profiles/src/config/constants.ts @@ -1,5 +1,7 @@ // import { EnvironmentConfig } from '~/config' +import { UserRole } from "~/libs/core" + export enum TRACKS_PROFILE_MAP { DEVELOP = 'Developer', DESIGN = 'Designer', @@ -27,9 +29,4 @@ export enum profileEditModes { export const MAX_PRINCIPAL_SKILLS_COUNT = 10 -export enum ADMIN_ROLES_ENUM { - ADMINISTRATOR = 'administrator', - ADMIN = 'admin', -} - -export const ADMIN_ROLES = [ADMIN_ROLES_ENUM.ADMINISTRATOR, ADMIN_ROLES_ENUM.ADMIN] +export const ADMIN_ROLES = [UserRole.administrator] diff --git a/src/apps/profiles/src/lib/helpers.ts b/src/apps/profiles/src/lib/helpers.ts index 4be3c6825..4b5c959a1 100644 --- a/src/apps/profiles/src/lib/helpers.ts +++ b/src/apps/profiles/src/lib/helpers.ts @@ -1,6 +1,6 @@ /* eslint-disable complexity */ -import { UserProfile } from '~/libs/core' -import { ADMIN_ROLES, ADMIN_ROLES_ENUM } from '../config' +import { UserProfile, UserRole } from '~/libs/core' +import { ADMIN_ROLES } from '../config' declare global { interface Window { tcUniNav: any } @@ -144,7 +144,7 @@ export function canDownloadProfile(authProfile: UserProfile | undefined, profile } // Check if user has admin roles - if (authProfile.roles?.some(role => ADMIN_ROLES.includes(role.toLowerCase() as ADMIN_ROLES_ENUM))) { + if (authProfile.roles?.some(role => ADMIN_ROLES.includes(role.toLowerCase() as UserRole))) { return true } From b90b66f4b107574477d1b2a74044e4a6c34ae3f3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 15 Jan 2026 18:35:08 +0530 Subject: [PATCH 16/18] fix: review comments --- .../work-expirence/WorkExpirence.tsx | 98 ++++++++----------- .../standard-skills.service.ts | 44 ++++++++- 2 files changed, 80 insertions(+), 62 deletions(-) diff --git a/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx index 717c68667..e5a943d52 100644 --- a/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx +++ b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx @@ -1,8 +1,8 @@ -import { Dispatch, FC, SetStateAction, useEffect, useMemo, useRef, useState } from 'react' +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { MemberTraitsAPI, useMemberTraits, UserProfile, UserTrait, UserTraitIds } from '~/libs/core' -import { fetchSkillsByIds } from '~/libs/shared/lib/services/standard-skills' +import { useSkillsByIds } from '~/libs/shared/lib/services/standard-skills' import { EDIT_MODE_QUERY_PARAM, profileEditModes } from '../../config' import { AddButton, EditMemberPropertyBtn, EmptySection } from '../../components' @@ -32,75 +32,57 @@ const WorkExpirence: FC = (props: WorkExpirenceProps) => { const workExpirence: UserTrait[] | undefined = useMemo(() => memberWorkExpirenceTraits?.[0]?.traits?.data, [memberWorkExpirenceTraits]) - const [skillNamesMap, setSkillNamesMap] = useState>({}) - const [loadingSkills, setLoadingSkills] = useState(false) - const fetchedSkillIdsRef = useRef>(new Set()) - - useEffect(() => { + // Collect all unique skill IDs from work experience entries + const allSkillIds = useMemo(() => { if (!workExpirence) { - setLoadingSkills(false) - return + return [] } - const allSkillIds = new Set() + const skillIdsSet = new Set() workExpirence.forEach((work: UserTrait) => { if (work.associatedSkills && Array.isArray(work.associatedSkills)) { work.associatedSkills.forEach((skillId: string) => { if (skillId && typeof skillId === 'string') { - allSkillIds.add(skillId) + skillIdsSet.add(skillId) } }) } }) - if (allSkillIds.size > 0) { - const skillIdsToFetch = Array.from(allSkillIds) - .filter(id => !fetchedSkillIdsRef.current.has(id)) - - if (skillIdsToFetch.length > 0) { - setLoadingSkills(true) - skillIdsToFetch.forEach(id => fetchedSkillIdsRef.current.add(id)) - - fetchSkillsByIds(skillIdsToFetch) - .then(skills => { - setSkillNamesMap(prevMap => { - const newMap: Record = { ...prevMap } - skills.forEach(skill => { - if (skill.id && skill.name) { - newMap[skill.id] = skill.name - } - }) - skillIdsToFetch.forEach(skillId => { - if (!newMap[skillId]) { - newMap[skillId] = skillId - } - }) - return newMap - }) - }) - .catch(() => { - setSkillNamesMap(prevMap => { - const fallbackMap: Record = { ...prevMap } - skillIdsToFetch.forEach(skillId => { - if (!fallbackMap[skillId]) { - fallbackMap[skillId] = skillId - } - }) - return fallbackMap - }) - }) - .finally(() => { - setLoadingSkills(false) - }) - } else { - setLoadingSkills(false) - } - } else { - setLoadingSkills(false) - } + return Array.from(skillIdsSet) }, [workExpirence]) - const areSkillsLoaded = (work: UserTrait): boolean => { + // Fetch skills using SWR hook + const { data: fetchedSkills, error: skillsError } = useSkillsByIds( + allSkillIds.length > 0 ? allSkillIds : undefined, + ) + + // Determine loading state: data is undefined and no error yet + const loadingSkills = fetchedSkills === undefined && !skillsError + + // Build skill names map from fetched skills + const skillNamesMap = useMemo(() => { + const map: Record = {} + + if (fetchedSkills) { + fetchedSkills.forEach(skill => { + if (skill.id && skill.name) { + map[skill.id] = skill.name + } + }) + } + + // For skills that weren't found, use ID as fallback + allSkillIds.forEach(skillId => { + if (!map[skillId]) { + map[skillId] = skillId + } + }) + + return map + }, [fetchedSkills, allSkillIds]) + + const areSkillsLoaded = useCallback((work: UserTrait): boolean => { if (!work.associatedSkills || !Array.isArray(work.associatedSkills) || work.associatedSkills.length === 0) { return true } @@ -109,7 +91,7 @@ const WorkExpirence: FC = (props: WorkExpirenceProps) => { const skillName = skillNamesMap[skillId] return skillName && skillName !== skillId }) - } + }, [skillNamesMap]) useEffect(() => { if (props.authProfile && editMode === profileEditModes.workExperience) { diff --git a/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts b/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts index bad3ebea7..603ad7ac2 100644 --- a/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts +++ b/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts @@ -1,3 +1,7 @@ +import { useMemo } from 'react' +import { SWRResponse } from 'swr' +import useSWR from 'swr' + import { EnvironmentConfig } from '~/config' import { UserSkill, xhrGetAsync, xhrPostAsync, xhrPutAsync } from '~/libs/core' @@ -45,21 +49,53 @@ export async function updateMemberSkills( } /** - * Fetch skills by their IDs + * Fetcher function for useSWR to fetch skills by their IDs * @param skillIds Array of skill UUIDs * @returns Promise with array of UserSkill objects */ -export async function fetchSkillsByIds(skillIds: string[]): Promise { +async function fetchSkillsByIdsFetcher(skillIds: string[]): Promise { if (!skillIds || skillIds.length === 0) { - return Promise.resolve([]) + return [] } try { const skillPromises = skillIds.map(skillId => xhrGetAsync(`${baseUrl}/skills/${skillId}`) .catch(() => undefined)) const results = await Promise.all(skillPromises) - return results.filter((skill): skill is UserSkill => (skill !== null || skill !== undefined)) + return results.filter((skill): skill is UserSkill => skill !== null && skill !== undefined) } catch { return [] } } + +/** + * Hook to fetch skills by their IDs using SWR + * @param skillIds Array of skill UUIDs + * @returns SWRResponse with array of UserSkill objects + */ +export function useSkillsByIds(skillIds: string[] | undefined): SWRResponse { + const swrKey = useMemo(() => { + if (!skillIds || skillIds.length === 0) { + return null + } + return ['skills-by-ids', [...skillIds].sort().join(',')] + }, [skillIds]) + + return useSWR( + swrKey, + () => fetchSkillsByIdsFetcher(skillIds!), + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + }, + ) +} + +/** + * Fetch skills by their IDs (legacy async function for backward compatibility) + * @param skillIds Array of skill UUIDs + * @returns Promise with array of UserSkill objects + */ +export async function fetchSkillsByIds(skillIds: string[]): Promise { + return fetchSkillsByIdsFetcher(skillIds) +} From 64edac4aa1bc07f2a80243f62638a1b09f550e7e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 15 Jan 2026 18:41:49 +0530 Subject: [PATCH 17/18] fix: lint --- .../src/member-profile/work-expirence/WorkExpirence.tsx | 6 +++--- .../services/standard-skills/standard-skills.service.ts | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx index e5a943d52..1f2e6730b 100644 --- a/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx +++ b/src/apps/profiles/src/member-profile/work-expirence/WorkExpirence.tsx @@ -1,7 +1,8 @@ import { Dispatch, FC, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' +import { SWRResponse } from 'swr' -import { MemberTraitsAPI, useMemberTraits, UserProfile, UserTrait, UserTraitIds } from '~/libs/core' +import { MemberTraitsAPI, useMemberTraits, UserProfile, UserSkill, UserTrait, UserTraitIds } from '~/libs/core' import { useSkillsByIds } from '~/libs/shared/lib/services/standard-skills' import { EDIT_MODE_QUERY_PARAM, profileEditModes } from '../../config' @@ -52,8 +53,7 @@ const WorkExpirence: FC = (props: WorkExpirenceProps) => { return Array.from(skillIdsSet) }, [workExpirence]) - // Fetch skills using SWR hook - const { data: fetchedSkills, error: skillsError } = useSkillsByIds( + const { data: fetchedSkills, error: skillsError }: SWRResponse = useSkillsByIds( allSkillIds.length > 0 ? allSkillIds : undefined, ) diff --git a/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts b/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts index 603ad7ac2..346261290 100644 --- a/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts +++ b/src/libs/shared/lib/services/standard-skills/standard-skills.service.ts @@ -1,6 +1,5 @@ import { useMemo } from 'react' -import { SWRResponse } from 'swr' -import useSWR from 'swr' +import useSWR, { SWRResponse } from 'swr' import { EnvironmentConfig } from '~/config' import { UserSkill, xhrGetAsync, xhrPostAsync, xhrPutAsync } from '~/libs/core' @@ -76,9 +75,11 @@ async function fetchSkillsByIdsFetcher(skillIds: string[]): Promise export function useSkillsByIds(skillIds: string[] | undefined): SWRResponse { const swrKey = useMemo(() => { if (!skillIds || skillIds.length === 0) { - return null + return undefined } - return ['skills-by-ids', [...skillIds].sort().join(',')] + + return ['skills-by-ids', [...skillIds].sort() + .join(',')] }, [skillIds]) return useSWR( From afbe389036fba6beeda041425fee5dc454f48a0b Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 15 Jan 2026 18:45:55 +0530 Subject: [PATCH 18/18] fix: lint --- src/apps/profiles/src/config/constants.ts | 2 +- src/apps/profiles/src/lib/helpers.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apps/profiles/src/config/constants.ts b/src/apps/profiles/src/config/constants.ts index ddf1b7f1b..544b1efa4 100644 --- a/src/apps/profiles/src/config/constants.ts +++ b/src/apps/profiles/src/config/constants.ts @@ -1,6 +1,6 @@ // import { EnvironmentConfig } from '~/config' -import { UserRole } from "~/libs/core" +import { UserRole } from '~/libs/core' export enum TRACKS_PROFILE_MAP { DEVELOP = 'Developer', diff --git a/src/apps/profiles/src/lib/helpers.ts b/src/apps/profiles/src/lib/helpers.ts index 4b5c959a1..0f39cb702 100644 --- a/src/apps/profiles/src/lib/helpers.ts +++ b/src/apps/profiles/src/lib/helpers.ts @@ -1,5 +1,6 @@ /* eslint-disable complexity */ import { UserProfile, UserRole } from '~/libs/core' + import { ADMIN_ROLES } from '../config' declare global {