From 9dfaad6d99d56e00d6c4ef93baabe740c10f69ae Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 5 Feb 2026 12:39:18 +0530 Subject: [PATCH 1/9] PM-3688 Fix onboarding bio-title payload --- .../src/hooks/useAutoSavePersonalization.ts | 43 ++++++++----------- .../src/models/PersonalizationInfo.ts | 31 ++++++------- .../src/pages/personalization/index.tsx | 23 +++++++--- .../profile-header/ProfileHeader.tsx | 5 ++- src/libs/shared/lib/utils/methods.ts | 16 +++---- 5 files changed, 57 insertions(+), 61 deletions(-) diff --git a/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts b/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts index ffb0d525c..71f0cbc1a 100644 --- a/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts +++ b/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts @@ -1,6 +1,8 @@ -import { Dispatch, MutableRefObject, SetStateAction, useEffect, useMemo, useState } from 'react' +import { Dispatch, MutableRefObject, SetStateAction, useEffect, useState } from 'react' import _ from 'lodash' +import { updateOrCreateMemberTraitsAsync, UserTraitCategoryNames, UserTraitIds } from '~/libs/core' + import PersonalizationInfo from '../models/PersonalizationInfo' export interface useAutoSavePersonalizationType { @@ -10,45 +12,34 @@ export interface useAutoSavePersonalizationType { } type useAutoSavePersonalizationFunctionType = ( - reduxPersonalizations: PersonalizationInfo[] | undefined, - savingFields: string[], + reduxPersonalization: PersonalizationInfo | undefined, updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void, createMemberPersonalizations: (infos: PersonalizationInfo[]) => void, shouldSavingData: MutableRefObject, + profileHandle: string | undefined, ) => useAutoSavePersonalizationType export const useAutoSavePersonalization: useAutoSavePersonalizationFunctionType = ( - reduxPersonalizations: PersonalizationInfo[] | undefined, - savingFields: string[], - updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void, - createMemberPersonalizations: (infos: PersonalizationInfo[]) => void, + reduxPersonalization: PersonalizationInfo | undefined, + updateMemberPersonalizations: (infos: any[]) => void, + createMemberPersonalizations: (infos: any[]) => void, shouldSavingData: MutableRefObject, + profileHandle: string | undefined, ) => { const [loading, setLoading] = useState(false) const [personalizationInfo, setPersonalizationInfo] = useState(undefined) - const reduxPersonalization = useMemo(() => (reduxPersonalizations || []).find( - (trait: any) => _.some(savingFields, (savingField: string) => trait[savingField] !== undefined), - ), [reduxPersonalizations, savingFields]) const saveData: any = async () => { - if (!personalizationInfo) { - return - } - - const datas: PersonalizationInfo[] = [ - ..._.reject( - reduxPersonalizations || [], - (trait: any) => _.some(savingFields, (savingField: string) => trait[savingField] !== undefined), - ), - personalizationInfo, - ] + if (!personalizationInfo) return setLoading(true) - if (!reduxPersonalization) { - await createMemberPersonalizations(datas) - } else { - await updateMemberPersonalizations(datas) - } + updateOrCreateMemberTraitsAsync(profileHandle || '', [{ + categoryName: UserTraitCategoryNames.personalization, + traitId: UserTraitIds.personalization, + traits: { + data: personalizationInfo, + }, + }]) setLoading(false) } diff --git a/src/apps/onboarding/src/models/PersonalizationInfo.ts b/src/apps/onboarding/src/models/PersonalizationInfo.ts index e76a5aea4..c3a9216f2 100644 --- a/src/apps/onboarding/src/models/PersonalizationInfo.ts +++ b/src/apps/onboarding/src/models/PersonalizationInfo.ts @@ -1,19 +1,16 @@ -export default interface PersonalizationInfo { - referAs?: string - profileSelfTitle?: string - shortBio?: string - openToWork?: { - availability?: string, - preferredRoles?: string[], - } +export interface OpenToWorkTrait { + availability?: string + preferredRoles?: string[] } -export const emptyPersonalizationInfo: () => PersonalizationInfo = () => ({ - openToWork: { - availability: '', - preferredRoles: [], - }, - profileSelfTitle: '', - referAs: '', - shortBio: '', -}) +export type PersonalizationTrait = + | { profileSelfTitle: string } + | { shortBio: string } + | { referAs: string } + | { openToWork: OpenToWorkTrait } + | { links: Array<{ url: string; name: string }> } + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export default interface PersonalizationInfo extends Array {} + +export const emptyPersonalizationInfo = (): PersonalizationInfo => [] diff --git a/src/apps/onboarding/src/pages/personalization/index.tsx b/src/apps/onboarding/src/pages/personalization/index.tsx index af2392067..5de2c4f29 100644 --- a/src/apps/onboarding/src/pages/personalization/index.tsx +++ b/src/apps/onboarding/src/pages/personalization/index.tsx @@ -5,6 +5,7 @@ import classNames from 'classnames' import { Button, IconOutline, PageDivider } from '~/libs/ui' import { EnvironmentConfig } from '~/config' +import { upsertTrait } from '~/libs/shared' import { ProgressBar } from '../../components/progress-bar' import { @@ -37,7 +38,7 @@ import styles from './styles.module.scss' interface PagePersonalizationContentReduxProps { memberInfo?: MemberInfo, - reduxPersonalizations: PersonalizationInfo[] | undefined + reduxPersonalizations: PersonalizationInfo | undefined reduxEducations: EducationInfo[] | undefined reduxWorks: WorkInfo[] | undefined reduxOnboardingChecklist: OnboardingChecklistInfo | undefined @@ -69,10 +70,10 @@ const PagePersonalizationContent: FC = props => setPersonalizationInfo, }: useAutoSavePersonalizationType = useAutoSavePersonalization( props.reduxPersonalizations, - ['profileSelfTitle'], props.updateMemberPersonalizations, props.createMemberPersonalizations, shouldSavingData, + props.memberInfo?.handle, ) const { @@ -85,6 +86,13 @@ const PagePersonalizationContent: FC = props => shouldSavingMemberData, ) + const profileSelfTitle + = personalizationInfo + ?.find( + (t): t is { profileSelfTitle: string } => 'profileSelfTitle' in t, + ) + ?.profileSelfTitle ?? '' + useEffect(() => { if ( !loading @@ -129,12 +137,13 @@ const PagePersonalizationContent: FC = props => upsertTrait( + 'profileSelfTitle', + value || '', + prev ?? props.reduxPersonalizations ?? [], + )) }} placeholder='Ex: I’m a creative rockstar' tabIndex={0} diff --git a/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx b/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx index bc231d89a..a59aea165 100644 --- a/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx +++ b/src/apps/profiles/src/member-profile/profile-header/ProfileHeader.tsx @@ -188,7 +188,10 @@ const ProfileHeader: FC = (props: ProfileHeaderProps) => { function renderOpenToWorkSummary(): JSX.Element { - if (!hasOpenToWork || !props.profile.availableForGigs) return <> + if ( + !hasOpenToWork + || !props.profile.availableForGigs + || (openToWorkItem.preferredRoles?.length === 0 && openToWorkItem.availability === undefined)) return <> const availabilityLabel = getAvailabilityLabel(openToWorkItem.availability) const roleLabels = getPreferredRoleLabels(openToWorkItem.preferredRoles) diff --git a/src/libs/shared/lib/utils/methods.ts b/src/libs/shared/lib/utils/methods.ts index 83ab6946f..9d2c40884 100644 --- a/src/libs/shared/lib/utils/methods.ts +++ b/src/libs/shared/lib/utils/methods.ts @@ -1,12 +1,8 @@ -import { UserTrait } from '~/libs/core' - -export function upsertTrait( - key: string, +export function upsertTrait( + key: keyof T | string, value: any, - data: UserTrait[] = [], -): UserTrait[] { - return [ - ...data.filter(trait => !trait[key]), - { [key]: value }, - ] + data: T[] = [], +): T[] { + const filtered = data.filter(item => !(key in item)) + return [...filtered, { [key]: value } as T] } From 70a3488289a1415d0a24193cd045ad5d47625291 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 5 Feb 2026 13:22:31 +0530 Subject: [PATCH 2/9] clean up code --- .../src/hooks/useAutoSavePersonalization.ts | 24 +++++++++---------- .../src/pages/personalization/index.tsx | 4 ---- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts b/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts index 71f0cbc1a..fea09293a 100644 --- a/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts +++ b/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts @@ -13,16 +13,12 @@ export interface useAutoSavePersonalizationType { type useAutoSavePersonalizationFunctionType = ( reduxPersonalization: PersonalizationInfo | undefined, - updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void, - createMemberPersonalizations: (infos: PersonalizationInfo[]) => void, shouldSavingData: MutableRefObject, profileHandle: string | undefined, ) => useAutoSavePersonalizationType export const useAutoSavePersonalization: useAutoSavePersonalizationFunctionType = ( reduxPersonalization: PersonalizationInfo | undefined, - updateMemberPersonalizations: (infos: any[]) => void, - createMemberPersonalizations: (infos: any[]) => void, shouldSavingData: MutableRefObject, profileHandle: string | undefined, ) => { @@ -33,15 +29,17 @@ export const useAutoSavePersonalization: useAutoSavePersonalizationFunctionType if (!personalizationInfo) return setLoading(true) - updateOrCreateMemberTraitsAsync(profileHandle || '', [{ - categoryName: UserTraitCategoryNames.personalization, - traitId: UserTraitIds.personalization, - traits: { - data: personalizationInfo, - }, - }]) - - setLoading(false) + try { + await updateOrCreateMemberTraitsAsync(profileHandle || '', [{ + categoryName: UserTraitCategoryNames.personalization, + traitId: UserTraitIds.personalization, + traits: { + data: personalizationInfo, + }, + }]) + } finally { + setLoading(false) + } } useEffect(() => { diff --git a/src/apps/onboarding/src/pages/personalization/index.tsx b/src/apps/onboarding/src/pages/personalization/index.tsx index 5de2c4f29..d9e8efc58 100644 --- a/src/apps/onboarding/src/pages/personalization/index.tsx +++ b/src/apps/onboarding/src/pages/personalization/index.tsx @@ -47,8 +47,6 @@ interface PagePersonalizationContentReduxProps { } interface PagePersonalizationContentProps extends PagePersonalizationContentReduxProps { - updateMemberPersonalizations: (infos: PersonalizationInfo[]) => void - createMemberPersonalizations: (infos: PersonalizationInfo[]) => void setMemberPhotoUrl: (photoUrl: string) => void updateMemberPhotoUrl: (photoUrl: string) => void updateMemberDescription: (photoUrl: string) => void @@ -70,8 +68,6 @@ const PagePersonalizationContent: FC = props => setPersonalizationInfo, }: useAutoSavePersonalizationType = useAutoSavePersonalization( props.reduxPersonalizations, - props.updateMemberPersonalizations, - props.createMemberPersonalizations, shouldSavingData, props.memberInfo?.handle, ) From c543789a833d58b8d5c038e5e8728831a6111478 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 5 Feb 2026 16:03:03 +0530 Subject: [PATCH 3/9] Revert to match api response --- .../src/hooks/useAutoSavePersonalization.ts | 13 +++++--- .../src/models/PersonalizationInfo.ts | 30 ++++++++++++------- .../src/pages/open-to-work/index.tsx | 20 +++++-------- .../src/pages/personalization/index.tsx | 19 ++++-------- .../ModifyAboutMeModal/ModifyAboutMeModal.tsx | 16 ++++++---- .../ModifyMemberLinksModal.tsx | 14 +++++---- .../OpenForGigsModifyModal.tsx | 21 +++++++------ 7 files changed, 71 insertions(+), 62 deletions(-) diff --git a/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts b/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts index fea09293a..c52faf114 100644 --- a/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts +++ b/src/apps/onboarding/src/hooks/useAutoSavePersonalization.ts @@ -1,4 +1,4 @@ -import { Dispatch, MutableRefObject, SetStateAction, useEffect, useState } from 'react' +import { Dispatch, MutableRefObject, SetStateAction, useEffect, useMemo, useState } from 'react' import _ from 'lodash' import { updateOrCreateMemberTraitsAsync, UserTraitCategoryNames, UserTraitIds } from '~/libs/core' @@ -12,19 +12,24 @@ export interface useAutoSavePersonalizationType { } type useAutoSavePersonalizationFunctionType = ( - reduxPersonalization: PersonalizationInfo | undefined, + reduxPersonalizations: PersonalizationInfo[] | undefined, shouldSavingData: MutableRefObject, profileHandle: string | undefined, ) => useAutoSavePersonalizationType export const useAutoSavePersonalization: useAutoSavePersonalizationFunctionType = ( - reduxPersonalization: PersonalizationInfo | undefined, + reduxPersonalizations: PersonalizationInfo[] | undefined, shouldSavingData: MutableRefObject, profileHandle: string | undefined, ) => { const [loading, setLoading] = useState(false) const [personalizationInfo, setPersonalizationInfo] = useState(undefined) + const reduxPersonalization = useMemo( + () => reduxPersonalizations?.[0], + [reduxPersonalizations], + ) + const saveData: any = async () => { if (!personalizationInfo) return @@ -34,7 +39,7 @@ export const useAutoSavePersonalization: useAutoSavePersonalizationFunctionType categoryName: UserTraitCategoryNames.personalization, traitId: UserTraitIds.personalization, traits: { - data: personalizationInfo, + data: [personalizationInfo], }, }]) } finally { diff --git a/src/apps/onboarding/src/models/PersonalizationInfo.ts b/src/apps/onboarding/src/models/PersonalizationInfo.ts index c3a9216f2..ee6ac4adb 100644 --- a/src/apps/onboarding/src/models/PersonalizationInfo.ts +++ b/src/apps/onboarding/src/models/PersonalizationInfo.ts @@ -3,14 +3,24 @@ export interface OpenToWorkTrait { preferredRoles?: string[] } -export type PersonalizationTrait = - | { profileSelfTitle: string } - | { shortBio: string } - | { referAs: string } - | { openToWork: OpenToWorkTrait } - | { links: Array<{ url: string; name: string }> } - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export default interface PersonalizationInfo extends Array {} +export default interface PersonalizationInfo { + referAs?: string + profileSelfTitle?: string + shortBio?: string + links?: Array<{ url: string; name: string }> + openToWork?: { + availability?: string, + preferredRoles?: string[], + } +} -export const emptyPersonalizationInfo = (): PersonalizationInfo => [] +export const emptyPersonalizationInfo: () => PersonalizationInfo = () => ({ + links: [], + openToWork: { + availability: '', + preferredRoles: [], + }, + profileSelfTitle: '', + referAs: '', + shortBio: '', +}) diff --git a/src/apps/onboarding/src/pages/open-to-work/index.tsx b/src/apps/onboarding/src/pages/open-to-work/index.tsx index 0835c62bc..02debd730 100644 --- a/src/apps/onboarding/src/pages/open-to-work/index.tsx +++ b/src/apps/onboarding/src/pages/open-to-work/index.tsx @@ -7,12 +7,10 @@ import classNames from 'classnames' import { Button, IconOutline, PageDivider } from '~/libs/ui' import { updateOrCreateMemberTraitsAsync, useMemberTraits, - UserTrait, UserTraitCategoryNames, UserTraitIds, UserTraits } from '~/libs/core' import { OpenToWorkData } from '~/libs/shared/lib/components/modify-open-to-work-modal' -import { upsertTrait } from '~/libs/shared' import OpenToWorkForm from '~/libs/shared/lib/components/modify-open-to-work-modal/ModifyOpenToWorkModal' import { ProgressBar } from '../../components/progress-bar' @@ -48,11 +46,9 @@ export const PageOpenToWorkContent: FC = props => { useEffect(() => { if (!memberPersonalizationTraits) return - const personalizationData = memberPersonalizationTraits?.[0]?.traits?.data || [] + const personalizationData = memberPersonalizationTraits?.[0]?.traits?.data?.[0] || {} - const openToWorkItem = personalizationData.find( - (item: UserTrait) => item?.openToWork, - )?.openToWork ?? {} + const openToWorkItem = personalizationData.openToWork || {} setFormValue(prev => ({ ...prev, @@ -76,16 +72,16 @@ export const PageOpenToWorkContent: FC = props => { async function goToNextStep(): Promise { setLoading(true) - const existingData = memberPersonalizationTraits?.[0]?.traits?.data || [] + const existing = memberPersonalizationTraits?.[0]?.traits?.data?.[0] || {} - const personalizationData = upsertTrait( - 'openToWork', - { + const personalizationData = [{ + ...existing, + openToWork: { + ...(existing.openToWork || {}), availability: formValue.availability, preferredRoles: formValue.preferredRoles, }, - existingData, - ) + }] try { await Promise.all([ diff --git a/src/apps/onboarding/src/pages/personalization/index.tsx b/src/apps/onboarding/src/pages/personalization/index.tsx index d9e8efc58..199e83e9d 100644 --- a/src/apps/onboarding/src/pages/personalization/index.tsx +++ b/src/apps/onboarding/src/pages/personalization/index.tsx @@ -5,7 +5,6 @@ import classNames from 'classnames' import { Button, IconOutline, PageDivider } from '~/libs/ui' import { EnvironmentConfig } from '~/config' -import { upsertTrait } from '~/libs/shared' import { ProgressBar } from '../../components/progress-bar' import { @@ -38,7 +37,7 @@ import styles from './styles.module.scss' interface PagePersonalizationContentReduxProps { memberInfo?: MemberInfo, - reduxPersonalizations: PersonalizationInfo | undefined + reduxPersonalizations: PersonalizationInfo[] | undefined reduxEducations: EducationInfo[] | undefined reduxWorks: WorkInfo[] | undefined reduxOnboardingChecklist: OnboardingChecklistInfo | undefined @@ -82,12 +81,7 @@ const PagePersonalizationContent: FC = props => shouldSavingMemberData, ) - const profileSelfTitle - = personalizationInfo - ?.find( - (t): t is { profileSelfTitle: string } => 'profileSelfTitle' in t, - ) - ?.profileSelfTitle ?? '' + const profileSelfTitle = personalizationInfo?.profileSelfTitle || '' useEffect(() => { if ( @@ -135,11 +129,10 @@ const PagePersonalizationContent: FC = props => label='Bio Title' value={profileSelfTitle} onChange={function onChange(value: string | undefined) { - setPersonalizationInfo(prev => upsertTrait( - 'profileSelfTitle', - value || '', - prev ?? props.reduxPersonalizations ?? [], - )) + setPersonalizationInfo(prev => ({ + ...(prev ?? props.reduxPersonalizations ?? {}), + profileSelfTitle: value || '', + })) }} placeholder='Ex: I’m a creative rockstar' tabIndex={0} diff --git a/src/apps/profiles/src/member-profile/about-me/ModifyAboutMeModal/ModifyAboutMeModal.tsx b/src/apps/profiles/src/member-profile/about-me/ModifyAboutMeModal/ModifyAboutMeModal.tsx index c5064bc30..ab0e8c0a8 100644 --- a/src/apps/profiles/src/member-profile/about-me/ModifyAboutMeModal/ModifyAboutMeModal.tsx +++ b/src/apps/profiles/src/member-profile/about-me/ModifyAboutMeModal/ModifyAboutMeModal.tsx @@ -11,7 +11,6 @@ import { UserTraitCategoryNames, UserTraitIds, } from '~/libs/core' -import { upsertTrait } from '~/libs/shared' import styles from './ModifyAboutMeModal.module.scss' @@ -69,11 +68,16 @@ const ModifyAboutMeModal: FC = (props: ModifyAboutMeMod setIsSaving(true) setFormSaveError(undefined) - const personalizationData = upsertTrait( - 'profileSelfTitle', - updatedTitle, - props.memberPersonalizationTraitsData, - ) + console.log('personalizationData in about me', props.memberPersonalizationTraitsData) + + const existing = props.memberPersonalizationTraitsData?.[0] || {} + + const personalizationData: UserTrait[] = [ + { + ...existing, + profileSelfTitle: updatedTitle, + }, + ] Promise.all([ updateMemberProfileAsync( diff --git a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.tsx b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.tsx index dc49417b1..0a724f57d 100644 --- a/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.tsx +++ b/src/apps/profiles/src/member-profile/links/ModifyMemberLinksModal/ModifyMemberLinksModal.tsx @@ -11,7 +11,6 @@ import { UserTraitCategoryNames, UserTraitIds, } from '~/libs/core' -import { upsertTrait } from '~/libs/shared' import { LinkForm, UserLink } from './LinkForm' import { LinkFormHandle } from './LinkForm/LinkForm' @@ -136,11 +135,14 @@ const ModifyMemberLinksModal: FC = (props: ModifyMe function handleLinksSave(): void { setIsSaving(true) - const personalizationData = upsertTrait( - 'links', - updatedLinks, - props.memberPersonalizationTraitsFullData ?? [], - ) + const existing = props.memberPersonalizationTraitsFullData?.[0] || {} + + const personalizationData: UserTrait[] = [ + { + ...existing, + links: updatedLinks, + }, + ] updateOrCreateMemberTraitsAsync(props.profile.handle, [{ categoryName: UserTraitCategoryNames.personalization, diff --git a/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx b/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx index a3be72a35..bf484fd41 100644 --- a/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx +++ b/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx @@ -13,7 +13,6 @@ import { OpenToWorkData } from '~/libs/shared/lib/components/modify-open-to-work import { updateMemberProfile, } from '~/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store' -import { upsertTrait } from '~/libs/shared/lib/utils/methods' import OpenToWorkForm from '~/libs/shared/lib/components/modify-open-to-work-modal/ModifyOpenToWorkModal' import styles from './OpenForGigsModifyModal.module.scss' @@ -41,11 +40,11 @@ const OpenForGigsModifyModal: FC = (props: OpenForG useEffect(() => { if (!memberPersonalizationTraits) return - const personalizationData = memberPersonalizationTraits?.[0]?.traits?.data || [] + const personalizationData = memberPersonalizationTraits?.[0]?.traits?.data?.[0] || {} - const openToWorkItem = personalizationData.find( - (item: UserTrait) => item?.openToWork, - )?.openToWork ?? {} + console.log('personalizationData in Gigs [correct]', personalizationData) + + const openToWorkItem = personalizationData.openToWork || {} setFormValue(prev => ({ ...prev, @@ -61,16 +60,16 @@ const OpenForGigsModifyModal: FC = (props: OpenForG function handleOpenForWorkSave(): void { setIsSaving(true) - const existingData = memberPersonalizationTraits?.[0]?.traits?.data || [] + const existing = memberPersonalizationTraits?.[0]?.traits?.data?.[0] || {} - const personalizationData = upsertTrait( - 'openToWork', - { + const personalizationData = [{ + ...existing, + openToWork: { + ...(existing.openToWork || {}), availability: formValue.availability, preferredRoles: formValue.preferredRoles, }, - existingData, - ) + }] Promise.all([ // Update availableForGigs in member profile From 986a054364a519cc3ae91dd19f532beeb681d055 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 5 Feb 2026 16:23:42 +0530 Subject: [PATCH 4/9] clean up unused function --- src/libs/shared/lib/utils/index.ts | 1 - src/libs/shared/lib/utils/methods.ts | 8 -------- 2 files changed, 9 deletions(-) delete mode 100644 src/libs/shared/lib/utils/methods.ts diff --git a/src/libs/shared/lib/utils/index.ts b/src/libs/shared/lib/utils/index.ts index 22450e6e2..f579b0f94 100644 --- a/src/libs/shared/lib/utils/index.ts +++ b/src/libs/shared/lib/utils/index.ts @@ -5,4 +5,3 @@ export * from './handle-error' export * from './industry' export * from './string' export * from './text-format' -export * from './methods' diff --git a/src/libs/shared/lib/utils/methods.ts b/src/libs/shared/lib/utils/methods.ts deleted file mode 100644 index 9d2c40884..000000000 --- a/src/libs/shared/lib/utils/methods.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function upsertTrait( - key: keyof T | string, - value: any, - data: T[] = [], -): T[] { - const filtered = data.filter(item => !(key in item)) - return [...filtered, { [key]: value } as T] -} From 9f755e791d1002e6212377a8fcddbe5ee674f858 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 5 Feb 2026 16:29:39 +0530 Subject: [PATCH 5/9] clean console logs --- .../about-me/ModifyAboutMeModal/ModifyAboutMeModal.tsx | 2 -- .../OpenForGigsModifyModal/OpenForGigsModifyModal.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/apps/profiles/src/member-profile/about-me/ModifyAboutMeModal/ModifyAboutMeModal.tsx b/src/apps/profiles/src/member-profile/about-me/ModifyAboutMeModal/ModifyAboutMeModal.tsx index ab0e8c0a8..54120e964 100644 --- a/src/apps/profiles/src/member-profile/about-me/ModifyAboutMeModal/ModifyAboutMeModal.tsx +++ b/src/apps/profiles/src/member-profile/about-me/ModifyAboutMeModal/ModifyAboutMeModal.tsx @@ -68,8 +68,6 @@ const ModifyAboutMeModal: FC = (props: ModifyAboutMeMod setIsSaving(true) setFormSaveError(undefined) - console.log('personalizationData in about me', props.memberPersonalizationTraitsData) - const existing = props.memberPersonalizationTraitsData?.[0] || {} const personalizationData: UserTrait[] = [ diff --git a/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx b/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx index bf484fd41..d185ef003 100644 --- a/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx +++ b/src/apps/profiles/src/member-profile/profile-header/OpenForGigsModifyModal/OpenForGigsModifyModal.tsx @@ -42,8 +42,6 @@ const OpenForGigsModifyModal: FC = (props: OpenForG const personalizationData = memberPersonalizationTraits?.[0]?.traits?.data?.[0] || {} - console.log('personalizationData in Gigs [correct]', personalizationData) - const openToWorkItem = personalizationData.openToWork || {} setFormValue(prev => ({ From b1fcf96dfb864d5f668810b439a5291cedbf88d2 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 6 Feb 2026 12:13:41 +1100 Subject: [PATCH 6/9] Better challenge details page to system-admin app (https://topcoder.atlassian.net/browse/PM-3264) --- src/apps/admin/src/admin-app.routes.tsx | 9 + .../ChallengeDetailsPage.module.scss | 87 ++++ .../ChallengeDetailsPage.tsx | 449 ++++++++++++++++++ .../ChallengeDetailsPage/index.ts | 1 + .../ChallengeList/ChallengeList.tsx | 14 +- .../models/challenge-management/Challenge.ts | 32 ++ .../services/challenge-management.service.ts | 22 + 7 files changed, 607 insertions(+), 7 deletions(-) create mode 100644 src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.module.scss create mode 100644 src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.tsx create mode 100644 src/apps/admin/src/challenge-management/ChallengeDetailsPage/index.ts diff --git a/src/apps/admin/src/admin-app.routes.tsx b/src/apps/admin/src/admin-app.routes.tsx index b5db82d55..606e40794 100644 --- a/src/apps/admin/src/admin-app.routes.tsx +++ b/src/apps/admin/src/admin-app.routes.tsx @@ -33,6 +33,10 @@ const ChallengeManagementPage: LazyLoadedComponent = lazyLoad( () => import('./challenge-management/ChallengeManagementPage'), 'ChallengeManagementPage', ) +const ChallengeDetailsPage: LazyLoadedComponent = lazyLoad( + () => import('./challenge-management/ChallengeDetailsPage'), + 'ChallengeDetailsPage', +) const ManageUserPage: LazyLoadedComponent = lazyLoad( () => import('./challenge-management/ManageUserPage'), 'ManageUserPage', @@ -193,6 +197,11 @@ export const adminRoutes: ReadonlyArray = [ id: 'challenge-management-page', route: '', }, + { + element: , + id: 'challenge-details-page', + route: ':challengeId', + }, { element: , id: 'manage-user', diff --git a/src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.module.scss b/src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.module.scss new file mode 100644 index 000000000..a94e0ce92 --- /dev/null +++ b/src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.module.scss @@ -0,0 +1,87 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; + gap: $sp-6; +} + +.headerButtons { + display: flex; + flex-wrap: wrap; + gap: $sp-3; + justify-content: flex-end; +} + +.section { + display: flex; + flex-direction: column; + gap: $sp-3; +} + +.sectionTitle { + margin: 0; + font-size: 20px; + line-height: 28px; + font-weight: 600; +} + +.statusRow { + display: flex; + align-items: flex-end; + flex-wrap: wrap; + gap: $sp-3; +} + +.winnersList { + display: flex; + flex-direction: column; + gap: $sp-3; +} + +.winnerRow { + display: flex; + align-items: flex-end; + flex-wrap: wrap; + gap: $sp-3; +} + +.selectWrapper { + min-width: 360px; + + :global(.input-el) { + margin-bottom: 0; + } +} + +.sectionActions { + display: flex; +} + +.apiResponse { + margin: 0; + max-height: 480px; + overflow: auto; + padding: $sp-4; + border: 1px solid #ddd; + border-radius: $sp-1; + background-color: #f7f7f7; + font-family: 'Courier New', Courier, monospace; + font-size: 12px; + line-height: 18px; +} + +.loadingSpinnerContainer { + position: relative; + height: 100px; + margin-top: -30px; + + .spinner { + background: none; + } +} + +.noData { + margin: 0; + color: #555; +} diff --git a/src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.tsx b/src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.tsx new file mode 100644 index 000000000..c160e3818 --- /dev/null +++ b/src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.tsx @@ -0,0 +1,449 @@ +import { + ChangeEvent, + FC, + useEffect, + useMemo, + useState, +} from 'react' +import { useLocation, useParams } from 'react-router-dom' +import { toast } from 'react-toastify' + +import { + Button, + InputSelect, + InputSelectOption, + LinkButton, + LoadingSpinner, +} from '~/libs/ui' + +import { rootRoute } from '../../config/routes.config' +import { FieldHandleSelect, PageWrapper } from '../../lib/components' +import { useEventCallback } from '../../lib/hooks' +import { + Challenge, + ChallengeFilterCriteria, + ChallengePrizeSet, + ChallengeWinner, + SelectOption, +} from '../../lib/models' +import { getChallengeById, updateChallengeById } from '../../lib/services' +import { createChallengeQueryString, handleError } from '../../lib/utils' + +import styles from './ChallengeDetailsPage.module.scss' + +const CHALLENGE_STATUS_OPTIONS: string[] = [ + 'NEW', + 'DRAFT', + 'APPROVED', + 'ACTIVE', + 'COMPLETED', + 'DELETED', + 'CANCELLED', + 'CANCELLED_FAILED_REVIEW', + 'CANCELLED_FAILED_SCREENING', + 'CANCELLED_ZERO_SUBMISSIONS', + 'CANCELLED_WINNER_UNRESPONSIVE', + 'CANCELLED_CLIENT_REQUEST', + 'CANCELLED_REQUIREMENTS_INFEASIBLE', + 'CANCELLED_ZERO_REGISTRATIONS', + 'CANCELLED_PAYMENT_FAILED', +] + +type WinnersByPlacement = Record + +type RouteState = { + previousChallengeListFilter?: ChallengeFilterCriteria +} + +type WinnerUpdate = Pick + +function formatStatusLabel(rawStatus: string): string { + const normalized = rawStatus + .trim() + .toUpperCase() + if (normalized.startsWith('CANCELLED_')) { + const reason = normalized + .replace('CANCELLED_', '') + .toLowerCase() + .replace(/_/g, ' ') + .replace(/\b\w/g, char => char.toUpperCase()) + return `Cancelled: ${reason}` + } + + return normalized + .toLowerCase() + .replace(/_/g, ' ') + .replace(/\b\w/g, char => char.toUpperCase()) +} + +function getOrdinal(place: number): string { + if (place % 100 >= 11 && place % 100 <= 13) { + return `${place}th` + } + + switch (place % 10) { + case 1: { + return `${place}st` + } + + case 2: { + return `${place}nd` + } + + case 3: { + return `${place}rd` + } + + default: { + return `${place}th` + } + } +} + +function getPlacementPrizeSet(challenge?: Challenge): ChallengePrizeSet | undefined { + return challenge?.prizeSets?.find( + prizeSet => `${prizeSet.type}`.toUpperCase() === 'PLACEMENT', + ) +} + +function getPlacementCount(challenge?: Challenge): number { + const placementPrizeSet = getPlacementPrizeSet(challenge) + const placementPrizeCount = placementPrizeSet?.prizes?.length ?? 0 + const winnerPlacementCount = Math.max( + 0, + ...(challenge?.winners ?? []).map(winner => winner.placement), + ) + + return Math.max(placementPrizeCount, winnerPlacementCount) +} + +function createWinnersByPlacement(challenge?: Challenge): WinnersByPlacement { + const totalPlacements = getPlacementCount(challenge) + const winnersByPlacement: WinnersByPlacement = {} + const winnerLookup = new Map() + const winners = challenge?.winners ?? [] + + winners.forEach(winner => { + winnerLookup.set(winner.placement, { + label: winner.handle, + value: winner.userId, + }) + }) + + for (let placement = 1; placement <= totalPlacements; placement += 1) { + winnersByPlacement[placement] = winnerLookup.get(placement) + } + + return winnersByPlacement +} + +function createWinnerPayload( + placements: number[], + winnersByPlacement: WinnersByPlacement, +): WinnerUpdate[] { + const payload: WinnerUpdate[] = [] + + placements.forEach(placement => { + const selectedWinner = winnersByPlacement[placement] + if (!selectedWinner) { + return + } + + const userId = Number(selectedWinner.value) + if (!Number.isFinite(userId) || userId <= 0) { + return + } + + payload.push({ + handle: `${selectedWinner.label}`, + placement, + userId, + }) + }) + + return payload +} + +/** + * Challenge details management page. + */ +export const ChallengeDetailsPage: FC = () => { + const { challengeId = '' }: { challengeId?: string } + = useParams<{ challengeId: string }>() + const location = useLocation() + const routeState = (location.state || {}) as RouteState + const [challengeInfo, setChallengeInfo] = useState() + const [selectedStatus, setSelectedStatus] = useState('') + const [winnersByPlacement, setWinnersByPlacement] = useState({}) + const [isLoading, setIsLoading] = useState(false) + const [isSavingStatus, setIsSavingStatus] = useState(false) + const [isSavingWinners, setIsSavingWinners] = useState(false) + + const hydrateChallenge = useEventCallback((challenge: Challenge): void => { + setChallengeInfo(challenge) + setSelectedStatus(challenge.status || '') + setWinnersByPlacement(createWinnersByPlacement(challenge)) + }) + + const loadChallenge = useEventCallback(async () => { + if (!challengeId) { + return + } + + setIsLoading(true) + try { + const challenge = await getChallengeById(challengeId) + hydrateChallenge(challenge) + } catch (error) { + handleError(error) + } finally { + setIsLoading(false) + } + }) + + useEffect(() => { + loadChallenge() + }, [challengeId, loadChallenge]) + + const statusOptions = useMemo(() => { + const values = [...CHALLENGE_STATUS_OPTIONS] + if (challengeInfo?.status && !values.includes(challengeInfo.status)) { + values.push(challengeInfo.status) + } + + return values.map(value => ({ + label: formatStatusLabel(value), + value, + })) + }, [challengeInfo?.status]) + + const placementNumbers = useMemo( + () => Array.from({ length: getPlacementCount(challengeInfo) }, (_, index) => index + 1), + [challengeInfo], + ) + + const backToChallengeListRoute = useMemo(() => { + const previousChallengeListFilter = routeState.previousChallengeListFilter + const query = previousChallengeListFilter + ? `?${createChallengeQueryString(previousChallengeListFilter)}` + : '' + return `${rootRoute}/challenge-management${query}` + }, [routeState.previousChallengeListFilter]) + + const pageTitle = challengeInfo?.name || 'Challenge Details' + + const handleStatusChange = useEventCallback( + (event: ChangeEvent): void => { + setSelectedStatus(event.target.value) + }, + ) + + const handleSaveStatus = useEventCallback(async () => { + if ( + !challengeId + || !challengeInfo + || !selectedStatus + || selectedStatus === challengeInfo.status + ) { + return + } + + setIsSavingStatus(true) + try { + const updatedChallenge = await updateChallengeById(challengeId, { + status: selectedStatus as Challenge['status'], + }) + hydrateChallenge(updatedChallenge) + toast.success('Challenge status updated successfully') + } catch (error) { + handleError(error) + } finally { + setIsSavingStatus(false) + } + }) + + const createHandleWinnerChange = (placement: number) => ( + selectedWinner: SelectOption, + ): void => { + setWinnersByPlacement(previous => ({ + ...previous, + [placement]: selectedWinner, + })) + } + + const createHandleWinnerClear = (placement: number) => (): void => { + setWinnersByPlacement(previous => ({ + ...previous, + [placement]: undefined, + })) + } + + const handleSaveWinners = useEventCallback(async () => { + if (!challengeId || !challengeInfo) { + return + } + + const winnerPayload = createWinnerPayload( + placementNumbers, + winnersByPlacement, + ) + const payload: { + status?: Challenge['status'] + winners: WinnerUpdate[] + } = { + winners: winnerPayload, + } + + if (selectedStatus && selectedStatus !== challengeInfo.status) { + payload.status = selectedStatus as Challenge['status'] + } + + const statusAfterUpdate = payload.status || challengeInfo.status + if (winnerPayload.length > 0 && statusAfterUpdate !== 'COMPLETED') { + toast.error('Set challenge status to COMPLETED before saving winners.') + return + } + + setIsSavingWinners(true) + try { + const updatedChallenge = await updateChallengeById(challengeId, payload) + hydrateChallenge(updatedChallenge) + toast.success('Challenge winners updated successfully') + } catch (error) { + handleError(error) + } finally { + setIsSavingWinners(false) + } + }) + + return ( + + + Manage Users + + + Manage Submissions + + + Back + + + )} + > + {isLoading && ( +
+ +
+ )} + {!isLoading && !challengeInfo && ( +

Unable to load challenge details.

+ )} + {!isLoading && challengeInfo && ( + <> +
+

Status

+
+ + +
+
+ +
+

Winners

+ {placementNumbers.length === 0 && ( +

+ This challenge has no placement prizes configured. +

+ )} + {placementNumbers.length > 0 && ( + <> +
+ {placementNumbers.map(placement => ( +
+ + +
+ ))} +
+
+ +
+ + )} +
+ +
+

Raw API Response

+
+                            {JSON.stringify(challengeInfo, undefined, 2)}
+                        
+
+ + )} +
+ ) +} + +export default ChallengeDetailsPage diff --git a/src/apps/admin/src/challenge-management/ChallengeDetailsPage/index.ts b/src/apps/admin/src/challenge-management/ChallengeDetailsPage/index.ts new file mode 100644 index 000000000..f26dc5e89 --- /dev/null +++ b/src/apps/admin/src/challenge-management/ChallengeDetailsPage/index.ts @@ -0,0 +1 @@ +export { default as ChallengeDetailsPage } from './ChallengeDetailsPage' diff --git a/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx b/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx index f3b3a184d..55df372c2 100644 --- a/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx +++ b/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx @@ -1,5 +1,5 @@ import { Dispatch, FC, SetStateAction, useContext, useMemo, useState } from 'react' -import { useNavigate } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import _ from 'lodash' import cn from 'classnames' import moment from 'moment' @@ -291,15 +291,15 @@ const ChallengeList: FC = props => { label: 'Title', propertyName: 'name', renderer: (challenge: Challenge) => ( - // eslint-disable-next-line jsx-a11y/anchor-is-valid - {challenge.name} - + ), type: 'element', }, diff --git a/src/apps/admin/src/lib/models/challenge-management/Challenge.ts b/src/apps/admin/src/lib/models/challenge-management/Challenge.ts index 2f9f5e786..4f5c913aa 100644 --- a/src/apps/admin/src/lib/models/challenge-management/Challenge.ts +++ b/src/apps/admin/src/lib/models/challenge-management/Challenge.ts @@ -1,8 +1,19 @@ export enum ChallengeStatus { New = 'NEW', Draft = 'DRAFT', + Approved = 'APPROVED', Active = 'ACTIVE', Completed = 'COMPLETED', + Deleted = 'DELETED', + Cancelled = 'CANCELLED', + CancelledFailedReview = 'CANCELLED_FAILED_REVIEW', + CancelledFailedScreening = 'CANCELLED_FAILED_SCREENING', + CancelledZeroSubmissions = 'CANCELLED_ZERO_SUBMISSIONS', + CancelledWinnerUnresponsive = 'CANCELLED_WINNER_UNRESPONSIVE', + CancelledClientRequest = 'CANCELLED_CLIENT_REQUEST', + CancelledRequirementsInfeasible = 'CANCELLED_REQUIREMENTS_INFEASIBLE', + CancelledZeroRegistrations = 'CANCELLED_ZERO_REGISTRATIONS', + CancelledPaymentFailed = 'CANCELLED_PAYMENT_FAILED', } export type ChallengeType = { @@ -17,6 +28,25 @@ export type ChallengeTrack = { abbreviation: string } +export type ChallengeWinner = { + userId: number + handle: string + placement: number + type?: string +} + +export type ChallengePrize = { + description?: string + type: string + value: number +} + +export type ChallengePrizeSet = { + description?: string + type: string + prizes: ChallengePrize[] +} + export interface Challenge { /** Challenge UUID. */ id: string @@ -46,6 +76,8 @@ export interface Challenge { /** Challenge phases. */ phases: Array<{ name: string; isOpen: boolean; scheduledEndDate: string }> tags: Array + winners?: ChallengeWinner[] + prizeSets?: ChallengePrizeSet[] /** Challenge billing info. */ billing?: { billingAccountId?: string | number diff --git a/src/apps/admin/src/lib/services/challenge-management.service.ts b/src/apps/admin/src/lib/services/challenge-management.service.ts index 4c9b4abd8..7090f9204 100644 --- a/src/apps/admin/src/lib/services/challenge-management.service.ts +++ b/src/apps/admin/src/lib/services/challenge-management.service.ts @@ -5,6 +5,7 @@ import { PaginatedResponse, xhrGetAsync, xhrGetPaginatedAsync, + xhrPatchAsync, xhrPostAsync, xhrRequestAsync, } from '~/libs/core' @@ -15,6 +16,7 @@ import { ChallengeResource, ChallengeTrack, ChallengeType, + ChallengeWinner, ResourceEmail, ResourceRole, } from '../models' @@ -138,3 +140,23 @@ export const addChallengeResource = async (data: { export const getChallengeById = async ( id: Challenge['id'], ): Promise => xhrGetAsync(`${challengeBaseUrl}/challenges/${id}`) + +type UpdateChallengePayload = { + status?: Challenge['status'] + winners?: Array< + Pick + > +} + +/** + * Partially updates challenge details by id. + * @param {string} id the challenge id. + * @param {UpdateChallengePayload} data challenge update payload. + */ +export const updateChallengeById = async ( + id: Challenge['id'], + data: UpdateChallengePayload, +): Promise => xhrPatchAsync( + `${challengeBaseUrl}/challenges/${id}`, + data, +) From a3f68d80af1c5d4e39af390ba2500ee4f53e37aa Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 6 Feb 2026 12:41:28 +1100 Subject: [PATCH 7/9] Tweaks for challenge details page in system-admin app and fixes for personalization format for social links - https://topcoder.atlassian.net/browse/PM-2073 --- .../ChallengeDetailsPage.tsx | 160 ++++++++++++++---- src/apps/profiles/src/lib/helpers.ts | 60 ++++++- .../src/member-profile/about-me/AboutMe.tsx | 10 +- .../ModifyAboutMeModal/ModifyAboutMeModal.tsx | 8 +- .../src/member-profile/links/MemberLinks.tsx | 22 +-- 5 files changed, 204 insertions(+), 56 deletions(-) diff --git a/src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.tsx b/src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.tsx index c160e3818..e5f5c2c17 100644 --- a/src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.tsx +++ b/src/apps/admin/src/challenge-management/ChallengeDetailsPage/ChallengeDetailsPage.tsx @@ -17,16 +17,21 @@ import { } from '~/libs/ui' import { rootRoute } from '../../config/routes.config' -import { FieldHandleSelect, PageWrapper } from '../../lib/components' +import { PageWrapper } from '../../lib/components' import { useEventCallback } from '../../lib/hooks' import { Challenge, ChallengeFilterCriteria, ChallengePrizeSet, + ChallengeResource, ChallengeWinner, - SelectOption, } from '../../lib/models' -import { getChallengeById, updateChallengeById } from '../../lib/services' +import { + getChallengeById, + getChallengeResources, + getResourceRoles, + updateChallengeById, +} from '../../lib/services' import { createChallengeQueryString, handleError } from '../../lib/utils' import styles from './ChallengeDetailsPage.module.scss' @@ -49,7 +54,7 @@ const CHALLENGE_STATUS_OPTIONS: string[] = [ 'CANCELLED_PAYMENT_FAILED', ] -type WinnersByPlacement = Record +type WinnersByPlacement = Record type RouteState = { previousChallengeListFilter?: ChallengeFilterCriteria @@ -120,14 +125,11 @@ function getPlacementCount(challenge?: Challenge): number { function createWinnersByPlacement(challenge?: Challenge): WinnersByPlacement { const totalPlacements = getPlacementCount(challenge) const winnersByPlacement: WinnersByPlacement = {} - const winnerLookup = new Map() + const winnerLookup = new Map() const winners = challenge?.winners ?? [] winners.forEach(winner => { - winnerLookup.set(winner.placement, { - label: winner.handle, - value: winner.userId, - }) + winnerLookup.set(winner.placement, `${winner.userId}`) }) for (let placement = 1; placement <= totalPlacements; placement += 1) { @@ -140,22 +142,30 @@ function createWinnersByPlacement(challenge?: Challenge): WinnersByPlacement { function createWinnerPayload( placements: number[], winnersByPlacement: WinnersByPlacement, + submitterHandleByUserId: Record, + fallbackHandleByUserId: Record, ): WinnerUpdate[] { const payload: WinnerUpdate[] = [] placements.forEach(placement => { - const selectedWinner = winnersByPlacement[placement] - if (!selectedWinner) { + const selectedWinnerUserId = winnersByPlacement[placement] + if (!selectedWinnerUserId) { return } - const userId = Number(selectedWinner.value) + const userId = Number(selectedWinnerUserId) if (!Number.isFinite(userId) || userId <= 0) { return } + const handle = submitterHandleByUserId[selectedWinnerUserId] + || fallbackHandleByUserId[selectedWinnerUserId] + if (!handle) { + return + } + payload.push({ - handle: `${selectedWinner.label}`, + handle, placement, userId, }) @@ -178,6 +188,12 @@ export const ChallengeDetailsPage: FC = () => { const [isLoading, setIsLoading] = useState(false) const [isSavingStatus, setIsSavingStatus] = useState(false) const [isSavingWinners, setIsSavingWinners] = useState(false) + const [isLoadingSubmitters, setIsLoadingSubmitters] = useState(false) + const [submitterOptions, setSubmitterOptions] = useState([ + { label: 'Select submitter', value: '' }, + ]) + const [submitterHandleByUserId, setSubmitterHandleByUserId] + = useState>({}) const hydrateChallenge = useEventCallback((challenge: Challenge): void => { setChallengeInfo(challenge) @@ -201,10 +217,91 @@ export const ChallengeDetailsPage: FC = () => { } }) + const loadChallengeSubmitters = useEventCallback(async () => { + if (!challengeId) { + return + } + + setIsLoadingSubmitters(true) + try { + const roles = await getResourceRoles() + const submitterRoleIds = roles + .filter(role => role.name.toLowerCase() + .includes('submitter')) + .map(role => role.id) + + if (submitterRoleIds.length === 0) { + setSubmitterOptions([{ label: 'Select submitter', value: '' }]) + setSubmitterHandleByUserId({}) + return + } + + const resourcesByRole = await Promise.all( + submitterRoleIds.map(async roleId => { + const resources: ChallengeResource[] = [] + let page = 1 + const perPage = 200 + let totalPages = 1 + + do { + // eslint-disable-next-line no-await-in-loop + const response = await getChallengeResources(challengeId, { + page, + perPage, + roleId, + }) + resources.push(...response.data) + totalPages = response.totalPages + page += 1 + } while (page <= totalPages) + + return resources + }), + ) + + const deduplicatedByMemberId = new Map() + resourcesByRole.flat() + .forEach(resource => { + if (!deduplicatedByMemberId.has(resource.memberId)) { + deduplicatedByMemberId.set(resource.memberId, resource) + } + }) + + const submitters = Array.from(deduplicatedByMemberId.values()) + .sort((left, right) => ( + left.memberHandle.localeCompare(right.memberHandle) + )) + + const handleMap: Record = {} + const options: InputSelectOption[] = [ + { label: 'Select submitter', value: '' }, + ...submitters.map(submitter => { + const userId = `${submitter.memberId}` + handleMap[userId] = submitter.memberHandle + return { + label: `${submitter.memberHandle} (${submitter.memberId})`, + value: userId, + } + }), + ] + + setSubmitterHandleByUserId(handleMap) + setSubmitterOptions(options) + } catch (error) { + handleError(error) + } finally { + setIsLoadingSubmitters(false) + } + }) + useEffect(() => { loadChallenge() }, [challengeId, loadChallenge]) + useEffect(() => { + loadChallengeSubmitters() + }, [challengeId, loadChallengeSubmitters]) + const statusOptions = useMemo(() => { const values = [...CHALLENGE_STATUS_OPTIONS] if (challengeInfo?.status && !values.includes(challengeInfo.status)) { @@ -231,6 +328,12 @@ export const ChallengeDetailsPage: FC = () => { }, [routeState.previousChallengeListFilter]) const pageTitle = challengeInfo?.name || 'Challenge Details' + const currentWinnerHandleByUserId = useMemo( + () => Object.fromEntries( + (challengeInfo?.winners ?? []).map(winner => [`${winner.userId}`, winner.handle]), + ), + [challengeInfo?.winners], + ) const handleStatusChange = useEventCallback( (event: ChangeEvent): void => { @@ -263,18 +366,11 @@ export const ChallengeDetailsPage: FC = () => { }) const createHandleWinnerChange = (placement: number) => ( - selectedWinner: SelectOption, + event: ChangeEvent, ): void => { setWinnersByPlacement(previous => ({ ...previous, - [placement]: selectedWinner, - })) - } - - const createHandleWinnerClear = (placement: number) => (): void => { - setWinnersByPlacement(previous => ({ - ...previous, - [placement]: undefined, + [placement]: event.target.value || undefined, })) } @@ -286,6 +382,8 @@ export const ChallengeDetailsPage: FC = () => { const winnerPayload = createWinnerPayload( placementNumbers, winnersByPlacement, + submitterHandleByUserId, + currentWinnerHandleByUserId, ) const payload: { status?: Challenge['status'] @@ -399,24 +497,16 @@ export const ChallengeDetailsPage: FC = () => {
{placementNumbers.map(placement => (
- -
))}
diff --git a/src/apps/profiles/src/lib/helpers.ts b/src/apps/profiles/src/lib/helpers.ts index ec497f2c7..f6e3e719c 100644 --- a/src/apps/profiles/src/lib/helpers.ts +++ b/src/apps/profiles/src/lib/helpers.ts @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import { UserProfile, UserRole } from '~/libs/core' +import { UserProfile, UserRole, UserTrait } from '~/libs/core' import { availabilityOptions, preferredRoleOptions } from '~/libs/shared/lib/components/modify-open-to-work-modal' import { ADMIN_ROLES, PHONE_NUMBER_ROLES } from '../config' @@ -210,3 +210,61 @@ export function formatRoleList(labels: string[]): string { return `${labels.slice(0, -1) .join(', ')} and ${labels[labels.length - 1]}` } + +function isObjectLike(value: any): boolean { + return !!value && typeof value === 'object' +} + +export function flattenPersonalizationData(personalizationData: UserTrait[] = []): UserTrait[] { + return personalizationData.reduce((accumulator: UserTrait[], item: UserTrait) => { + if (!isObjectLike(item)) return accumulator + + accumulator.push(item) + + if (Array.isArray(item.personalization)) { + item.personalization.forEach((nestedItem: UserTrait) => { + if (isObjectLike(nestedItem)) { + accumulator.push(nestedItem) + } + }) + } + + return accumulator + }, []) +} + +export function getFirstProfileSelfTitle(personalizationData: UserTrait[] = []): string | undefined { + return flattenPersonalizationData(personalizationData) + .map((trait: UserTrait) => ( + typeof trait.profileSelfTitle === 'string' ? trait.profileSelfTitle.trim() : '' + )) + .find(Boolean) +} + +export function getPersonalizationLinks(personalizationData: UserTrait[] = []): UserTrait[] { + const linksByKey = new Set() + + return flattenPersonalizationData(personalizationData) + .reduce((accumulator: UserTrait[], trait: UserTrait) => { + if (!Array.isArray(trait.links)) return accumulator + + trait.links.forEach((link: UserTrait) => { + const name = typeof link?.name === 'string' ? link.name.trim() : '' + const url = typeof link?.url === 'string' ? link.url.trim() : '' + + if (!name || !url) return + + const dedupeKey = `${name.toLowerCase()}-${url}` + if (linksByKey.has(dedupeKey)) return + + linksByKey.add(dedupeKey) + accumulator.push({ + ...link, + name, + url, + }) + }) + + return accumulator + }, []) +} diff --git a/src/apps/profiles/src/member-profile/about-me/AboutMe.tsx b/src/apps/profiles/src/member-profile/about-me/AboutMe.tsx index 1bf347dbd..b487c0259 100644 --- a/src/apps/profiles/src/member-profile/about-me/AboutMe.tsx +++ b/src/apps/profiles/src/member-profile/about-me/AboutMe.tsx @@ -7,7 +7,7 @@ import { NamesAndHandleAppearance, useMemberTraits, UserProfile, UserTraitIds, U import { AddButton, EditMemberPropertyBtn, EmptySection } from '../../components' import { EDIT_MODE_QUERY_PARAM, profileEditModes } from '../../config' -import { canSeePhones } from '../../lib/helpers' +import { canSeePhones, getFirstProfileSelfTitle } from '../../lib/helpers' import { Phones } from '../phones' import { ModifyAboutMeModal } from './ModifyAboutMeModal' @@ -33,8 +33,10 @@ const AboutMe: FC = (props: AboutMeProps) => { } = useMemberTraits(props.profile.handle, { traitIds: UserTraitIds.personalization }) - const memberTitleTrait: any - = memberPersonalizationTraits?.[0]?.traits?.data?.find((trait: any) => trait.profileSelfTitle) + const memberTitle: string | undefined = useMemo( + () => getFirstProfileSelfTitle(memberPersonalizationTraits?.[0]?.traits?.data), + [memberPersonalizationTraits], + ) const hasEmptyDescription = useMemo(() => ( props.profile && !props.profile.description @@ -82,7 +84,7 @@ const AboutMe: FC = (props: AboutMeProps) => {
-

{memberTitleTrait?.profileSelfTitle}

+

{memberTitle}

{canEdit && !hasEmptyDescription && ( = (props: ModifyAboutMeMod ] = useState() useEffect(() => { - const profileSelfTitleData: any - = props.memberPersonalizationTraitsData?.find( - (trait: any) => trait.profileSelfTitle, - ) - setMemberTitle(profileSelfTitleData?.profileSelfTitle) + setMemberTitle(getFirstProfileSelfTitle(props.memberPersonalizationTraitsData)) }, [props.memberPersonalizationTraitsData]) function handleMemberTitleChange(event: React.ChangeEvent): void { diff --git a/src/apps/profiles/src/member-profile/links/MemberLinks.tsx b/src/apps/profiles/src/member-profile/links/MemberLinks.tsx index 9a3bcc0ac..8d413f77f 100644 --- a/src/apps/profiles/src/member-profile/links/MemberLinks.tsx +++ b/src/apps/profiles/src/member-profile/links/MemberLinks.tsx @@ -11,7 +11,7 @@ import { import { AddButton, EditMemberPropertyBtn } from '../../components' import { EDIT_MODE_QUERY_PARAM, profileEditModes } from '../../config' -import { notifyUniNavi } from '../../lib' +import { getPersonalizationLinks, notifyUniNavi } from '../../lib' import { ModifyMemberLinksModal } from './ModifyMemberLinksModal' import { ReactComponent as GitHubLinkIcon } from './assets/github-link-icon.svg' @@ -57,10 +57,10 @@ const MemberLinks: FC = (props: MemberLinksProps) => { const { data: memberPersonalizationTraits, mutate: mutateTraits, loading }: MemberTraitsAPI = useMemberTraits(props.profile.handle, { traitIds: UserTraitIds.personalization }) - const memberLinks: UserTrait | undefined - = useMemo(() => memberPersonalizationTraits?.[0]?.traits?.data?.find( - (trait: UserTrait) => trait.links, - ), [memberPersonalizationTraits]) + const memberLinks: UserTrait[] = useMemo( + () => getPersonalizationLinks(memberPersonalizationTraits?.[0]?.traits?.data), + [memberPersonalizationTraits], + ) useEffect(() => { if (props.authProfile && editMode === profileEditModes.links) { @@ -85,12 +85,12 @@ const MemberLinks: FC = (props: MemberLinksProps) => { }, 1000) } - return !loading && (canEdit || memberLinks?.links?.length) ? ( + return !loading && (canEdit || memberLinks.length) ? (
- {memberLinks?.links.length ? ( + {memberLinks.length ? (
{ - memberLinks?.links.map((trait: UserTrait) => ( + memberLinks.map((trait: UserTrait) => ( = (props: MemberLinksProps) => {
) : undefined} - {canEdit && !!memberLinks?.links.length && ( + {canEdit && !!memberLinks.length && ( )} - {canEdit && !memberLinks?.links.length && ( + {canEdit && !memberLinks.length && ( = (props: MemberLinksProps) => { onClose={handleEditModalClose} onSave={handleEditModalSaved} profile={props.profile} - memberLinks={memberLinks ? memberLinks.links : undefined} + memberLinks={memberLinks} memberPersonalizationTraitsFullData={memberPersonalizationTraits?.[0]?.traits?.data} /> ) From 1424a179b3da41d9c0c3ea0d17c22e589a10de5d Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 6 Feb 2026 15:14:13 +0530 Subject: [PATCH 8/9] PM-3532 Update redux on open to work step --- .../onboarding/src/pages/open-to-work/index.tsx | 16 ++++++++++++++-- src/apps/onboarding/src/redux/actions/member.ts | 13 +++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/apps/onboarding/src/pages/open-to-work/index.tsx b/src/apps/onboarding/src/pages/open-to-work/index.tsx index 02debd730..4bd36cc61 100644 --- a/src/apps/onboarding/src/pages/open-to-work/index.tsx +++ b/src/apps/onboarding/src/pages/open-to-work/index.tsx @@ -14,7 +14,7 @@ import { OpenToWorkData } from '~/libs/shared/lib/components/modify-open-to-work import OpenToWorkForm from '~/libs/shared/lib/components/modify-open-to-work-modal/ModifyOpenToWorkModal' import { ProgressBar } from '../../components/progress-bar' -import { updateMemberOpenForWork } from '../../redux/actions/member' +import { updateMemberOpenForWork, updatePersonalizations } from '../../redux/actions/member' import styles from './styles.module.scss' @@ -22,6 +22,7 @@ interface PageOpenToWorkContentProps { profileHandle: string availableForGigs: boolean updateMemberOpenForWork: (isOpenForWork: boolean) => void + updatePersonalizations: (personalizations: any[]) => void } export const PageOpenToWorkContent: FC = props => { @@ -84,7 +85,7 @@ export const PageOpenToWorkContent: FC = props => { }] try { - await Promise.all([ + const [, updatedTraits] = await Promise.all([ // profile flag props.updateMemberOpenForWork(formValue.availableForGigs), @@ -98,6 +99,16 @@ export const PageOpenToWorkContent: FC = props => { }]), ]) + const personalizationTrait = updatedTraits?.find( + (t: any) => t.traitId === UserTraitIds.personalization, + ) + + const nextPersonalizations = personalizationTrait?.traits?.data + + if (Array.isArray(nextPersonalizations)) { + props.updatePersonalizations(nextPersonalizations) + } + navigate('../works') } catch (e) { toast.error('Failed to save work preferences') @@ -166,6 +177,7 @@ const mapStateToProps: any = (state: any) => ({ const mapDispatchToProps: any = { updateMemberOpenForWork, + updatePersonalizations, } export const PageOpenToWork: any = connect(mapStateToProps, mapDispatchToProps)(PageOpenToWorkContent) diff --git a/src/apps/onboarding/src/redux/actions/member.ts b/src/apps/onboarding/src/redux/actions/member.ts index fbc0945ab..836759f96 100644 --- a/src/apps/onboarding/src/redux/actions/member.ts +++ b/src/apps/onboarding/src/redux/actions/member.ts @@ -365,16 +365,17 @@ export const createPersonalizationsPayloadData: any = (personalizations: Persona profileSelfTitle, shortBio, availableForGigs, - availability, - preferredRoles, + openToWork, }: any = personalization return _.omitBy({ ...personalization, availableForGigs, - openToWork: { - availability, - preferredRoles, - }, + openToWork: openToWork + ? { + availability: openToWork.availability, + preferredRoles: openToWork.preferredRoles, + } + : undefined, profileSelfTitle, referAs, shortBio, From e9d4d6923413c47e395ace86f662c8a2c923d0bc Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 6 Feb 2026 15:27:43 +0530 Subject: [PATCH 9/9] Add types --- src/apps/onboarding/src/pages/open-to-work/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apps/onboarding/src/pages/open-to-work/index.tsx b/src/apps/onboarding/src/pages/open-to-work/index.tsx index 4bd36cc61..9ebfd75bf 100644 --- a/src/apps/onboarding/src/pages/open-to-work/index.tsx +++ b/src/apps/onboarding/src/pages/open-to-work/index.tsx @@ -7,6 +7,7 @@ import classNames from 'classnames' import { Button, IconOutline, PageDivider } from '~/libs/ui' import { updateOrCreateMemberTraitsAsync, useMemberTraits, + UserTrait, UserTraitCategoryNames, UserTraitIds, UserTraits } from '~/libs/core' @@ -15,6 +16,7 @@ import OpenToWorkForm from '~/libs/shared/lib/components/modify-open-to-work-mod import { ProgressBar } from '../../components/progress-bar' import { updateMemberOpenForWork, updatePersonalizations } from '../../redux/actions/member' +import PersonalizationInfo from '../../models/PersonalizationInfo' import styles from './styles.module.scss' @@ -22,7 +24,7 @@ interface PageOpenToWorkContentProps { profileHandle: string availableForGigs: boolean updateMemberOpenForWork: (isOpenForWork: boolean) => void - updatePersonalizations: (personalizations: any[]) => void + updatePersonalizations: (personalizations: PersonalizationInfo[]) => void } export const PageOpenToWorkContent: FC = props => { @@ -100,7 +102,7 @@ export const PageOpenToWorkContent: FC = props => { ]) const personalizationTrait = updatedTraits?.find( - (t: any) => t.traitId === UserTraitIds.personalization, + (t: UserTrait) => t.traitId === UserTraitIds.personalization, ) const nextPersonalizations = personalizationTrait?.traits?.data