Skip to content

Commit 6cf84b5

Browse files
committed
feat: added download button in profile page
1 parent c0de22c commit 6cf84b5

File tree

5 files changed

+166
-34
lines changed

5 files changed

+166
-34
lines changed

src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.module.scss

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,69 @@
77
height: 100%;
88

99
.profileHeaderWrap {
10+
position: relative;
1011
background: url('../../lib/assets/profile-header-bg.png') no-repeat right top / auto, linear-gradient(#0d83c5, #0e89d5);
1112

1213
@include ltelg {
1314
background: url('../../lib/assets/profile-header-bg-mobile.png') no-repeat right top /100% 100%;
1415
}
1516

17+
.downloadButtonWrap {
18+
position: absolute;
19+
top: $sp-4;
20+
right: calc((100% - #{$xxl-min}) / 2);
21+
max-width: $xxl-min;
22+
width: 100%;
23+
display: flex;
24+
justify-content: flex-end;
25+
padding-right: $sp-8;
26+
z-index: 10;
27+
pointer-events: none;
28+
29+
@include ltexl {
30+
right: $sp-8;
31+
}
32+
33+
@include ltemd {
34+
padding-right: $sp-6;
35+
right: $sp-6;
36+
}
37+
38+
@include ltesm {
39+
padding-right: $sp-4;
40+
right: $sp-4;
41+
}
42+
43+
@include ltelg {
44+
position: absolute;
45+
top: $sp-4;
46+
right: $sp-4;
47+
left: auto;
48+
max-width: none;
49+
width: auto;
50+
padding: 0;
51+
pointer-events: auto;
52+
}
53+
54+
> * {
55+
pointer-events: auto;
56+
}
57+
}
58+
59+
.downloadButton {
60+
color: $tc-white;
61+
padding: $sp-2 $sp-4;
62+
border-radius: 4px;
63+
font-weight: $font-weight-bold;
64+
font-family: $font-roboto;
65+
font-size: 16px;
66+
67+
&:disabled {
68+
opacity: 0.6;
69+
cursor: not-allowed;
70+
}
71+
}
72+
1673
.profileHeaderContent {
1774
padding: 0;
1875
max-height: 260px;

src/apps/profiles/src/member-profile/page-layout/ProfilePageLayout.tsx

Lines changed: 90 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { FC } from 'react'
1+
import { Dispatch, FC, SetStateAction, useState } from 'react'
22

3-
import { UserProfile } from '~/libs/core'
3+
import { UserProfile, downloadProfileAsync } from '~/libs/core'
44
import { Button, ContentLayout, IconSolid, PageTitle } from '~/libs/ui'
55

66
// import { MemberTCActivityInfo } from '../tc-activity'
@@ -26,35 +26,91 @@ interface ProfilePageLayoutProps {
2626
handleBackBtn: () => void
2727
}
2828

29-
const ProfilePageLayout: FC<ProfilePageLayoutProps> = (props: ProfilePageLayoutProps) => (
30-
<div className={styles.container}>
31-
32-
<PageTitle>{`${props.profile.handle} | Community Profile | Topcoder`}</PageTitle>
33-
34-
<div className={styles.profileHeaderWrap}>
35-
<ContentLayout
36-
outerClass={styles.profileHeaderContentOuter}
37-
contentClass={styles.profileHeaderContent}
38-
>
39-
{props.isTalentSearch && (
40-
<div className={styles.backBtn}>
41-
<Button
42-
link
43-
label='Search Results'
44-
icon={IconSolid.ChevronLeftIcon}
45-
iconToLeft
46-
onClick={props.handleBackBtn}
47-
/>
48-
</div>
49-
)}
50-
<ProfileHeader
51-
profile={props.profile}
52-
authProfile={props.authProfile}
53-
refreshProfile={props.refreshProfile}
54-
/>
55-
</ContentLayout>
56-
<div className={styles.profileHeaderBottom} />
57-
</div>
29+
const ProfilePageLayout: FC<ProfilePageLayoutProps> = (props: ProfilePageLayoutProps) => {
30+
function canDownloadProfile(authProfile: UserProfile | undefined, profile: UserProfile): boolean {
31+
if (!authProfile) {
32+
return false
33+
}
34+
// Check if user is viewing their own profile
35+
if (authProfile.handle === profile.handle) {
36+
return true
37+
}
38+
// Check if user has admin roles
39+
const adminRoles = ['administrator', 'admin']
40+
if (authProfile.roles?.some(role => adminRoles.includes(role.toLowerCase()))) {
41+
return true
42+
}
43+
// Check if user has PM or Talent Manager roles
44+
const allowedRoles = ['Project Manager', 'Talent Manager']
45+
if (authProfile.roles?.some(role => allowedRoles.some(allowed => role.toLowerCase() === allowed.toLowerCase()))) {
46+
return true
47+
}
48+
return false
49+
}
50+
51+
const canDownload: boolean = canDownloadProfile(props.authProfile, props.profile)
52+
53+
const [isDownloading, setIsDownloading]: [boolean, Dispatch<SetStateAction<boolean>>]
54+
= useState<boolean>(false)
55+
56+
async function handleDownloadProfile(): Promise<void> {
57+
if (isDownloading) {
58+
return
59+
}
60+
setIsDownloading(true)
61+
try {
62+
await downloadProfileAsync(props.profile.handle)
63+
} catch (error) {
64+
// Error handling - could show a toast notification here
65+
console.error('Failed to download profile:', error)
66+
} finally {
67+
setIsDownloading(false)
68+
}
69+
}
70+
71+
return (
72+
<div className={styles.container}>
73+
74+
<PageTitle>{`${props.profile.handle} | Community Profile | Topcoder`}</PageTitle>
75+
76+
<div className={styles.profileHeaderWrap}>
77+
{
78+
canDownload && (
79+
<div className={styles.downloadButtonWrap}>
80+
<Button
81+
label='Download Profile'
82+
icon={IconSolid.DownloadIcon}
83+
iconToRight={true}
84+
onClick={handleDownloadProfile}
85+
disabled={isDownloading}
86+
className={styles.downloadButton}
87+
/>
88+
</div>
89+
)
90+
}
91+
<ContentLayout
92+
outerClass={styles.profileHeaderContentOuter}
93+
contentClass={styles.profileHeaderContent}
94+
>
95+
{props.isTalentSearch && (
96+
<div className={styles.backBtn}>
97+
<Button
98+
link
99+
label='Search Results'
100+
icon={IconSolid.ChevronLeftIcon}
101+
iconToLeft
102+
onClick={props.handleBackBtn}
103+
/>
104+
</div>
105+
)}
106+
<ProfileHeader
107+
profile={props.profile}
108+
authProfile={props.authProfile}
109+
refreshProfile={props.refreshProfile}
110+
/>
111+
</ContentLayout>
112+
<div className={styles.profileHeaderBottom} />
113+
</div>
58114

59115
<ContentLayout
60116
outerClass={styles.profileOuter}
@@ -121,7 +177,8 @@ const ProfilePageLayout: FC<ProfilePageLayoutProps> = (props: ProfilePageLayoutP
121177

122178
<OnboardingCompleted authProfile={props.authProfile} />
123179

124-
</div>
125-
)
180+
</div>
181+
)
182+
}
126183

127184
export default ProfilePageLayout

src/libs/core/lib/profile/profile-functions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {
1414
modifyTracksAsync,
1515
updateMemberProfileAsync,
1616
updateMemberPhotoAsync,
17+
downloadProfileAsync,
1718
updateOrCreateMemberTraitsAsync,
1819
updateDeleteOrCreateMemberTraitAsync,
1920
} from './profile.functions'

src/libs/core/lib/profile/profile-functions/profile-store/profile-xhr.store.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { xhrDeleteAsync, xhrGetAsync, xhrPatchAsync, xhrPostAsync, xhrPutAsync } from '../../../xhr'
1+
import { xhrDeleteAsync, xhrGetAsync, xhrGetBlobAsync, xhrPatchAsync, xhrPostAsync, xhrPutAsync } from '../../../xhr'
22
import { CountryLookup } from '../../country-lookup.model'
33
import { EditNameRequest } from '../../edit-name-request.model'
44
import { ModifyTracksRequest } from '../../modify-tracks.request'
@@ -126,3 +126,7 @@ export async function updateMemberPhoto(handle: string, payload: FormData): Prom
126126
},
127127
})
128128
}
129+
130+
export async function downloadProfile(handle: string): Promise<Blob> {
131+
return xhrGetBlobAsync<Blob>(`${profileUrl(handle)}/profileDownload`)
132+
}

src/libs/core/lib/profile/profile-functions/profile.functions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getMemberStats, getVerification, profileStoreGet, profileStorePatchName
1616
import {
1717
createMemberTraits,
1818
deleteMemberTrait,
19+
downloadProfile,
1920
getCountryLookup,
2021
modifyTracks,
2122
updateMemberEmailPreferences,
@@ -143,6 +144,18 @@ export async function updateMemberPhotoAsync(handle: string, payload: FormData):
143144
return updateMemberPhoto(handle, payload)
144145
}
145146

147+
export async function downloadProfileAsync(handle: string): Promise<void> {
148+
const blob = await downloadProfile(handle)
149+
const url = window.URL.createObjectURL(blob)
150+
const link = document.createElement('a')
151+
link.href = url
152+
link.setAttribute('download', `profile-${handle}.pdf`)
153+
document.body.appendChild(link)
154+
link.click()
155+
link.parentNode?.removeChild(link)
156+
window.URL.revokeObjectURL(url)
157+
}
158+
146159
export async function updateOrCreateMemberTraitsAsync(
147160
handle: string,
148161
traits: UserTraits[],

0 commit comments

Comments
 (0)