diff --git a/frontend/app/src/people/interfaces.ts b/frontend/app/src/people/interfaces.ts index 523fe1cb5..5b9bdbc0c 100644 --- a/frontend/app/src/people/interfaces.ts +++ b/frontend/app/src/people/interfaces.ts @@ -407,6 +407,11 @@ export interface BountyHeaderProps { checkboxIdToSelectedMapLanguage: any; } +export interface PeopleHeaderProps { + onChangeLanguage: (number) => void; + checkboxIdToSelectedMapLanguage: any; +} + export interface DeleteTicketModalProps { closeModal: () => void; confirmDelete: () => void; diff --git a/frontend/app/src/people/main/Body.tsx b/frontend/app/src/people/main/Body.tsx index 195053218..747a06e8b 100644 --- a/frontend/app/src/people/main/Body.tsx +++ b/frontend/app/src/people/main/Body.tsx @@ -3,6 +3,9 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; import styled from 'styled-components'; import { EuiLoadingSpinner, EuiGlobalToastList } from '@elastic/eui'; +import PeopleHeader from 'people/widgetViews/PeopleHeader'; +import { Person as PersonType } from 'store/main'; +import filterByCodingLanguage from 'people/utils/filterPeople'; import { SearchTextInput } from '../../components/common'; import { colors } from '../../config/colors'; import { useFuse, useIsMobile, usePageScroll, useScreenWidth } from '../../hooks'; @@ -24,6 +27,7 @@ const Body = styled.div<{ isMobile: boolean }>` & > .header { display: flex; justify-content: flex-end; + gap: 8px; padding: 10px 0; } & > .content { @@ -50,6 +54,8 @@ function BodyComponent() { const [loading, setLoading] = useState(true); const screenWidth = useScreenWidth(); const [openStartUpModel, setOpenStartUpModel] = useState(false); + const [checkboxIdToSelectedMapLanguage, setCheckboxIdToSelectedMapLanguage] = useState({}); + const [filterResult, setFilterResult] = useState(main.people); const closeModal = () => setOpenStartUpModel(false); const { peoplePageNumber } = ui; const history = useHistory(); @@ -71,6 +77,16 @@ function BodyComponent() { const loadBackwardFunc = () => loadMore(-1); const { loadingBottom, handleScroll } = usePageScroll(loadForwardFunc, loadBackwardFunc); + const onChangeLanguage = (optionId: any) => { + const newCheckboxIdToSelectedMapLanguage = { + ...checkboxIdToSelectedMapLanguage, + ...{ + [optionId]: !checkboxIdToSelectedMapLanguage[optionId] + } + }; + setCheckboxIdToSelectedMapLanguage(newCheckboxIdToSelectedMapLanguage); + }; + const toastsEl = ( { + setFilterResult(filterByCodingLanguage(main.people, checkboxIdToSelectedMapLanguage)); + }, [checkboxIdToSelectedMapLanguage]); + // update search useEffect(() => { (async () => { @@ -116,6 +136,11 @@ function BodyComponent() { }} >
+ +
- {(people ?? []).map((t: any) => ( + {(ui.searchText ? people : filterResult).map((t: any) => ( ))} - {!people.length && } + {!(ui.searchText ? people : filterResult)?.length && }
diff --git a/frontend/app/src/people/utils/__test__/__mockData__/users.ts b/frontend/app/src/people/utils/__test__/__mockData__/users.ts new file mode 100644 index 000000000..6009d0202 --- /dev/null +++ b/frontend/app/src/people/utils/__test__/__mockData__/users.ts @@ -0,0 +1,75 @@ +import { Person } from 'store/main'; + +export const users: Person[] = [ + { + id: 1, + pubkey: 'test_pub_key', + alias: '', + contact_key: 'test_owner_contact_key', + owner_route_hint: 'test_owner_route_hint', + unique_name: 'test 1', + tags: [], + photo_url: '', + route_hint: 'test_hint:1099567661057', + price_to_meet: 0, + url: 'https://mockApi.com', + description: 'description', + verification_signature: 'test_verification_signature', + extras: { + email: [{ value: 'testEmail@sphinx.com' }], + liquid: [{ value: 'none' }], + wanted: [], + coding_languages: [{ label: 'Typescript', value: 'Typescript' }] + }, + owner_alias: 'test 1', + owner_pubkey: 'test_pub_key', + img: '/static/avatarPlaceholders/placeholder_34.jpg' + }, + { + id: 2, + pubkey: 'test_pub_key', + alias: 'test 2', + contact_key: 'test_owner_contact_key', + owner_route_hint: 'test_owner_route_hint', + unique_name: 'test 2', + tags: [], + photo_url: '', + route_hint: 'test_hint:1099567661057', + price_to_meet: 0, + url: 'https://mockApi.com', + description: 'description', + verification_signature: 'test_verification_signature', + extras: { + email: [{ value: 'testEmail@sphinx.com' }], + liquid: [{ value: 'none' }], + wanted: [], + coding_languages: [{ label: 'Java', value: 'Java' }] + }, + owner_alias: 'test 2', + owner_pubkey: 'test_pub_key', + img: '/static/avatarPlaceholders/placeholder_34.jpg' + }, + { + id: 3, + pubkey: 'test_pub_key', + alias: 'test 3', + contact_key: 'test_owner_contact_key', + owner_route_hint: 'test_owner_route_hint', + unique_name: 'test 3', + tags: [], + photo_url: '', + route_hint: 'test_hint:1099567661057', + price_to_meet: 0, + url: 'https://mockApi.com', + description: 'description', + verification_signature: 'test_verification_signature', + extras: { + email: [{ value: 'testEmail@sphinx.com' }], + liquid: [{ value: 'none' }], + wanted: [] + }, + owner_alias: 'test 3', + owner_pubkey: 'test_pub_key', + img: '/static/avatarPlaceholders/placeholder_34.jpg' + } +]; diff --git a/frontend/app/src/people/utils/__tests__/filterValidation.spec.ts b/frontend/app/src/people/utils/__test__/filterValidation.spec.ts similarity index 69% rename from frontend/app/src/people/utils/__tests__/filterValidation.spec.ts rename to frontend/app/src/people/utils/__test__/filterValidation.spec.ts index 321026833..34546362a 100644 --- a/frontend/app/src/people/utils/__tests__/filterValidation.spec.ts +++ b/frontend/app/src/people/utils/__test__/filterValidation.spec.ts @@ -1,50 +1,69 @@ -import { bountyHeaderFilter, bountyHeaderLanguageFilter } from '../filterValidation'; - -describe('testing filters', () => { - describe('bountyHeaderFilter', () => { - test('o/t/t', () => { - expect(bountyHeaderFilter({ Open: true }, true, true)).toEqual(false); - }); - test('a/t/t', () => { - expect(bountyHeaderFilter({ Assigned: true }, true, true)).toEqual(false); - }); - test('p/t/t', () => { - expect(bountyHeaderFilter({ Paid: true }, true, true)).toEqual(true); - }); - test('/t/t', () => { - expect(bountyHeaderFilter({}, true, true)).toEqual(true); - }); - test('o/f/t', () => { - expect(bountyHeaderFilter({ Open: true }, false, true)).toEqual(false); - }); - test('a/f/t', () => { - expect(bountyHeaderFilter({ Assigned: true }, false, true)).toEqual(true); - }); - test('p/f/t', () => { - expect(bountyHeaderFilter({ Paid: true }, false, true)).toEqual(false); - }); - }); - describe('bountyHeaderLanguageFilter', () => { - test('match', () => { - expect(bountyHeaderLanguageFilter(['Javascript', 'Python'], { Javascript: true })).toEqual( - true - ); - }); - test('no-match', () => { - expect( - bountyHeaderLanguageFilter(['Javascript'], { Python: true, Javascript: false }) - ).toEqual(false); - }); - test('no filters', () => { - expect(bountyHeaderLanguageFilter(['Javascript'], {})).toEqual(true); - }); - test('no languages', () => { - expect(bountyHeaderLanguageFilter([], { Javascript: true })).toEqual(false); - }); - test('false filters', () => { - expect( - bountyHeaderLanguageFilter(['Javascript'], { Javascript: false, Python: false }) - ).toEqual(true); - }); - }); -}); +import { bountyHeaderFilter, bountyHeaderLanguageFilter } from '../filterValidation'; +import filterByCodingLanguage from '../filterPeople'; +import { users } from '../__test__/__mockData__/users'; + +describe('testing filters', () => { + describe('bountyHeaderFilter', () => { + test('o/t/t', () => { + expect(bountyHeaderFilter({ Open: true }, true, true)).toEqual(false); + }); + test('a/t/t', () => { + expect(bountyHeaderFilter({ Assigned: true }, true, true)).toEqual(false); + }); + test('p/t/t', () => { + expect(bountyHeaderFilter({ Paid: true }, true, true)).toEqual(true); + }); + test('/t/t', () => { + expect(bountyHeaderFilter({}, true, true)).toEqual(true); + }); + test('o/f/t', () => { + expect(bountyHeaderFilter({ Open: true }, false, true)).toEqual(false); + }); + test('a/f/t', () => { + expect(bountyHeaderFilter({ Assigned: true }, false, true)).toEqual(true); + }); + test('p/f/t', () => { + expect(bountyHeaderFilter({ Paid: true }, false, true)).toEqual(false); + }); + }); + describe('bountyHeaderLanguageFilter', () => { + test('match', () => { + expect(bountyHeaderLanguageFilter(['Javascript', 'Python'], { Javascript: true })).toEqual( + true + ); + }); + test('no-match', () => { + expect( + bountyHeaderLanguageFilter(['Javascript'], { Python: true, Javascript: false }) + ).toEqual(false); + }); + test('no filters', () => { + expect(bountyHeaderLanguageFilter(['Javascript'], {})).toEqual(true); + }); + test('no languages', () => { + expect(bountyHeaderLanguageFilter([], { Javascript: true })).toEqual(false); + }); + test('false filters', () => { + expect( + bountyHeaderLanguageFilter(['Javascript'], { Javascript: false, Python: false }) + ).toEqual(true); + }); + }); + describe('peopleHeaderCodingLanguageFilters', () => { + test('match', () => { + expect(filterByCodingLanguage(users, { Typescript: true })).toStrictEqual([users[0]]); + }); + test('no_match', () => { + expect(filterByCodingLanguage(users, { Rust: true })).toStrictEqual([]); + }); + test('no filters', () => { + expect(filterByCodingLanguage(users, {})).toEqual(users); + }); + test('false filters', () => { + expect(filterByCodingLanguage(users, { PHP: false, MySQL: false })).toStrictEqual(users); + }); + test('no users', () => { + expect(filterByCodingLanguage([], { Typescript: true })).toStrictEqual([]); + }); + }); +}); diff --git a/frontend/app/src/people/utils/filterPeople.ts b/frontend/app/src/people/utils/filterPeople.ts new file mode 100644 index 000000000..8e20fddd2 --- /dev/null +++ b/frontend/app/src/people/utils/filterPeople.ts @@ -0,0 +1,22 @@ +import { Person } from 'store/main'; + +interface CodingLanguage { + [language: string]: boolean; +} + +const filterByCodingLanguage = (users: Person[], codingLanguages: CodingLanguage) => { + const requiredLanguages = Object.keys(codingLanguages).filter( + (key: string) => codingLanguages[key] + ); + + return users.filter((user: Person) => { + const userCodingLanguages = (user.extras.coding_languages ?? []).map( + (t: { [key: string]: string }) => t.value + ); + return requiredLanguages?.every((requiredLanguage: string) => + userCodingLanguages.includes(requiredLanguage) + ); + }); +}; + +export default filterByCodingLanguage; diff --git a/frontend/app/src/people/widgetViews/PeopleHeader.tsx b/frontend/app/src/people/widgetViews/PeopleHeader.tsx new file mode 100644 index 000000000..35427a5af --- /dev/null +++ b/frontend/app/src/people/widgetViews/PeopleHeader.tsx @@ -0,0 +1,247 @@ +import styled from 'styled-components'; +import { PeopleHeaderProps } from 'people/interfaces'; +import { observer } from 'mobx-react-lite'; +import React, { useState, useEffect } from 'react'; +import { EuiCheckboxGroup, EuiPopover, EuiText } from '@elastic/eui'; +import MaterialIcon from '@material/react-material-icon'; +import { colors } from 'config'; +import { filterCount } from 'people/utils/ExtraFunctions'; +import { GetValue, coding_languages } from '../utils/languageLabelStyle'; + +interface styledProps { + color?: any; +} + +const FilterWrapper = styled.div` + display: flex; + align-items: center; + gap: 4px; +`; + +const FilterTrigger = styled.div` + width: 78px; + height: 48px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + margin-left: 19px; + cursor: pointer; + user-select: none; + .filterImageContainer { + display: flex; + justify-content: center; + align-items: center; + height: 48px; + width: 36px; + .materialIconImage { + color: ${(p: any) => p.color && p.color.grayish.G200}; + cursor: pointer; + font-size: 18px; + margin-top: 4px; + } + } + .filterText { + font-family: 'Barlow'; + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 19px; + display: flex; + align-items: center; + color: ${(p: any) => p.color && p.color.grayish.G200}; + } + &:hover { + .filterImageContainer { + .materialIconImage { + color: ${(p: any) => p.color && p.color.grayish.G50} !important; + cursor: pointer; + font-size: 18px; + margin-top: 4px; + } + } + .filterText { + color: ${(p: any) => p.color && p.color.grayish.G50}; + } + } + &:active { + .filterImageContainer { + .materialIconImage { + color: ${(p: any) => p.color && p.color.grayish.G10} !important; + cursor: pointer; + font-size: 18px; + margin-top: 4px; + } + } + .filterText { + color: ${(p: any) => p.color && p.color.grayish.G10}; + } + } +`; + +const FilterCount = styled.div` + height: 20px; + width: 20px; + border-radius: 50%; + margin-left: 4px; + display: flex; + justify-content: center; + align-items: center; + margin-top: -5px; + background: ${(p: any) => p?.color && p.color.blue1}; + .filterCountText { + font-family: 'Barlow'; + font-style: normal; + font-weight: 500; + font-size: 13px; + display: flex; + align-items: center; + text-align: center; + color: ${(p: any) => p.color && p.color.pureWhite}; + } +`; + +const PopOverBox = styled.div` + display: flex; + flex-direction: column; + max-height: 304px; + padding: 15px 0px 20px 21px; + .rightBoxHeading { + font-family: 'Barlow'; + font-style: normal; + font-weight: 700; + font-size: 12px; + line-height: 32px; + text-transform: uppercase; + color: ${(p: any) => p.color && p.color.grayish.G100}; + } +`; + +const EuiPopOverCheckboxWrapper = styled.div` + min-width: 285px; + max-width: 285px; + height: 240px; + user-select: none; + + &.CheckboxOuter > div { + height: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + justify-content: center; + .euiCheckboxGroup__item { + .euiCheckbox__square { + top: 5px; + border: 1px solid ${(p: any) => p?.color && p?.color?.grayish.G500}; + border-radius: 2px; + } + .euiCheckbox__input + .euiCheckbox__square { + background: ${(p: any) => p?.color && p?.color?.pureWhite} no-repeat center; + } + .euiCheckbox__input:checked + .euiCheckbox__square { + border: 1px solid ${(p: any) => p?.color && p?.color?.blue1}; + background: ${(p: any) => p?.color && p?.color?.blue1} no-repeat center; + background-image: url('static/checkboxImage.svg'); + } + .euiCheckbox__label { + font-family: 'Barlow'; + font-style: normal; + font-weight: 500; + font-size: 13px; + line-height: 16px; + color: ${(p: any) => p?.color && p?.color?.grayish.G50}; + &:hover { + color: ${(p: any) => p?.color && p?.color?.grayish.G05}; + } + } + input.euiCheckbox__input:checked ~ label { + color: ${(p: any) => p?.color && p?.color?.blue1}; + } + } + } +`; + +const Coding_Languages = GetValue(coding_languages); + +const PeopleHeader = ({ onChangeLanguage, checkboxIdToSelectedMapLanguage }: PeopleHeaderProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [filterCountNumber, setFilterCountNumber] = useState(0); + const onToggleButton = () => setIsPopoverOpen((prev: boolean) => !prev); + const closePopover = () => setIsPopoverOpen(false); + const color = colors['light']; + + useEffect(() => { + setFilterCountNumber(filterCount(checkboxIdToSelectedMapLanguage)); + }, [checkboxIdToSelectedMapLanguage]); + + const panelStyles = { + border: 'none', + boxShadow: `0px 1px 20px ${color.black90}`, + background: `${color.pureWhite}`, + borderRadius: '6px', + minWidth: '300px', + minHeight: '304px', + marginTop: '0px', + marginLeft: '20px' + }; + + return ( + + +
+ +
+ + Filter + + + } + panelStyle={panelStyles} + isOpen={isPopoverOpen} + closePopover={closePopover} + panelClassName="yourClassNameHere" + panelPaddingSize="none" + anchorPosition="downLeft" + > +
+ + Skills + + { + onChangeLanguage(id); + }} + /> + + +
+
+ {filterCountNumber > 0 && ( + + {filterCountNumber} + + )} +
+ ); +}; + +export default observer(PeopleHeader);