Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,18 @@
flex-direction: column;
gap: 8px;
}

.moreButton {
all: unset;
cursor: pointer;
color: #374151;
font-size: 13px;
font-weight: 700;
display: inline-block;
padding: 2px 0;
}

.moreButton:hover {
text-decoration: underline;
cursor: pointer;
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ export const TeamCalendar: FC<TeamCalendarProps> = (props: TeamCalendarProps) =>
[isMobile],
)

const handleMoreClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
if (isMobile) return // hover only matters on desktop
const dateKey = e.currentTarget.dataset.dateKey
if (!dateKey) return
setOpenDateKey(dateKey)
}, [isMobile])

const monthDates = useMemo(
() => getMonthDates(currentDate.getFullYear(), currentDate.getMonth()),
[currentDate],
Expand Down Expand Up @@ -132,7 +139,7 @@ export const TeamCalendar: FC<TeamCalendarProps> = (props: TeamCalendarProps) =>
const leaveEntry = teamLeaveDates.find(item => item.date === dateKey)
const users = leaveEntry?.usersOnLeave ?? []
const sortedUsers = [...users].sort(compareUsersByName)
const displayedUsers = sortedUsers.slice(0, 10)
const displayedUsers = sortedUsers.slice(0, 2)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
Reducing the number of displayed users from 10 to 2 might significantly change the user experience. Ensure that this change is intentional and aligns with the design requirements.

const overflowCount = sortedUsers.length - displayedUsers.length
const weekendClass = isWeekend(date) ? styles.weekend : undefined

Expand Down Expand Up @@ -191,7 +198,16 @@ export const TeamCalendar: FC<TeamCalendarProps> = (props: TeamCalendarProps) =>
})}
{overflowCount > 0 && (
<div className={styles.overflowIndicator}>
{`+${overflowCount} more`}
<button
type='button'
className={styles.moreButton}
data-date-key={dateKey}
onClick={handleMoreClick}
aria-haspopup='dialog'
aria-label={`Show ${overflowCount} more for ${dateKey}`}
>
{`+${overflowCount} more`}
</button>
</div>
)}
</div>
Expand All @@ -202,7 +218,7 @@ export const TeamCalendar: FC<TeamCalendarProps> = (props: TeamCalendarProps) =>
})}
</div>

{isMobile && openDateKey && (() => {
{openDateKey && (() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The condition isMobile && openDateKey was removed. Ensure that this change does not unintentionally affect the mobile-specific behavior of the calendar.

const selectedDate = paddedDates.find(d => d && getDateKey(d) === openDateKey)
if (!selectedDate) return undefined

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,24 @@
width: 100%;
}
}

.applyMessage {
display: flex;
flex-direction: column;
gap: $sp-3;
padding: $sp-4;
border-radius: 8px;
}

.signInLink {
display: inline-block;
width: fit-content;
font-size: 16px;
font-weight: 600;
color: var(--tc-primary);
text-decoration: none;

&:hover {
text-decoration: underline;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import remarkFrontmatter from 'remark-frontmatter'
import remarkGfm from 'remark-gfm'

import { Button, IconSolid } from '~/libs/ui'
import { EnvironmentConfig } from '~/config'

import type { Engagement, EngagementAssignment } from '../../lib/models'
import { formatDate, formatLocation, truncateText } from '../../lib/utils'
Expand Down Expand Up @@ -58,6 +59,8 @@ interface AssignmentCardProps {
onRejectOffer?: () => void
onContactTalentManager: (contactEmail?: string) => void
canContactTalentManager?: boolean
profileGateError?: string
profileHandle?: string
}

const DESCRIPTION_MAX_LENGTH = 160
Expand Down Expand Up @@ -154,6 +157,42 @@ const AssignmentCard: FC<AssignmentCardProps> = (props: AssignmentCardProps) =>
const showAssignedActions = assignmentStatus === 'assigned'
const showOfferActions = assignmentStatus === 'selected'

const renderOfferActions = (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
The renderOfferActions function is defined within the AssignmentCard component, which means it will be re-created on every render. Consider using useCallback to memoize this function if it doesn't depend on any props or state that change frequently.

profileGateError?: string,
onAcceptOffer?: () => void,
onRejectOffer?: () => void,
actionButtonClass?: string,
): JSX.Element | undefined => {
if (
profileGateError
|| !showOfferActions
|| !onAcceptOffer
|| !onRejectOffer
) {
return undefined
}

return (
<>
<Button
label='Accept Offer'
onClick={onAcceptOffer}
primary
textWrap
className={actionButtonClass}
/>
<Button
label='Reject Offer'
onClick={onRejectOffer}
secondary
variant='danger'
textWrap
className={actionButtonClass}
/>
</>
)
}

return (
<div className={styles.card}>
<div className={styles.header}>
Expand Down Expand Up @@ -205,6 +244,21 @@ const AssignmentCard: FC<AssignmentCardProps> = (props: AssignmentCardProps) =>
<span className={styles.moreSkills}>{`+${extraSkillsCount} more`}</span>
)}
</div>
<div>
{ props.profileGateError && (
<div className={styles.applyMessage}>
<span className={styles.termsError}>
{props.profileGateError}
</span>
<a
className={styles.signInLink}
href={`${EnvironmentConfig.URLS.USER_PROFILE}/${props.profileHandle}`}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
The URL construction for the profile link uses string interpolation with props.profileHandle. Ensure that profileHandle is properly sanitized to prevent potential injection attacks or malformed URLs.

>
Please update your profile here.
</a>
</div>
)}
</div>
<div className={styles.actions}>
{showAssignedActions && (
<>
Expand All @@ -224,24 +278,11 @@ const AssignmentCard: FC<AssignmentCardProps> = (props: AssignmentCardProps) =>
/>
</>
)}
{showOfferActions && props.onAcceptOffer && props.onRejectOffer && (
<>
<Button
label='Accept Offer'
onClick={props.onAcceptOffer}
primary
textWrap
className={styles.actionButton}
/>
<Button
label='Reject Offer'
onClick={props.onRejectOffer}
secondary
variant='danger'
textWrap
className={styles.actionButton}
/>
</>
{renderOfferActions(
props.profileGateError,
props.onAcceptOffer,
props.onRejectOffer,
styles.actionButton,
)}
<Button
label='Contact Talent Manager'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'

import { EnvironmentConfig } from '~/config'
import { useProfileContext } from '~/libs/core'
import { useProfileCompleteness, useProfileContext } from '~/libs/core'
import { Button, ContentLayout, IconOutline, LoadingSpinner } from '~/libs/ui'
import { Pagination } from '~/apps/admin/src/lib/components/common/Pagination'

Expand Down Expand Up @@ -76,6 +76,9 @@ const MyAssignmentsPage: FC = () => {
const profileContext = useProfileContext()
const isLoggedIn = profileContext.isLoggedIn
const userId = profileContext.profile?.userId
const profileHandle = profileContext.profile?.handle
const profileCompleteness = useProfileCompleteness(profileHandle)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The useProfileCompleteness hook is called with profileHandle, which could be undefined if profileContext.profile is not available. Ensure that useProfileCompleteness can handle an undefined argument gracefully.

const [profileGateError, setProfileGateError] = useState<string | undefined>()

const [assignments, setAssignments] = useState<Engagement[]>([])
const [loading, setLoading] = useState<boolean>(false)
Expand Down Expand Up @@ -333,6 +336,23 @@ const MyAssignmentsPage: FC = () => {
}

const handleAcceptOfferClick = function (): void {
setProfileGateError(undefined)

if (profileCompleteness?.isLoading) {
return
}

if (
profileCompleteness
&& typeof profileCompleteness.percent === 'number'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The check typeof profileCompleteness.percent === 'number' is necessary, but consider adding a check to ensure profileCompleteness is not undefined before accessing percent to avoid potential runtime errors.

&& profileCompleteness.percent < 100
) {
setProfileGateError(
'Your profile must be 100% complete before applying.',
)
return
}

handleOpenOfferModal(engagement, 'accept')
}

Expand All @@ -352,6 +372,8 @@ const MyAssignmentsPage: FC = () => {
onRejectOffer={handleRejectOfferClick}
onContactTalentManager={handleContactTalentManager}
canContactTalentManager={Boolean(contactEmail)}
profileGateError={profileGateError}
profileHandle={profileHandle}
/>
)
})}
Expand Down
43 changes: 40 additions & 3 deletions src/apps/onboarding/src/pages/open-to-work/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useNavigate } from 'react-router-dom'
import { FC, MutableRefObject, useEffect, useRef, useState } from 'react'
import { Dispatch, FC, MutableRefObject, SetStateAction, useEffect, useRef, useState } from 'react'
import { connect } from 'react-redux'
import { toast } from 'react-toastify'
import classNames from 'classnames'
Expand All @@ -12,7 +12,8 @@ import { updateOrCreateMemberTraitsAsync,
UserTraitIds,
UserTraits } from '~/libs/core'
import { OpenToWorkData } from '~/libs/shared/lib/components/modify-open-to-work-modal'
import OpenToWorkForm from '~/libs/shared/lib/components/modify-open-to-work-modal/ModifyOpenToWorkModal'
import OpenToWorkForm,
{ validateOpenToWork } from '~/libs/shared/lib/components/modify-open-to-work-modal/ModifyOpenToWorkModal'

import { ProgressBar } from '../../components/progress-bar'
import { updateMemberOpenForWork, updatePersonalizations } from '../../redux/actions/member'
Expand Down Expand Up @@ -46,6 +47,14 @@ export const PageOpenToWorkContent: FC<PageOpenToWorkContentProps> = props => {
preferredRoles: [],
})

const [submitAttempted, setSubmitAttempted] = useState(false)

const [formErrors, setFormErrors]: [
{ [key: string]: string },
Dispatch<SetStateAction<{ [key: string]: string }>>
]
= useState<{ [key: string]: string }>({})

useEffect(() => {
if (!memberPersonalizationTraits) return

Expand All @@ -61,6 +70,14 @@ export const PageOpenToWorkContent: FC<PageOpenToWorkContentProps> = props => {
}))
}, [memberPersonalizationTraits, props.availableForGigs])

function handleFormChange(nextValue: OpenToWorkData): void {
setFormValue(nextValue)

if (submitAttempted) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
The validateOpenToWork function is called on every form change when submitAttempted is true. This could lead to performance issues if the validation logic is complex or if the form changes frequently. Consider optimizing the validation logic or debouncing the validation calls.

setFormErrors(validateOpenToWork(nextValue))
}
}

useEffect(() => {
if (!loading && !shouldSavingData.current && !!shouldNavigateTo.current) {
navigate(shouldNavigateTo.current)
Expand All @@ -73,6 +90,16 @@ export const PageOpenToWorkContent: FC<PageOpenToWorkContentProps> = props => {
}

async function goToNextStep(): Promise<void> {
setSubmitAttempted(true)

const errors = validateOpenToWork(formValue)
setFormErrors(errors)

if (Object.keys(errors).length > 0) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The check if (Object.keys(errors).length > 0) is used to determine if there are validation errors. Ensure that validateOpenToWork always returns an object, even if there are no errors, to avoid potential runtime errors.

// Don't move to next step
return
}

setLoading(true)

const existing = memberPersonalizationTraits?.[0]?.traits?.data?.[0] || {}
Expand Down Expand Up @@ -118,6 +145,14 @@ export const PageOpenToWorkContent: FC<PageOpenToWorkContentProps> = props => {
}
}

// reset error when open to work toggle off
useEffect(() => {
if (!formValue.availableForGigs) {
setFormErrors({})
setSubmitAttempted(false)
}
}, [formValue.availableForGigs])

return (
<div className={classNames('d-flex flex-column', styles.container)}>
<h2>
Expand All @@ -136,8 +171,10 @@ export const PageOpenToWorkContent: FC<PageOpenToWorkContentProps> = props => {
<div className='mt-26'>
<OpenToWorkForm
value={formValue}
onChange={setFormValue}
onChange={handleFormChange}
disabled={loading}
formErrors={formErrors}
showErrors={submitAttempted}
/>
</div>
</div>
Expand Down
Loading
Loading