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
269 changes: 1 addition & 268 deletions src/shared/components/engagement-listing/EngagementCard/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import IconBlackDuration from 'assets/images/icon-black-calendar.svg';
import IconBlackLocation from 'assets/images/icon-black-location.svg';
import IconBlackPayment from 'assets/images/icon-black-payment.svg';
import iconBlackSkills from 'assets/images/icon-skills.png';
import IconTimezone from 'assets/images/icon-timezone.svg';

import './style.scss';

Expand All @@ -22,12 +21,6 @@ const WORKLOAD_LABELS = {
FRACTIONAL: 'Fractional',
};

const ANTICIPATED_START_LABELS = {
IMMEDIATE: 'Immediate',
FEW_DAYS: 'Few Days',
FEW_WEEKS: 'Few Weeks',
};

const STATUS_LABELS = {
OPEN: 'Open',
PENDING_ASSIGNMENT: 'Pending Assignment',
Expand All @@ -36,62 +29,20 @@ const STATUS_LABELS = {
CLOSED: 'Closed',
};

const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const UNKNOWN_SKILL_LABEL = 'Unknown skill';
const DEFAULT_LOCALE = 'en-US';
const SIMPLE_TZ_PATTERN = /^[A-Za-z]{2,6}$/;
const OFFSET_TZ_PATTERN = /^(?:UTC|GMT)?\s*([+-])\s*(\d{1,2})(?::?(\d{2}))?$/i;
const BARE_OFFSET_PATTERN = /^([+-])(\d{2})(?::?(\d{2}))$/;
const TIMEZONE_ABBREVIATION_LONG_NAMES = {
ACDT: 'Australian Central Daylight Time',
ACST: 'Australian Central Standard Time',
AEDT: 'Australian Eastern Daylight Time',
AEST: 'Australian Eastern Standard Time',
AKDT: 'Alaska Daylight Time',
AKST: 'Alaska Standard Time',
AWST: 'Australian Western Standard Time',
BST: 'British Summer Time',
CDT: 'Central Daylight Time',
CEST: 'Central European Summer Time',
CET: 'Central European Standard Time',
CST: 'Central Standard Time',
EDT: 'Eastern Daylight Time',
EEST: 'Eastern European Summer Time',
EET: 'Eastern European Standard Time',
EST: 'Eastern Standard Time',
GMT: 'Greenwich Mean Time',
HST: 'Hawaii-Aleutian Standard Time',
IST: 'India Standard Time',
JST: 'Japan Standard Time',
KST: 'Korea Standard Time',
MDT: 'Mountain Daylight Time',
MST: 'Mountain Standard Time',
NZDT: 'New Zealand Daylight Time',
NZST: 'New Zealand Standard Time',
PDT: 'Pacific Daylight Time',
PST: 'Pacific Standard Time',
SAST: 'South Africa Standard Time',
UTC: 'Coordinated Universal Time',
WEST: 'Western European Summer Time',
WET: 'Western European Standard Time',
};

const REGION_NAME_OVERRIDES = {
UK: 'United Kingdom',
};
const regionDisplayNames = typeof Intl !== 'undefined' && typeof Intl.DisplayNames === 'function'
? new Intl.DisplayNames([DEFAULT_LOCALE], { type: 'region' })
: null;
let timezoneAbbreviationMap;

function asArray(value) {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}

function isUuid(value) {
return typeof value === 'string' && UUID_PATTERN.test(value);
}

function toTitleCase(value) {
return value
.toLowerCase()
Expand Down Expand Up @@ -127,21 +78,6 @@ function normalizeLabel(value, normalizedMap) {
return spaced || raw;
}

function normalizeSkillLabel(skill) {
if (!skill) return null;

if (typeof skill === 'object' && skill !== null) {
const label = skill.name || skill.title;
if (label) return String(label);
const skillId = skill.id || skill.value;
if (isUuid(skillId)) return UNKNOWN_SKILL_LABEL;
return skillId ? String(skillId) : null;
}

if (isUuid(skill)) return UNKNOWN_SKILL_LABEL;
return String(skill);
}

function normalizeLocationValue(value) {
if (!value) return null;
if (typeof value === 'object' && value !== null) {
Expand Down Expand Up @@ -173,154 +109,6 @@ function normalizeRegionValue(value) {
return trimmed;
}

function getIntlTimeZoneName(timeZone, style) {
if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat !== 'function') {
return null;
}

try {
const formatter = new Intl.DateTimeFormat(DEFAULT_LOCALE, {
timeZone,
timeZoneName: style,
});

if (typeof formatter.formatToParts !== 'function') {
return null;
}

const parts = formatter.formatToParts(new Date());
const namePart = parts.find(part => part.type === 'timeZoneName');
return namePart && namePart.value ? namePart.value : null;
} catch (error) {
return null;
}
}

function getMomentTimeZoneName(timeZone) {
if (!moment || !moment.tz || !moment.tz.zone) {
return null;
}

if (!moment.tz.zone(timeZone)) {
return null;
}

try {
return moment.tz(new Date(), timeZone).format('z');
} catch (error) {
return null;
}
}

function getTimeZoneAbbreviationMap() {
if (timezoneAbbreviationMap) return timezoneAbbreviationMap;
timezoneAbbreviationMap = new Map();
if (!moment || !moment.tz || typeof moment.tz.names !== 'function') {
return timezoneAbbreviationMap;
}

moment.tz.names().forEach((zoneName) => {
try {
const abbr = moment.tz(new Date(), zoneName).format('z');
if (!abbr) return;
const normalized = abbr.toUpperCase();
if (!timezoneAbbreviationMap.has(normalized)) {
timezoneAbbreviationMap.set(normalized, zoneName);
}
} catch (error) {
// ignore invalid timezone data
}
});

return timezoneAbbreviationMap;
}

function resolveTimeZoneAbbreviationName(abbreviation) {
const normalized = abbreviation.toUpperCase();
if (TIMEZONE_ABBREVIATION_LONG_NAMES[normalized]) {
return TIMEZONE_ABBREVIATION_LONG_NAMES[normalized];
}

const map = getTimeZoneAbbreviationMap();
const zoneName = map.get(normalized);
if (!zoneName) return null;

return (
getIntlTimeZoneName(zoneName, 'long')
|| getIntlTimeZoneName(zoneName, 'longGeneric')
);
}

function formatUtcOffset(sign, hours, minutes) {
const hourValue = Number(hours);
const minuteValue = Number(minutes || 0);

if (Number.isNaN(hourValue) || Number.isNaN(minuteValue)) {
return null;
}

const normalizedHours = String(Math.abs(hourValue)).padStart(2, '0');
const normalizedMinutes = String(Math.abs(minuteValue)).padStart(2, '0');
const suffix = normalizedMinutes !== '00' ? `:${normalizedMinutes}` : '';

return `UTC${sign}${normalizedHours}${suffix}`;
}

function normalizeUtcOffset(value) {
if (!value) return null;
const normalized = String(value).trim();
if (!normalized) return null;

if (/^(utc|gmt)$/i.test(normalized)) {
return 'UTC';
}

const offsetMatch = normalized.match(OFFSET_TZ_PATTERN);
if (offsetMatch) {
return formatUtcOffset(offsetMatch[1], offsetMatch[2], offsetMatch[3]);
}

const bareMatch = normalized.match(BARE_OFFSET_PATTERN);
if (bareMatch) {
return formatUtcOffset(bareMatch[1], bareMatch[2], bareMatch[3]);
}

return null;
}

function normalizeTimezoneValue(value) {
const normalizedValue = normalizeLocationValue(value);
if (!normalizedValue) return null;

const trimmed = normalizedValue.trim();
if (!trimmed) return null;

if (trimmed.toLowerCase() === 'any') {
return 'Any';
}

const longName = getIntlTimeZoneName(trimmed, 'long') || getIntlTimeZoneName(trimmed, 'longGeneric');
if (longName) {
return longName;
}

const offset = normalizeUtcOffset(trimmed);
if (offset) {
return offset;
}

if (SIMPLE_TZ_PATTERN.test(trimmed)) {
return resolveTimeZoneAbbreviationName(trimmed) || trimmed.toUpperCase();
}

const fallbackShortName = getMomentTimeZoneName(trimmed) || getIntlTimeZoneName(trimmed, 'short');
if (fallbackShortName) {
return fallbackShortName;
}

return trimmed;
}

function uniqNormalizedStrings(values) {
const seen = new Set();
return values.reduce((acc, value) => {
Expand Down Expand Up @@ -365,10 +153,6 @@ function getWorkloadDisplay(workload) {
return normalizeLabel(workload, WORKLOAD_LABELS);
}

function getAnticipatedStartDisplay(value) {
return normalizeLabel(value, ANTICIPATED_START_LABELS);
}

function getCompensationDisplay(compensationRange) {
if (typeof compensationRange === 'object' && compensationRange !== null) {
const label = compensationRange.name || compensationRange.title;
Expand Down Expand Up @@ -406,17 +190,10 @@ function EngagementCard({ engagement }) {
role,
workload,
compensationRange,
skills: engagementSkills,
requiredSkills,
skillsets,
location,
locations: engagementLocations,
timezone,
timezones,
timeZones,
countries,
status,
anticipatedStart,
nanoId,
id,
engagementId,
Expand All @@ -431,44 +208,18 @@ function EngagementCard({ engagement }) {
durationWeeks,
durationMonths,
);
const anticipatedStartText = getAnticipatedStartDisplay(anticipatedStart);

const skillsSource = [engagementSkills, requiredSkills, skillsets]
.find(value => Array.isArray(value) && value.length)
|| engagementSkills
|| requiredSkills
|| skillsets;
const skills = Array.from(new Set(
asArray(skillsSource)
.map(normalizeSkillLabel)
.filter(Boolean),
));
const skillsText = skills.length
? skills.slice(0, 2).join(', ')
: 'Not Specified';
const limitedSkillsText = skills.length > 2
? `${skillsText},...`
: skillsText;

const baseLocations = [
...asArray(location),
...asArray(engagementLocations),
]
.map(normalizeRegionValue)
.filter(Boolean);
const timezoneValues = [
...asArray(timezone),
...asArray(timezones),
...asArray(timeZones),
]
.map(normalizeTimezoneValue)
.filter(Boolean);
const countryValues = asArray(countries)
.map(normalizeRegionValue)
.filter(Boolean);
const isAnyValue = value => value.trim().toLowerCase() === 'any';
const hasAnyLocation = [...baseLocations, ...countryValues].some(isAnyValue);
const hasAnyTimezone = timezoneValues.some(isAnyValue);
const filteredBaseLocations = baseLocations.filter(value => !isAnyValue(value));
const filteredCountries = countryValues.filter(value => !isAnyValue(value));
const locations = uniqNormalizedStrings([
Expand All @@ -477,15 +228,6 @@ function EngagementCard({ engagement }) {
...filteredCountries,
]);
const locationText = locations.length ? locations.join(', ') : 'Remote';
const filteredTimezones = uniqNormalizedStrings(
timezoneValues.filter(value => !isAnyValue(value)),
);
let timezoneText = 'Not Specified';
if (filteredTimezones.length) {
timezoneText = filteredTimezones.join(', ');
} else if (hasAnyTimezone) {
timezoneText = 'Any';
}

const resolvedEngagementId = nanoId || id || engagementId;
const engagementLink = resolvedEngagementId
Expand All @@ -505,12 +247,6 @@ function EngagementCard({ engagement }) {
<div styleName="icon-val">
<img src={iconBlackSkills} alt="role-icon" /> {getRoleDisplay(role)}
</div>
<div styleName="icon-val">
<img src={iconBlackSkills} alt="skills-icon" /> {limitedSkillsText}
</div>
<div styleName="icon-val">
<IconTimezone /> {timezoneText}
</div>
<div styleName="icon-val">
<IconBlackLocation /> {locationText}
</div>
Expand All @@ -523,9 +259,6 @@ function EngagementCard({ engagement }) {
<div styleName="icon-val">
<IconBlackDuration /> {durationText}
</div>
<div styleName="icon-val">
<IconBlackDuration /> {`Anticipated start: ${anticipatedStartText}`}
</div>
<div styleName="row-btn">
<a styleName="primary-green-md" href={engagementLink}>
VIEW DETAILS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@

.job-infos {
display: grid;
grid-template-columns: 1.2fr 1fr 1.2fr 1.2fr 1fr 1.3fr 0.9fr 1fr 141px;
grid-template-columns: 1.2fr 1.2fr 1fr 1.3fr 0.9fr 141px;

Choose a reason for hiding this comment

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

[⚠️ correctness]
The change in grid-template-columns reduces the number of columns from 9 to 6. Ensure that this change aligns with the removal of the skills, start date, and time zone columns as described in the PR. Verify that the remaining columns are correctly aligned and that this change does not affect the layout negatively on different screen sizes.

column-gap: 20px;
align-items: center;

Expand Down
Loading