Skip to content

Commit c01b210

Browse files
Merge pull request #7193 from hotosm/feat/7053-start-indicator
Display the stars as per mapping level beside the username
2 parents 7a37ead + 3ecb6f6 commit c01b210

File tree

4 files changed

+108
-41
lines changed

4 files changed

+108
-41
lines changed

frontend/src/components/svgIcons/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export { ExitIcon } from './exit';
2323
export { EyeIcon } from './eye';
2424
export { HomeIcon } from './home';
2525
export { LockIcon } from './lock';
26-
export { StarIcon, FullStarIcon, HalfStarIcon } from './star';
26+
export { StarIcon, FullStarIcon, HalfStarIcon, FullStarIconYellow, FullStarIconBlue } from './star';
2727
export { GripIcon } from './grip';
2828
export { GithubIcon } from './github';
2929
export { RightIcon } from './right';

frontend/src/components/svgIcons/star.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,28 @@ export class HalfStarIcon extends PureComponent {
4040
);
4141
}
4242
}
43+
44+
export class FullStarIconYellow extends PureComponent {
45+
render() {
46+
return (
47+
<svg viewBox="0 0 576 512" {...this.props}>
48+
<path
49+
fill="#ff8800ff"
50+
d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"
51+
></path>
52+
</svg>
53+
);
54+
}
55+
}
56+
export class FullStarIconBlue extends PureComponent {
57+
render() {
58+
return (
59+
<svg viewBox="0 0 576 512" {...this.props}>
60+
<path
61+
fill="#023e70e2"
62+
d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"
63+
></path>
64+
</svg>
65+
);
66+
}
67+
}

frontend/src/components/taskSelection/contributions.js

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,46 +12,65 @@ import {
1212
MappedIcon,
1313
ValidatedIcon,
1414
AsteriskIcon,
15+
StarIcon,
1516
HalfStarIcon,
1617
FullStarIcon,
18+
FullStarIconYellow,
1719
ChartLineIcon,
20+
FullStarIconBlue,
1821
} from '../svgIcons';
1922
import ProjectProgressBar from '../projectCard/projectProgressBar';
2023
import { useComputeCompleteness } from '../../hooks/UseProjectCompletenessCalc';
2124
import { useFilterContributors } from '../../hooks/UseFilterContributors';
2225
import { OSMChaButton } from '../projectDetail/osmchaButton';
2326
import { useProjectContributionsLevelQuery } from '../../api/projects.js';
2427

25-
export const MappingLevelIcon = ({ mappingLevel }) => {
26-
if (!mappingLevel) {
28+
export const MappingLevelIcon = ({ mappingLevel, mappingLevelList }: Object) => {
29+
if (!mappingLevel || !mappingLevelList || mappingLevelList.length === 0) {
2730
return null;
2831
}
32+
33+
// Sort the mapping levels by ordering
34+
const sortedLevels = [...mappingLevelList].sort((a, b) => a.ordering - b.ordering);
35+
36+
// Find the current mapping level in the sorted list
2937
const upperCaseLevelStr = mappingLevel.toUpperCase();
30-
let level = null;
38+
const currentLevel = sortedLevels.find((level) => level.name.toUpperCase() === upperCaseLevelStr);
3139

32-
if (upperCaseLevelStr.includes('ADVANCED')) {
33-
level = 'ADVANCED';
34-
} else if (upperCaseLevelStr.includes('INTERMEDIATE')) {
35-
level = 'INTERMEDIATE';
40+
if (!currentLevel) {
41+
return null;
3642
}
3743

38-
if (level) {
39-
return (
40-
<FormattedMessage {...messages[`mappingLevel${level}`]}>
41-
{(msg) => (
42-
<span className="blue-grey ttl" title={msg}>
43-
{level === 'ADVANCED' ? (
44-
<FullStarIcon className="h1 w1 v-mid pb1" />
45-
) : (
46-
<HalfStarIcon className="h1 w1 v-mid pb1" />
47-
)}
48-
</span>
49-
)}
50-
</FormattedMessage>
51-
);
52-
}
44+
// Determine which icon to show based on position in sorted array
45+
const getStarIcon = () => {
46+
// Find the index (position) of the current level in the sorted array
47+
const currentIndex = sortedLevels.findIndex((level) => level.name === currentLevel.name);
48+
const lastIndex = sortedLevels.length - 1;
5349

54-
return null;
50+
// First 3 positions (indices 0, 1, 2)
51+
if (currentIndex === 0) {
52+
// Empty star for first position
53+
return <StarIcon className="h1 w1 v-mid pb1" />;
54+
} else if (currentIndex === 1) {
55+
// Half star for second position
56+
return <HalfStarIcon className="h1 w1 v-mid pb1" />;
57+
} else if (currentIndex === 2) {
58+
// Full star for third position
59+
return <FullStarIcon className="h1 w1 v-mid pb1" />;
60+
} else if (currentIndex === lastIndex) {
61+
// Yellow star for last position (highest level)
62+
return <FullStarIconYellow className="h1 w1 v-mid pb1" />;
63+
} else {
64+
// Full stars for anything in between
65+
return <FullStarIconBlue className="h1 w1 v-mid pb1" />;
66+
}
67+
};
68+
69+
return (
70+
<span className="blue-grey ttl" title={currentLevel.name}>
71+
{getStarIcon()}
72+
</span>
73+
);
5574
};
5675

5776
const sortByLits = [
@@ -95,7 +114,7 @@ const SortingHeader = ({ sortBy, setSortBy }: Object) => {
95114
);
96115
};
97116

98-
function Contributor({ user, activeUser, activeStatus, displayTasks }: Object) {
117+
function Contributor({ user, activeUser, activeStatus, displayTasks, mappingLevelList }: Object) {
99118
const intl = useIntl();
100119
const checkActiveUserAndStatus = (status, username) =>
101120
activeStatus === status && activeUser === username ? 'bg-blue-dark' : 'bg-grey-light';
@@ -123,7 +142,7 @@ function Contributor({ user, activeUser, activeStatus, displayTasks }: Object) {
123142
</>
124143
)}
125144
</FormattedMessage>
126-
{/* <MappingLevelIcon mappingLevel={user.mappingLevel} /> */}
145+
<MappingLevelIcon mappingLevel={user.mappingLevel} mappingLevelList={mappingLevelList} />
127146
</div>
128147

129148
<div className="w-20 fl tr dib truncate">
@@ -170,9 +189,16 @@ function Contributor({ user, activeUser, activeStatus, displayTasks }: Object) {
170189
);
171190
}
172191

173-
const Contributions = ({ project, tasks, contribsData, activeUser, activeStatus, selectTask }) => {
192+
const Contributions = ({
193+
project,
194+
tasks,
195+
contribsData,
196+
activeUser,
197+
activeStatus,
198+
selectTask,
199+
}: Object) => {
174200
const intl = useIntl();
175-
const { data } = useProjectContributionsLevelQuery();
201+
const { data: mappingLevelList } = useProjectContributionsLevelQuery();
176202

177203
const mappingLevels = useMemo(() => {
178204
const getLevelLabel = (level) => {
@@ -183,7 +209,7 @@ const Contributions = ({ project, tasks, contribsData, activeUser, activeStatus,
183209
};
184210

185211
const dynamicLevels =
186-
data?.map((level) => ({
212+
mappingLevelList?.map((level) => ({
187213
value: level.name,
188214
label: getLevelLabel(level.name || ''),
189215
})) || [];
@@ -193,7 +219,7 @@ const Contributions = ({ project, tasks, contribsData, activeUser, activeStatus,
193219
...dynamicLevels,
194220
{ value: 'NEWUSER', label: intl.formatMessage(messages.mappingLevelNEWUSER) },
195221
];
196-
}, [data, intl]);
222+
}, [mappingLevelList, intl]);
197223

198224
const defaultUserFilter = {
199225
label: intl.formatMessage(messages.userFilterDefaultLabel),
@@ -279,6 +305,7 @@ const Contributions = ({ project, tasks, contribsData, activeUser, activeStatus,
279305
activeStatus={activeStatus}
280306
displayTasks={displayTasks}
281307
key={k}
308+
mappingLevelList={mappingLevelList}
282309
/>
283310
))}
284311
</ReactPlaceholder>
Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,49 @@
1-
import { FormattedMessage } from 'react-intl';
2-
31
import { createComponentWithIntl } from '../../../utils/testWithIntl';
42
import { MappingLevelIcon } from '../contributions';
5-
import { FullStarIcon, HalfStarIcon } from '../../svgIcons';
3+
import { FullStarIcon, FullStarIconYellow, HalfStarIcon } from '../../svgIcons';
4+
5+
const mappingLevelList = [
6+
{ name: 'BEGINNER', ordering: 1 },
7+
{ name: 'INTERMEDIATE', ordering: 2 },
8+
{ name: 'ADVANCED', ordering: 3 },
9+
{ name: 'SUPER_MAPPER', ordering: 4 },
10+
];
611

712
describe('if mappingLevel is "Intermediate mapper"', () => {
8-
const element = createComponentWithIntl(<MappingLevelIcon mappingLevel="INTERMEDIATE" />);
13+
const element = createComponentWithIntl(
14+
<MappingLevelIcon mappingLevel="INTERMEDIATE" mappingLevelList={mappingLevelList} />,
15+
);
916
const instance = element.root;
1017
it('HalfStarIcon with correct classNames', () => {
1118
expect(instance.findByType(HalfStarIcon).props.className).toBe('h1 w1 v-mid pb1');
1219
});
13-
it('FormattedMessage with the correct id', () => {
14-
expect(instance.findByType(FormattedMessage).props.id).toBe('project.level.intermediate');
15-
});
1620
});
1721

1822
describe('if mappingLevel is "Advanced mapper"', () => {
19-
const element = createComponentWithIntl(<MappingLevelIcon mappingLevel="ADVANCED" />);
23+
const element = createComponentWithIntl(
24+
<MappingLevelIcon mappingLevel="ADVANCED" mappingLevelList={mappingLevelList} />,
25+
);
2026
const instance = element.root;
2127
it('should render a FullStarIcon', () => {
2228
expect(instance.findByType(FullStarIcon).props.className).toBe('h1 w1 v-mid pb1');
2329
});
24-
it('should render a FormattedMessage with the correct id', () => {
25-
expect(instance.findByType(FormattedMessage).props.id).toBe('project.level.advanced');
30+
});
31+
32+
describe('if mappingLevel is "Super Mapper"', () => {
33+
const element = createComponentWithIntl(
34+
<MappingLevelIcon mappingLevel="SUPER_MAPPER" mappingLevelList={mappingLevelList} />,
35+
);
36+
const instance = element.root;
37+
it('should render a FullStarIconYellow', () => {
38+
expect(instance.findByType(FullStarIconYellow).props.className).toBe('h1 w1 v-mid pb1');
2639
});
2740
});
2841

2942
describe('if mappingLevel is anything else', () => {
3043
it('should not render anything', () => {
31-
const element = createComponentWithIntl(<MappingLevelIcon mappingLevel="BEGINNER" />);
44+
const element = createComponentWithIntl(
45+
<MappingLevelIcon mappingLevel="MAPPER" mappingLevelList={mappingLevelList} />,
46+
);
3247
expect(element.toJSON()).toBeNull();
3348
});
3449
});

0 commit comments

Comments
 (0)