From 6cf84b518b12b365f05320011696133fccb08c01 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 14 Jan 2026 00:01:08 +0530 Subject: [PATCH 1/6] 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 2/6] 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