diff --git a/src/apps/profiles/src/config/constants.ts b/src/apps/profiles/src/config/constants.ts index ec7f7dce4..544b1efa4 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', @@ -26,3 +28,5 @@ export enum profileEditModes { // (removed) CES Survey/Userflow integrations export const MAX_PRINCIPAL_SKILLS_COUNT = 10 + +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 fa3969676..0f39cb702 100644 --- a/src/apps/profiles/src/lib/helpers.ts +++ b/src/apps/profiles/src/lib/helpers.ts @@ -1,5 +1,7 @@ /* eslint-disable complexity */ -import { UserProfile } from '~/libs/core' +import { UserProfile, UserRole } from '~/libs/core' + +import { ADMIN_ROLES } from '../config' declare global { interface Window { tcUniNav: any } @@ -125,3 +127,37 @@ export function isValidURL(urlToValidate: string): boolean { export function formatPlural(count: number, baseWord: string): string { return `${baseWord}${count === 1 ? '' : 's'}` } + +/** + * Check if the user can download the profile + * @param authProfile - The authenticated user profile + * @param profile - The profile to check if the user can download + * @returns {boolean} - Whether the user can download the profile + */ +export 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 + if (authProfile.roles?.some(role => ADMIN_ROLES.includes(role.toLowerCase() as UserRole))) { + 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 +} 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..ce475e84b 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,68 @@ 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; + + @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..17e4740fe 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 { downloadProfileAsync, UserProfile } from '~/libs/core' import { Button, ContentLayout, IconSolid, PageTitle } from '~/libs/ui' // import { MemberTCActivityInfo } from '../tc-activity' @@ -14,6 +14,7 @@ import { MemberTCAchievements } from '../tc-achievements' import { WorkExpirence } from '../work-expirence' import { EducationAndCertifications } from '../education-and-certifications' import { ProfileCompleteness } from '../profile-completeness' +import { canDownloadProfile } from '../../lib' import OnboardingCompleted from '../onboarding-complete/OnboardingCompleted' import styles from './ProfilePageLayout.module.scss' @@ -26,102 +27,137 @@ interface ProfilePageLayoutProps { handleBackBtn: () => void } -const ProfilePageLayout: FC = (props: ProfilePageLayoutProps) => ( -
- - {`${props.profile.handle} | Community Profile | Topcoder`} - -
- - {props.isTalentSearch && ( -
-
- )} - -
-
-
- - -
-
- = (props: ProfilePageLayoutProps) => { + + 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) {} finally { + setIsDownloading(false) + } + } + + return ( +
+ + {`${props.profile.handle} | Community Profile | Topcoder`} + +
+ { + canDownload && ( +
+
+ ) + } + + {props.isTalentSearch && ( +
+
+ )} + +
+
+
- + +
+
+ - + - {props.profile.userId === props.authProfile?.userId && ( - - )} -
-
- {props.authProfile?.handle === props.profile.handle && ( - - )} -
-
- -
-
+ - + {props.profile.userId === props.authProfile?.userId && ( + + )} +
+
+ {props.authProfile?.handle === props.profile.handle && ( + + )} +
+
+ +
+
-
-
+ + +
+
+
+ +
+
-
-
- -
-
- + - + -
-) +
+ ) +} 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..eed12e426 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,26 @@ export async function updateMemberPhotoAsync(handle: string, payload: FormData): return updateMemberPhoto(handle, payload) } +export async function downloadProfileAsync(handle: string): Promise { + let url: string | undefined + try { + const blob = await downloadProfile(handle) + 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) + } catch (error) { + console.error('Failed to download profile:', error) + } finally { + if (url) { + window.URL.revokeObjectURL(url) + } + } +} + export async function updateOrCreateMemberTraitsAsync( handle: string, traits: UserTraits[],