| @@ -6,6 +6,7 @@ import history from "./store/utils/history"; | |||
| import MainContainer from "./components/Section/MainContainer"; | |||
| import AppRoutes from "./AppRoutes"; | |||
| /* istanbul ignore file */ | |||
| function App() { | |||
| return ( | |||
| <> | |||
| @@ -5,6 +5,15 @@ import store from "../../store"; | |||
| import { mockState } from "../../mockState"; | |||
| import { Router } from "react-router-dom"; | |||
| import history from "../../store/utils/history"; | |||
| import mediaQuery from "css-mediaquery"; | |||
| function createMatchMedia(width) { | |||
| return (query) => ({ | |||
| matches: mediaQuery.match(query, { width }), | |||
| addListener: () => {}, | |||
| removeListener: () => {}, | |||
| }); | |||
| } | |||
| describe("CandidatesPage render tests", () => { | |||
| const cont = ( | |||
| @@ -18,23 +27,40 @@ describe("CandidatesPage render tests", () => { | |||
| let spyOnUseSelector; | |||
| beforeEach(() => { | |||
| window.matchMedia = createMatchMedia(601); | |||
| spyOnUseSelector = jest.spyOn(redux, "useSelector"); | |||
| spyOnUseSelector | |||
| .mockReturnValueOnce(mockState.technologies.technologies) | |||
| spyOnUseSelector.mockReturnValueOnce(mockState.technologies.technologies); | |||
| }); | |||
| afterEach(() => { | |||
| jest.restoreAllMocks(); | |||
| }); | |||
| it("Should render first header because width of screen is greater than 600", () => { | |||
| render(cont); | |||
| expect(screen.getByTestId("candidates-header1")).toBeDefined(); | |||
| }); | |||
| it("Should render second header because width of screen is greater than 600", () => { | |||
| render(cont); | |||
| expect(screen.queryByTestId("candidates-header2")).toBeNull(); | |||
| }); | |||
| it("Should render", () => { | |||
| render(cont); | |||
| expect(screen.getByTestId("candidates-page")).toBeDefined(); | |||
| }); | |||
| it("Should render button responsible for showing different components inside page", () => { | |||
| it("Should render first button responsible for showing different components inside page because width of screen is greater than 600", () => { | |||
| const { container } = render(cont); | |||
| expect(container.getElementsByClassName("all-white-btn")[0]).toBeDefined(); | |||
| }); | |||
| it("Should not render second button responsible for showing different components inside page because width of screen is greater than 600", () => { | |||
| const { container } = render(cont); | |||
| expect(container.getElementsByClassName("candidate-btn")[0]).toBeDefined(); | |||
| expect( | |||
| container.getElementsByClassName("candidate-btn-view-2")[0] | |||
| ).toBeUndefined(); | |||
| }); | |||
| it("should be rendered button which is used for showing input responsible for searching by name", () => { | |||
| @@ -42,9 +68,18 @@ describe("CandidatesPage render tests", () => { | |||
| expect(container.getElementsByClassName("candidate-btn")[1]).toBeDefined(); | |||
| }); | |||
| it("Should render filter button", () => { | |||
| it("Should render first filter button because width is greater than 600", () => { | |||
| const { container } = render(cont); | |||
| expect(container.getElementsByClassName("candidate-btn")[2]).toBeDefined(); | |||
| expect( | |||
| container.getElementsByClassName("candidate-btn-filters1")[0] | |||
| ).toBeDefined(); | |||
| }); | |||
| it("Should not render second filter button because width is greater than 600", () => { | |||
| const { container } = render(cont); | |||
| expect( | |||
| container.getElementsByClassName("candidate-btn-filters2")[0] | |||
| ).toBeUndefined(); | |||
| }); | |||
| it("input for searching by name should not be shown when component is initialy rendered", () => { | |||
| @@ -7,14 +7,17 @@ import history from "../../store/utils/history"; | |||
| import DayDetailsComponent from "../../components/Schedules/DayDetailsComponent"; | |||
| import ColorModeProvider from "../../context/ColorModeContext"; | |||
| const setCurrentlySelected = jest.fn(); | |||
| const setCurrentlySelectedDay = jest.fn(); | |||
| const props = { | |||
| selectedDate: "20.12.2023", | |||
| selectionProcesses: mockState.schedule.schedule, | |||
| open: jest.fn(), | |||
| onClose: jest.fn(), | |||
| setCurrentlySelected: jest.fn(), | |||
| setCurrentlySelectedDay: jest.fn(), | |||
| currentlySelectedDay: 1, | |||
| setCurrentlySelected: setCurrentlySelected, | |||
| setCurrentlySelectedDay: setCurrentlySelectedDay, | |||
| currentlySelectedDay: 20, | |||
| numberOfDaysInMonth: 31, | |||
| history: { | |||
| replace: jest.fn(), | |||
| @@ -45,12 +48,12 @@ describe("DayDetailsComponent render tests", () => { | |||
| expect(screen.getByTestId("day-component-dialog")).toBeDefined(); | |||
| }); | |||
| it("Should render left arrow as disabled because we set that currenlty selected day is first day of month", () => { | |||
| it("Should render left arrow as enabled because currenlty selected day is not 1", () => { | |||
| render(cont); | |||
| expect(screen.getAllByTestId("day-details-left-arrow")[0]).toBeDefined(); | |||
| }); | |||
| it("Should render right arrow as enabled because we set that currenlty selected day is first day of month", () => { | |||
| it("Should render right arrow as enabled because currently selected day is not 31", () => { | |||
| render(cont); | |||
| expect(screen.getAllByTestId("day-details-right-arrow")[0]).toBeDefined(); | |||
| }); | |||
| @@ -68,4 +71,18 @@ describe("DayDetailsComponent render tests", () => { | |||
| const arg = { pathname: "/candidates/1" }; | |||
| expect(props.history.push).toHaveBeenCalledWith(arg); | |||
| }); | |||
| it("Should call function when we press right arrow", () => { | |||
| render(cont); | |||
| fireEvent.click(screen.getAllByTestId("day-details-right-arrow")[0]); | |||
| expect(setCurrentlySelected.mock.calls).toHaveLength(1); | |||
| expect(setCurrentlySelectedDay.mock.calls).toHaveLength(1); | |||
| }); | |||
| it("Should call function when we press right arrow", () => { | |||
| render(cont); | |||
| fireEvent.click(screen.getAllByTestId("day-details-left-arrow")[0]); | |||
| expect(setCurrentlySelected.mock.calls).toHaveLength(1); | |||
| expect(setCurrentlySelectedDay.mock.calls).toHaveLength(1); | |||
| }); | |||
| }); | |||
| @@ -1,20 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| const Auth = ({ children }) => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <div className="c-auth"> | |||
| <h1 className="c-auth__title">{t(`app.title`)}</h1> | |||
| {children} | |||
| </div> | |||
| ); | |||
| }; | |||
| Auth.propTypes = { | |||
| children: PropTypes.node, | |||
| }; | |||
| export default Auth; | |||
| @@ -1,40 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ReactComponent as Checked } from '../../assets/images/svg/checked.svg'; | |||
| import { ReactComponent as Unchecked } from '../../assets/images/svg/unchecked.svg'; | |||
| const Checkbox = ({ className, children, name, onChange, checked, field }) => ( | |||
| <label htmlFor={name} className={`c-checkbox ${className || ''}`}> | |||
| <input | |||
| name={name} | |||
| id={name} | |||
| className="c-checkbox__field" | |||
| type="checkbox" | |||
| checked={checked} | |||
| {...field} | |||
| onChange={onChange || field.onChange} | |||
| /> | |||
| <div className="c-checkbox__indicator"> | |||
| {checked ? ( | |||
| <Checked className="c-checkbox__icon" /> | |||
| ) : ( | |||
| <Unchecked className="c-checkbox__icon" /> | |||
| )} | |||
| </div> | |||
| <div className="c-checkbox__text">{children}</div> | |||
| </label> | |||
| ); | |||
| Checkbox.propTypes = { | |||
| children: PropTypes.node, | |||
| onChange: PropTypes.func, | |||
| checked: PropTypes.bool, | |||
| name: PropTypes.string, | |||
| field: PropTypes.shape({ | |||
| onChange: PropTypes.func, | |||
| }), | |||
| className: PropTypes.string, | |||
| }; | |||
| export default Checkbox; | |||
| @@ -1,123 +0,0 @@ | |||
| import React, { useEffect, useRef } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ErrorMessage, useField } from 'formik'; | |||
| import CurrencyInput from 'react-currency-input-field'; | |||
| import { formatMoneyNumeral } from '../../util/helpers/numeralHelpers'; | |||
| import { | |||
| PLUS_SYMBOL, | |||
| MINUS_SYMBOL, | |||
| NUMPAD_MINUS_SYMBOL, | |||
| NUMPAD_PLUS_SYMBOL, | |||
| K_KEYCODE, | |||
| } from '../../constants/keyCodeConstants'; | |||
| const CurrencyField = ({ | |||
| autoFocus, | |||
| notCentered, | |||
| notBold, | |||
| label, | |||
| onChange, | |||
| value, | |||
| ...props | |||
| }) => { | |||
| const [field, meta] = useField(props); | |||
| const inputField = useRef(null); | |||
| function styles() { | |||
| let style = 'c-currency-field'; | |||
| if (meta.error && meta.touched) { | |||
| style += ` c-currency-field--error`; | |||
| } | |||
| if (notCentered) { | |||
| style += ` c-currency-field--not-centered`; | |||
| } | |||
| if (notBold) { | |||
| style += ` c-currency-field--not-bold`; | |||
| } | |||
| return style; | |||
| } | |||
| useEffect(() => { | |||
| if (autoFocus) { | |||
| inputField.current.focus(); | |||
| } | |||
| }, [autoFocus, inputField]); | |||
| const onKeydownHandler = (event) => { | |||
| if ( | |||
| event.keyCode === MINUS_SYMBOL || | |||
| event.keyCode === PLUS_SYMBOL || | |||
| event.keyCode === NUMPAD_MINUS_SYMBOL || | |||
| event.keyCode === NUMPAD_PLUS_SYMBOL || | |||
| event.keyCode === K_KEYCODE | |||
| ) { | |||
| event.preventDefault(); | |||
| } | |||
| }; | |||
| const prefix = formatMoneyNumeral(0); | |||
| const prefixSymbol = () => { | |||
| if (prefix.includes('CAD')) { | |||
| return 'CAD '; | |||
| } | |||
| return '$'; | |||
| }; | |||
| return ( | |||
| <div className={styles()}> | |||
| {!!label && ( | |||
| <label className="c-currency-field__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| {value ? ( | |||
| <CurrencyInput | |||
| {...props} | |||
| prefix={prefixSymbol()} | |||
| onValueChange={(value) => { | |||
| onChange(value ? Number(value) : ''); | |||
| }} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| ref={inputField} | |||
| defaultValue={0} | |||
| value={value} | |||
| /> | |||
| ) : ( | |||
| <CurrencyInput | |||
| {...props} | |||
| prefix={prefixSymbol()} | |||
| onValueChange={(value) => { | |||
| onChange(value ? Number(value) : ''); | |||
| }} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| ref={inputField} | |||
| /> | |||
| )} | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => ( | |||
| <span className="c-currency-field__error">{errorMessage}</span> | |||
| )} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| CurrencyField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| disabled: PropTypes.bool, | |||
| onChange: PropTypes.func, | |||
| autoFocus: PropTypes.bool, | |||
| notCentered: PropTypes.bool, | |||
| notBold: PropTypes.bool, | |||
| value: PropTypes.number, | |||
| }; | |||
| export default CurrencyField; | |||
| @@ -1,33 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| const EmailField = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| ...props | |||
| }) => ( | |||
| <BaseInputField | |||
| type="email" | |||
| label={label} | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| {...props} | |||
| /> | |||
| ); | |||
| EmailField.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.string, | |||
| disabled: PropTypes.bool, | |||
| }; | |||
| export default EmailField; | |||
| @@ -1,74 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| import { | |||
| PERIOD_SYMBOL, | |||
| COMMA_SYMBOL, | |||
| PLUS_SYMBOL, | |||
| MINUS_SYMBOL, | |||
| NUMPAD_PERIOD_SYMBOL, | |||
| NUMPAD_MINUS_SYMBOL, | |||
| NUMPAD_PLUS_SYMBOL, | |||
| DOWN_ARROW_KEYCODE, | |||
| UP_ARROW_KEYCODE, | |||
| } from '../../constants/keyCodeConstants'; | |||
| const NumberField = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| preventAllExceptNumbers, | |||
| ...props | |||
| }) => { | |||
| const onKeydownHandler = (event) => { | |||
| if (preventAllExceptNumbers) { | |||
| if ( | |||
| event.keyCode === PERIOD_SYMBOL || | |||
| event.keyCode === COMMA_SYMBOL || | |||
| event.keyCode === NUMPAD_PERIOD_SYMBOL || | |||
| event.keyCode === DOWN_ARROW_KEYCODE || | |||
| event.keyCode === UP_ARROW_KEYCODE | |||
| ) { | |||
| event.preventDefault(); | |||
| } | |||
| } | |||
| if ( | |||
| event.keyCode === PLUS_SYMBOL || | |||
| event.keyCode === MINUS_SYMBOL || | |||
| event.keyCode === NUMPAD_MINUS_SYMBOL || | |||
| event.keyCode === NUMPAD_PLUS_SYMBOL || | |||
| event.keyCode === DOWN_ARROW_KEYCODE || | |||
| event.keyCode === UP_ARROW_KEYCODE | |||
| ) { | |||
| event.preventDefault(); | |||
| } | |||
| }; | |||
| return ( | |||
| <BaseInputField | |||
| type="number" | |||
| label={label} | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| {...props} | |||
| onKeyDown={(event) => onKeydownHandler(event)} | |||
| /> | |||
| ); | |||
| }; | |||
| NumberField.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), | |||
| disabled: PropTypes.bool, | |||
| preventAllExceptNumbers: PropTypes.bool, | |||
| }; | |||
| export default NumberField; | |||
| @@ -1,45 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import NumberFormat from 'react-number-format'; | |||
| import TextField from './TextField'; | |||
| const PercentageField = ({ field, ...props }) => { | |||
| const handleOnChange = (percentageField) => { | |||
| const { floatValue } = percentageField; | |||
| if (!props.onChange) { | |||
| throw Error('Provide an onChange handler'); | |||
| } | |||
| if (floatValue > 100) { | |||
| return props.onChange('100'); | |||
| } | |||
| if (floatValue <= 0 || !floatValue) { | |||
| return props.onChange('0'); | |||
| } | |||
| return props.onChange(floatValue.toString()); | |||
| }; | |||
| return ( | |||
| <NumberFormat | |||
| format="###%" | |||
| value={field.value} | |||
| customInput={TextField} | |||
| field={field} | |||
| {...props} | |||
| onValueChange={handleOnChange} | |||
| onChange={() => {}} | |||
| /> | |||
| ); | |||
| }; | |||
| PercentageField.propTypes = { | |||
| onChange: PropTypes.func, | |||
| field: PropTypes.shape({ | |||
| value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| }), | |||
| }; | |||
| export default PercentageField; | |||
| @@ -1,49 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ErrorMessage, useField } from 'formik'; | |||
| import PhoneInput from 'react-phone-number-input'; | |||
| import 'react-phone-number-input/style.css'; | |||
| const PhoneNumberField = ({ label, ...props }) => { | |||
| const [field, meta] = useField(props); | |||
| const inputErrorClassName = | |||
| meta.error && meta.touched ? 'c-input--error' : ''; | |||
| return ( | |||
| <div className={`c-input c-phone-number ${inputErrorClassName}`}> | |||
| {!!label && ( | |||
| <label className="c-input__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| <PhoneInput | |||
| international | |||
| defaultCountry="US" | |||
| {...field} | |||
| {...props} | |||
| onChange={(value) => { | |||
| props.onPhoneChange(value); | |||
| }} | |||
| countryOptionsOrder={['US']} | |||
| /> | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => ( | |||
| <span className="c-input__error">{errorMessage}</span> | |||
| )} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| PhoneNumberField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| disabled: PropTypes.bool, | |||
| onChange: PropTypes.func, | |||
| onPhoneChange: PropTypes.func, | |||
| }; | |||
| export default PhoneNumberField; | |||
| @@ -1,54 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ReactComponent as RadioOn } from '../../assets/images/svg/radio-on.svg'; | |||
| import { ReactComponent as RadioOff } from '../../assets/images/svg/radio-off.svg'; | |||
| const Checkbox = ({ | |||
| className, | |||
| children, | |||
| name, | |||
| checked, | |||
| field, | |||
| value, | |||
| selected, | |||
| id, | |||
| }) => ( | |||
| <label | |||
| htmlFor={name} | |||
| className={`c-radio ${selected ? 'c-radio--selected' : ''} ${ | |||
| className || '' | |||
| }`} | |||
| > | |||
| <input | |||
| name={name} | |||
| id={id} | |||
| className="c-radio__field" | |||
| type="radio" | |||
| checked={checked} | |||
| value={value} | |||
| {...field} | |||
| /> | |||
| <div className="c-radio__indicator"> | |||
| {selected ? ( | |||
| <RadioOn className="c-radio__icon" /> | |||
| ) : ( | |||
| <RadioOff className="c-radio__icon" /> | |||
| )} | |||
| </div> | |||
| <div className="c-radio__text">{children}</div> | |||
| </label> | |||
| ); | |||
| Checkbox.propTypes = { | |||
| children: PropTypes.node, | |||
| checked: PropTypes.bool, | |||
| name: PropTypes.string, | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| className: PropTypes.string, | |||
| value: PropTypes.string, | |||
| selected: PropTypes.bool, | |||
| id: PropTypes.string, | |||
| }; | |||
| export default Checkbox; | |||
| @@ -1,37 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import BaseInputField from './BaseInputField'; | |||
| const Search = ({ | |||
| field, | |||
| form, | |||
| label, | |||
| placeholder, | |||
| disabled, | |||
| className, | |||
| ...props | |||
| }) => ( | |||
| <BaseInputField | |||
| type="text" | |||
| label="" | |||
| placeholder={placeholder} | |||
| disabled={disabled} | |||
| form={form} | |||
| field={field} | |||
| isSearch | |||
| className={className} | |||
| {...props} | |||
| /> | |||
| ); | |||
| Search.propTypes = { | |||
| field: PropTypes.shape({}), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| placeholder: PropTypes.string, | |||
| disabled: PropTypes.bool, | |||
| className: PropTypes.string, | |||
| }; | |||
| export default Search; | |||
| @@ -1,122 +0,0 @@ | |||
| import React, { useEffect } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import Select, { components, createFilter } from 'react-select'; | |||
| import { ErrorMessage, useField } from 'formik'; | |||
| import { ReactComponent as FilledChevronDown } from '../../assets/images/svg/filled-chevron-down.svg'; | |||
| const SelectField = ({ | |||
| label, | |||
| disabled, | |||
| options, | |||
| link, | |||
| defaultSelected = null, | |||
| dropdownFullHeight, | |||
| selectOption, | |||
| ...props | |||
| }) => { | |||
| const [field, meta, helpers] = useField(props); | |||
| const filterConfig = { | |||
| ignoreCase: true, | |||
| ignoreAccents: true, | |||
| trim: true, | |||
| matchFrom: 'start', | |||
| }; | |||
| useEffect(() => { | |||
| if (defaultSelected) { | |||
| helpers.setValue(defaultSelected); | |||
| } | |||
| }, [defaultSelected]); // eslint-disable-line | |||
| const DropdownIndicator = (props) => | |||
| components.DropdownIndicator && ( | |||
| <components.DropdownIndicator {...props}> | |||
| <FilledChevronDown /> | |||
| </components.DropdownIndicator> | |||
| ); | |||
| function styles() { | |||
| let style = 'c-input'; | |||
| if (meta.error && meta.touched) { | |||
| style += ` c-input--error`; | |||
| } | |||
| if (dropdownFullHeight) { | |||
| style += ` c-input--dropdown-full-height`; | |||
| } | |||
| return style; | |||
| } | |||
| return ( | |||
| <div className={styles()}> | |||
| {!!label && ( | |||
| <label className="c-input__label" htmlFor={field.name}> | |||
| {label} | |||
| </label> | |||
| )} | |||
| {!!link && <div className="c-input__link">{link}</div>} | |||
| <Select | |||
| defaultValue={defaultSelected || options[0]} | |||
| components={{ DropdownIndicator }} | |||
| isSearchable={false} | |||
| classNamePrefix="c-select" | |||
| options={options} | |||
| isDisabled={disabled} | |||
| {...field} | |||
| {...props} | |||
| onBlur={(e) => { | |||
| helpers.setTouched(true); | |||
| field.onBlur(e); | |||
| }} | |||
| onChange={(selectedOption) => { | |||
| helpers.setValue(selectedOption); | |||
| if (props.onChange) { | |||
| props.onChange(); | |||
| } | |||
| if (selectOption) { | |||
| selectOption(selectedOption); | |||
| } | |||
| }} | |||
| filterOption={createFilter(filterConfig)} | |||
| /> | |||
| <ErrorMessage name={field.name}> | |||
| {(errorMessage) => { | |||
| if (typeof errorMessage === 'string') { | |||
| return <span className="c-input__error">{errorMessage}</span>; | |||
| } | |||
| return <span className="c-input__error">{errorMessage.value}</span>; | |||
| }} | |||
| </ErrorMessage> | |||
| </div> | |||
| ); | |||
| }; | |||
| SelectField.propTypes = { | |||
| field: PropTypes.shape({ | |||
| name: PropTypes.string, | |||
| }), | |||
| form: PropTypes.shape({}), | |||
| label: PropTypes.oneOfType([PropTypes.string, PropTypes.shape({})]), | |||
| disabled: PropTypes.bool, | |||
| options: PropTypes.arrayOf( | |||
| PropTypes.shape({ | |||
| label: PropTypes.string, | |||
| value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| }), | |||
| ), | |||
| onChange: PropTypes.func, | |||
| link: PropTypes.node, | |||
| defaultSelected: PropTypes.shape({ | |||
| label: PropTypes.string, | |||
| value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||
| }), | |||
| dropdownFullHeight: PropTypes.bool, | |||
| selectOption: PropTypes.func, | |||
| }; | |||
| export default SelectField; | |||
| @@ -1,26 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| const BlockSectionLoader = ({ children, isLoading, fullHeight, noShadow }) => ( | |||
| <div | |||
| className={`c-loader__wrapper c-loader__wrapper--block ${ | |||
| fullHeight ? 'c-loader__wrapper--full-height' : '' | |||
| } ${noShadow ? 'c-loader__wrapper--no-shadow' : ''}`} | |||
| > | |||
| {children} | |||
| {isLoading && ( | |||
| <div className="c-loader"> | |||
| <div className="c-loader__icon" /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| BlockSectionLoader.propTypes = { | |||
| children: PropTypes.node, | |||
| isLoading: PropTypes.bool, | |||
| fullHeight: PropTypes.bool, | |||
| noShadow: PropTypes.bool, | |||
| }; | |||
| export default BlockSectionLoader; | |||
| @@ -1,11 +0,0 @@ | |||
| import React from 'react'; | |||
| const FullPageLoader = () => { | |||
| return ( | |||
| <div className="c-loader c-loader--page"> | |||
| <div className="c-loader__icon" /> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default FullPageLoader; | |||
| @@ -1,57 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { | |||
| Dialog, | |||
| DialogContent, | |||
| DialogTitle, | |||
| DialogActions, | |||
| Button, | |||
| useMediaQuery, | |||
| useTheme, | |||
| } from '@mui/material'; | |||
| const DialogComponent = ({ | |||
| title, | |||
| content, | |||
| onClose, | |||
| open, | |||
| maxWidth, | |||
| fullWidth, | |||
| responsive, | |||
| }) => { | |||
| const theme = useTheme(); | |||
| const fullScreen = useMediaQuery(theme.breakpoints.down('md')); | |||
| const handleClose = () => { | |||
| onClose(); | |||
| }; | |||
| return ( | |||
| <Dialog | |||
| maxWidth={maxWidth} | |||
| fullWidth={fullWidth} | |||
| fullScreen={responsive && fullScreen} | |||
| onClose={handleClose} | |||
| open={open} | |||
| > | |||
| <DialogTitle>{title}</DialogTitle> | |||
| {content && <DialogContent>{content}</DialogContent>} | |||
| <DialogActions> | |||
| <Button onClick={handleClose}>OK</Button> | |||
| <Button onClick={handleClose}>Cancel</Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| ); | |||
| }; | |||
| DialogComponent.propTypes = { | |||
| title: PropTypes.any, | |||
| open: PropTypes.bool.isRequired, | |||
| content: PropTypes.any, | |||
| onClose: PropTypes.func.isRequired, | |||
| maxWidth: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']), | |||
| fullWidth: PropTypes.bool, | |||
| responsive: PropTypes.bool, | |||
| }; | |||
| export default DialogComponent; | |||
| @@ -1,29 +0,0 @@ | |||
| import React from 'react'; | |||
| import { Paper, Typography } from '@mui/material'; | |||
| import { DataGrid } from '@mui/x-data-grid'; | |||
| // Use these values from REDUX? | |||
| const rows = [ | |||
| { id: 1, col1: 'Example', col2: 'Row', col3: '1' }, | |||
| { id: 2, col1: 'Row', col2: 'Example', col3: '2' }, | |||
| { id: 3, col1: '3', col2: 'Row', col3: 'Example' }, | |||
| ]; | |||
| const columns = [ | |||
| { field: 'col1', headerName: 'Column 1', flex: 1 }, | |||
| { field: 'col2', headerName: 'Column 2', flex: 1 }, | |||
| { field: 'col3', headerName: 'Column 2', flex: 1 }, | |||
| ]; | |||
| const DataGridExample = () => { | |||
| return ( | |||
| <Paper sx={{ p: 2 }} elevation={5}> | |||
| <Typography variant="h4" gutterBottom align="center"> | |||
| DataGrid Example | |||
| </Typography> | |||
| <DataGrid autoHeight rows={rows} columns={columns} /> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default DataGridExample; | |||
| @@ -1,64 +0,0 @@ | |||
| import React, { useState } from 'react'; | |||
| import { Button, Divider, Paper, Typography } from '@mui/material'; | |||
| import DialogComponent from '../DialogComponent'; | |||
| import DrawerComponent from '../DrawerComponent'; | |||
| import PopoverComponent from '../PopoverComponent'; | |||
| const Modals = () => { | |||
| const [dialogOpen, setDialogOpen] = useState(false); | |||
| const [drawerOpen, setDrawerOpen] = useState(false); | |||
| const [popoverOpen, setPopoverOpen] = useState(false); | |||
| const [anchorEl, setAnchorEl] = useState(null); | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| p: 2, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| }} | |||
| elevation={5} | |||
| > | |||
| <Typography variant="h4" gutterBottom align="center"> | |||
| Modals Example | |||
| </Typography> | |||
| <Divider /> | |||
| <Button onClick={() => setDialogOpen(true)}>Open Dialog</Button> | |||
| <Button onClick={() => setDrawerOpen(true)}>Open Drawer</Button> | |||
| <Button | |||
| onClick={(e) => { | |||
| setPopoverOpen(true); | |||
| setAnchorEl(e.currentTarget); | |||
| }} | |||
| > | |||
| Open Popover | |||
| </Button> | |||
| <DialogComponent | |||
| title="Dialog Title" | |||
| content={<Typography>Dialog Content</Typography>} | |||
| open={dialogOpen} | |||
| onClose={() => setDialogOpen(false)} | |||
| maxWidth="md" | |||
| fullWidth | |||
| responsive | |||
| /> | |||
| <DrawerComponent | |||
| anchor="left" | |||
| content={<Typography sx={{ p: 2 }}>Drawer Content</Typography>} | |||
| open={drawerOpen} | |||
| toggleOpen={() => setDrawerOpen(!drawerOpen)} | |||
| /> | |||
| <PopoverComponent | |||
| anchorEl={anchorEl} | |||
| open={popoverOpen} | |||
| onClose={() => { | |||
| setPopoverOpen(false); | |||
| setAnchorEl(null); | |||
| }} | |||
| content={<Typography sx={{ p: 2 }}>Popover Content</Typography>} | |||
| /> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default Modals; | |||
| @@ -1,183 +0,0 @@ | |||
| import React, { useEffect, useState } from 'react'; | |||
| import { | |||
| Paper, | |||
| Box, | |||
| Grid, | |||
| Typography, | |||
| Divider, | |||
| TablePagination, | |||
| TextField, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| } from '@mui/material'; | |||
| // import { useTranslation } from 'react-i18next'; | |||
| import { useDispatch, useSelector, batch } from 'react-redux'; | |||
| import useDebounce from '../../../hooks/useDebounceHook'; | |||
| import { | |||
| itemsSelector, | |||
| pageSelector, | |||
| itemsPerPageSelector, | |||
| countSelector, | |||
| sortSelector, | |||
| } from '../../../store/selectors/randomDataSelectors'; | |||
| import { | |||
| loadData, | |||
| updatePage, | |||
| updateItemsPerPage, | |||
| updateFilter, | |||
| updateSort, | |||
| } from '../../../store/actions/randomData/randomDataActions'; | |||
| const PagingSortingFilteringExample = () => { | |||
| const [filterText, setFilterText] = useState(''); | |||
| const dispatch = useDispatch(); | |||
| // const { t } = useTranslation(); | |||
| const items = useSelector(itemsSelector); | |||
| const currentPage = useSelector(pageSelector); | |||
| const itemsPerPage = useSelector(itemsPerPageSelector); | |||
| const totalCount = useSelector(countSelector); | |||
| const sort = useSelector(sortSelector) || 'name-asc'; | |||
| // Use debounce to prevent too many rerenders | |||
| const debouncedFilterText = useDebounce(filterText, 500); | |||
| useEffect(() => { | |||
| dispatch(loadData(30)); | |||
| dispatch(updateSort(sort)); | |||
| }, []); | |||
| useEffect(() => { | |||
| batch(() => { | |||
| dispatch(updateFilter(filterText)); | |||
| currentPage > 0 && dispatch(updatePage(0)); | |||
| }); | |||
| }, [debouncedFilterText]); | |||
| const handleFilterTextChange = (event) => { | |||
| const filterText = event.target.value; | |||
| setFilterText(filterText); | |||
| }; | |||
| const handleSortChange = (event) => { | |||
| const sort = event.target.value; | |||
| dispatch(updateSort(sort)); | |||
| }; | |||
| const handlePageChange = (event, newPage) => { | |||
| dispatch(updatePage(newPage)); | |||
| }; | |||
| const handleItemsPerPageChange = (event) => { | |||
| const itemsPerPage = parseInt(event.target.value); | |||
| batch(() => { | |||
| dispatch(updateItemsPerPage(itemsPerPage)); | |||
| dispatch(updatePage(0)); | |||
| }); | |||
| }; | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| justifyContent: 'start', | |||
| py: 2, | |||
| minHeight: 500, | |||
| }} | |||
| elevation={5} | |||
| > | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| Pagination, Filtering and Sorting Example Client Side | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| flexWrap: 'wrap', | |||
| mx: 2, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| {/* TODO Separate into SelectComponent */} | |||
| <FormControl sx={{ flexGrow: 1 }}> | |||
| <InputLabel id="sort-label">Sort</InputLabel> | |||
| <Select | |||
| label="Sort" | |||
| labelId="sort-label" | |||
| id="sort-select-helper" | |||
| value={sort} | |||
| onChange={handleSortChange} | |||
| > | |||
| <MenuItem value="name-asc">Name - A-Z</MenuItem> | |||
| <MenuItem value="name-desc">Name - Z-A</MenuItem> | |||
| <MenuItem value="price-asc">Price - Lowest to Highest</MenuItem> | |||
| <MenuItem value="price-desc">Price - Highest to Lowest</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <TextField | |||
| sx={{ flexGrow: 1 }} | |||
| variant="outlined" | |||
| label="Filter" | |||
| placeholder="Filter" | |||
| value={filterText} | |||
| onChange={handleFilterTextChange} | |||
| /> | |||
| </Box> | |||
| </Box> | |||
| <Grid container> | |||
| {items && | |||
| items.length > 0 && | |||
| items | |||
| .slice( | |||
| currentPage * itemsPerPage, | |||
| currentPage * itemsPerPage + itemsPerPage | |||
| ) | |||
| .map((product, index) => ( | |||
| // ! DON'T USE index for key, this is for example only | |||
| <Grid item sx={{ p: 2 }} xs={12} sm={6} md={4} lg={3} key={index}> | |||
| {/* TODO separate into component */} | |||
| <Paper sx={{ p: 3, height: '100%' }} elevation={3}> | |||
| <Typography sx={{ fontWeight: 600 }}>Name: </Typography> | |||
| <Typography display="inline"> {product.name}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Designer: </Typography> | |||
| <Typography display="inline"> {product.designer}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Type: </Typography> | |||
| <Typography display="inline"> {product.type}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Price: </Typography> | |||
| <Typography display="inline"> ${product.price}</Typography> | |||
| </Paper> | |||
| </Grid> | |||
| ))} | |||
| </Grid> | |||
| <Box sx={{ width: '100%' }}> | |||
| <TablePagination | |||
| component="div" | |||
| count={totalCount} | |||
| page={currentPage} | |||
| onPageChange={handlePageChange} | |||
| rowsPerPage={itemsPerPage} | |||
| onRowsPerPageChange={handleItemsPerPageChange} | |||
| rowsPerPageOptions={[12, 24, 48, 96]} | |||
| labelRowsPerPage="Items per page" | |||
| showFirstButton | |||
| showLastButton | |||
| /> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default PagingSortingFilteringExample; | |||
| @@ -1,159 +0,0 @@ | |||
| import React, { useEffect, useState } from 'react'; | |||
| import { | |||
| Paper, | |||
| Box, | |||
| Grid, | |||
| Typography, | |||
| Divider, | |||
| TablePagination, | |||
| TextField, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| } from '@mui/material'; | |||
| // import { useTranslation } from 'react-i18next'; | |||
| import Backdrop from '../BackdropComponent'; | |||
| import useDebounce from '../../../hooks/useDebounceHook'; | |||
| import { useRandomData } from '../../../context/RandomDataContext'; | |||
| const PagingSortingFilteringExampleServerSide = () => { | |||
| const [filterText, setFilterText] = useState(''); | |||
| const { state, data } = useRandomData(); | |||
| const { items, loading, totalCount, currentPage, itemsPerPage, sort } = data; | |||
| const { setPage, setItemsPerPage, setSort, setFilter } = state; | |||
| // const { t } = useTranslation(); | |||
| // Use debounce to prevent too many rerenders | |||
| const debouncedFilterText = useDebounce(filterText, 500); | |||
| useEffect(() => { | |||
| setFilter(filterText); | |||
| }, [debouncedFilterText]); | |||
| const handleFilterTextChange = (event) => { | |||
| const filterText = event.target.value; | |||
| setFilterText(filterText); | |||
| }; | |||
| const handleSortChange = (event) => { | |||
| const sort = event.target.value; | |||
| setSort(sort); | |||
| }; | |||
| const handlePageChange = (event, newPage) => { | |||
| setPage(newPage); | |||
| }; | |||
| const handleItemsPerPageChange = (event) => { | |||
| const itemsPerPage = parseInt(event.target.value); | |||
| setItemsPerPage(itemsPerPage); | |||
| setPage(0); | |||
| }; | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| justifyContent: 'start', | |||
| py: 2, | |||
| minHeight: 500, | |||
| position: 'relative', | |||
| }} | |||
| elevation={5} | |||
| > | |||
| {loading && <Backdrop isLoading position="absolute" />} | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| Pagination, Filtering and Sorting Example Server Side | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| flexWrap: 'wrap', | |||
| mx: 2, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| <FormControl sx={{ flexGrow: 1 }}> | |||
| <InputLabel id="sort-label">Sort</InputLabel> | |||
| <Select | |||
| label="Sort" | |||
| labelId="sort-label" | |||
| id="sort-select-helper" | |||
| value={sort || ''} | |||
| onChange={handleSortChange} | |||
| > | |||
| <MenuItem value="">None</MenuItem> | |||
| <MenuItem value="name-asc">Name - A-Z</MenuItem> | |||
| <MenuItem value="name-desc">Name - Z-A</MenuItem> | |||
| <MenuItem value="price-asc">Price - Lowest to Highest</MenuItem> | |||
| <MenuItem value="price-desc">Price - Highest to Lowest</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <TextField | |||
| sx={{ flexGrow: 1 }} | |||
| variant="outlined" | |||
| label="Filter" | |||
| placeholder="Filter" | |||
| value={filterText} | |||
| onChange={handleFilterTextChange} | |||
| /> | |||
| </Box> | |||
| <Grid container sx={{ position: 'relative' }}> | |||
| {items && | |||
| items.length > 0 && | |||
| items.map((item) => ( | |||
| <Grid | |||
| item | |||
| sx={{ p: 2 }} | |||
| xs={12} | |||
| sm={6} | |||
| md={4} | |||
| lg={3} | |||
| key={item.id} | |||
| > | |||
| {/* TODO separate into component */} | |||
| <Paper sx={{ p: 3, height: '100%' }} elevation={3}> | |||
| <Typography sx={{ fontWeight: 600 }}>Name: </Typography> | |||
| <Typography display="inline"> {item.name}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Company: </Typography> | |||
| <Typography display="inline"> {item.company}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Color: </Typography> | |||
| <Typography display="inline"> {item.color}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Price: </Typography> | |||
| <Typography display="inline"> {item.price}</Typography> | |||
| </Paper> | |||
| </Grid> | |||
| ))} | |||
| </Grid> | |||
| <Box sx={{ width: '100%' }}> | |||
| <TablePagination | |||
| component="div" | |||
| count={totalCount} | |||
| page={currentPage} | |||
| onPageChange={handlePageChange} | |||
| rowsPerPage={itemsPerPage} | |||
| onRowsPerPageChange={handleItemsPerPageChange} | |||
| rowsPerPageOptions={[12, 24, 48, 96]} | |||
| labelRowsPerPage="Items per page" | |||
| showFirstButton | |||
| showLastButton | |||
| /> | |||
| </Box> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default PagingSortingFilteringExampleServerSide; | |||
| @@ -1,26 +0,0 @@ | |||
| import React, { useState } from 'react'; | |||
| import { Button, Menu, MenuItem } from '@mui/material'; | |||
| const MenuListComponent = () => { | |||
| const [anchorEl, setAnchorEl] = useState(null); | |||
| const open = Boolean(anchorEl); | |||
| const handleClick = (event) => { | |||
| setAnchorEl(event.currentTarget); | |||
| }; | |||
| const handleClose = () => { | |||
| setAnchorEl(null); | |||
| }; | |||
| return ( | |||
| <div> | |||
| <Button onClick={handleClick}>Menu List</Button> | |||
| <Menu id="menu-list" anchorEl={anchorEl} open={open} onClose={handleClose}> | |||
| <MenuItem onClick={handleClose}>Menu Item 1</MenuItem> | |||
| <MenuItem onClick={handleClose}>Menu Item 2</MenuItem> | |||
| <MenuItem onClick={handleClose}>Menu Item 3</MenuItem> | |||
| </Menu> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default MenuListComponent; | |||
| @@ -1,35 +0,0 @@ | |||
| import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Box, Popover } from '@mui/material'; | |||
| const PopoverComponent = ({ open, anchorEl, onClose, content }) => { | |||
| const handleClose = () => { | |||
| onClose(); | |||
| }; | |||
| return ( | |||
| <Box component="div"> | |||
| <Popover | |||
| sx={{ p: 5 }} | |||
| open={open} | |||
| anchorEl={anchorEl} | |||
| onClose={handleClose} | |||
| anchorOrigin={{ | |||
| vertical: 'bottom', | |||
| horizontal: 'left', | |||
| }} | |||
| > | |||
| {content} | |||
| </Popover> | |||
| </Box> | |||
| ); | |||
| }; | |||
| PopoverComponent.propTypes = { | |||
| anchorEl: PropTypes.object, | |||
| open: PropTypes.bool.isRequired, | |||
| onClose: PropTypes.func.isRequired, | |||
| content: PropTypes.any, | |||
| }; | |||
| export default PopoverComponent; | |||
| @@ -155,13 +155,13 @@ const DayDetailsComponent = ({ | |||
| {isLeftArrowDisabled === true ? ( | |||
| <div | |||
| className="day-details-arrow-container" | |||
| data-testid="day-details-left-arrow" | |||
| > | |||
| <img src={arrowLeftDisabled} /> | |||
| </div> | |||
| ) : ( | |||
| <div | |||
| className="day-details-arrow-container" | |||
| data-testid="day-details-left-arrow" | |||
| onClick={goBackOneDay} | |||
| > | |||
| <img src={arrowLeft} /> | |||
| @@ -1,16 +1,2 @@ | |||
| export const PERIOD_SYMBOL = 190; | |||
| export const COMMA_SYMBOL = 188; | |||
| export const PLUS_SYMBOL = 187; | |||
| export const MINUS_SYMBOL = 189; | |||
| export const NUMPAD_PERIOD_SYMBOL = 110; | |||
| export const NUMPAD_MINUS_SYMBOL = 109; | |||
| export const NUMPAD_PLUS_SYMBOL = 107; | |||
| export const K_KEYCODE = 75; | |||
| export const DOWN_ARROW_KEYCODE = 38; | |||
| export const UP_ARROW_KEYCODE = 40; | |||
| export const RIGHT_ARROW_KEYCODE = 39; | |||
| export const LEFT_ARROW_KEYCODE = 37; | |||
| export const BACKSPACE_KEYCODE = 8; | |||
| export const TAB_KEYCODE = 9; | |||
| /* istanbul ignore file */ | |||
| export const PAGE_SIZE_CANDIDATES = 9; | |||
| @@ -1,3 +1,4 @@ | |||
| /* istanbul ignore file */ | |||
| export const JWT_TOKEN = 'JwtToken'; | |||
| export const JWT_REFRESH_TOKEN = 'JwtRefreshToken'; | |||
| export const REFRESH_TOKEN_CONST = 'RefreshToken'; | |||
| @@ -1,3 +1,4 @@ | |||
| /* istanbul ignore file */ | |||
| export const BASE_PAGE = '/'; | |||
| export const FORGOT_PASSWORD_PAGE = '/forgot-password'; | |||
| export const HOME_PAGE = '/home'; | |||
| @@ -1,63 +0,0 @@ | |||
| import React, { createContext, useContext, useState } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import usePagingHook from '../hooks/usePagingHook'; | |||
| import { getRequest } from '../request/jsonServerRequest'; | |||
| const apiCall = (page, itemsPerPage, sort, sortDirection, filter) => | |||
| getRequest('/items', { | |||
| _page: page, | |||
| _limit: itemsPerPage, | |||
| // Conditionally add to params object if keys exist | |||
| ...(sort && { _sort: sort }), | |||
| ...(sortDirection && { _order: sortDirection }), | |||
| ...(filter && { q: filter }), | |||
| }); | |||
| const Context = createContext(); | |||
| export const useRandomData = () => useContext(Context); | |||
| const RandomDataProvider = ({ children }) => { | |||
| const setPage = (page) => { | |||
| setState({ ...state, page }); | |||
| }; | |||
| const setItemsPerPage = (itemsPerPage) => { | |||
| setState({ ...state, itemsPerPage }); | |||
| }; | |||
| const setSort = (sort) => { | |||
| setState({ ...state, sort }); | |||
| }; | |||
| const setFilter = (filter) => { | |||
| setState({ ...state, filter }); | |||
| }; | |||
| const [state, setState] = useState({ | |||
| page: 0, | |||
| setPage, | |||
| itemsPerPage: 12, | |||
| setItemsPerPage, | |||
| sort: '', | |||
| setSort, | |||
| filter: '', | |||
| setFilter, | |||
| }); | |||
| const data = usePagingHook( | |||
| state.page, | |||
| state.itemsPerPage, | |||
| state.sort, | |||
| state.filter, | |||
| apiCall | |||
| ); | |||
| return ( | |||
| <Context.Provider value={{ state, data }}>{children}</Context.Provider> | |||
| ); | |||
| }; | |||
| RandomDataProvider.propTypes = { | |||
| children: PropTypes.node, | |||
| }; | |||
| export default RandomDataProvider; | |||
| @@ -1,17 +0,0 @@ | |||
| import { useEffect, useState } from 'react'; | |||
| const useDebounce = (value, delay) => { | |||
| const [debouncedValue, setDebouncedValue] = useState(value); | |||
| useEffect(() => { | |||
| const timer = setTimeout(() => setDebouncedValue(value), delay || 500); | |||
| return () => { | |||
| clearTimeout(timer); | |||
| }; | |||
| }, [value, delay]); | |||
| return debouncedValue; | |||
| }; | |||
| export default useDebounce; | |||
| @@ -1,66 +0,0 @@ | |||
| import { useState, useCallback, useEffect } from 'react'; | |||
| import { unstable_batchedUpdates } from 'react-dom'; | |||
| const usePagingHook = (page, itemsPerPage, sort, filter, apiCallback) => { | |||
| const [items, setItems] = useState([]); | |||
| const [totalPages, setTotalPages] = useState(0); | |||
| const [currentPage, setCurrentPage] = useState(0); | |||
| const [loading, setLoading] = useState(false); | |||
| const [totalCount, setTotalCount] = useState(0); | |||
| const reload = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| const [sortColumn, sortDirection] = sort.split('-'); | |||
| const response = await apiCallback( | |||
| page, | |||
| itemsPerPage, | |||
| sortColumn, | |||
| sortDirection, | |||
| filter | |||
| ); | |||
| if (response.status === 200) { | |||
| // Prevents multiple rerenders | |||
| unstable_batchedUpdates(() => { | |||
| setItems(response.data); | |||
| setTotalCount(parseInt(response.headers['x-total-count'])); | |||
| setTotalPages( | |||
| Math.ceil(response.headers['x-total-count'] / itemsPerPage) | |||
| ); | |||
| setCurrentPage(page); | |||
| }); | |||
| } | |||
| } catch (e) { | |||
| console.error(e); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [ | |||
| setItems, | |||
| setLoading, | |||
| setTotalPages, | |||
| setCurrentPage, | |||
| apiCallback, | |||
| page, | |||
| itemsPerPage, | |||
| sort, | |||
| filter, | |||
| ]); | |||
| useEffect(() => { | |||
| reload(); | |||
| }, [reload]); | |||
| return { | |||
| items, | |||
| loading, | |||
| reload, | |||
| totalCount, | |||
| totalPages, | |||
| currentPage, | |||
| itemsPerPage, | |||
| sort, | |||
| }; | |||
| }; | |||
| export default usePagingHook; | |||
| @@ -5,6 +5,7 @@ import { initReactI18next } from 'react-i18next'; | |||
| import enTranslations from './resources/en'; | |||
| import rsTranslations from './resources/rs'; | |||
| /* istanbul ignore file */ | |||
| i18n.use(initReactI18next).init({ | |||
| lng: 'rs', | |||
| fallbackLng: 'en', | |||
| @@ -1,13 +0,0 @@ | |||
| /* body { | |||
| margin: 0; | |||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', | |||
| 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', | |||
| sans-serif; | |||
| -webkit-font-smoothing: antialiased; | |||
| -moz-osx-font-smoothing: grayscale; | |||
| } | |||
| code { | |||
| font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', | |||
| monospace; | |||
| } */ | |||
| @@ -10,6 +10,7 @@ import store from './store'; | |||
| import './i18n'; | |||
| import ColorModeProvider from './context/ColorModeContext'; | |||
| /* istanbul ignore file */ | |||
| ReactDOM.render( | |||
| <HelmetProvider> | |||
| <React.StrictMode> | |||
| @@ -1,3 +1,4 @@ | |||
| /* istanbul ignore file */ | |||
| export const mockState = { | |||
| user: { | |||
| user: { | |||
| @@ -80,9 +80,15 @@ const CandidatesPage = ({ history }) => { | |||
| <div className="r-b-rectangle"></div> | |||
| <div className="top-candidates-container"> | |||
| {!matches ? ( | |||
| <p className="candidates-header">Kandidati</p> | |||
| <p className="candidates-header" data-testid="candidates-header1"> | |||
| Kandidati | |||
| </p> | |||
| ) : ( | |||
| <p className="candidates-header" style={{ fontSize: "22px" }}> | |||
| <p | |||
| className="candidates-header" | |||
| data-testid="candidates-header2" | |||
| style={{ fontSize: "22px" }} | |||
| > | |||
| Kandidati | |||
| </p> | |||
| )} | |||
| @@ -118,7 +124,7 @@ const CandidatesPage = ({ history }) => { | |||
| </IconButton> | |||
| ) : ( | |||
| <IconButton | |||
| className="c-btn c-btn--primary-outlined candidate-btn" | |||
| className="c-btn c-btn--primary-outlined candidate-btn candidate-btn-view-2" | |||
| onClick={changeView} | |||
| > | |||
| Tablicni prikaz | |||
| @@ -156,7 +162,7 @@ const CandidatesPage = ({ history }) => { | |||
| )} | |||
| {!matches ? ( | |||
| <IconButton | |||
| className="c-btn c-btn--primary-outlined candidate-btn" | |||
| className="c-btn c-btn--primary-outlined candidate-btn candidate-btn-filters1" | |||
| onClick={handleToggleFiltersDrawer} | |||
| > | |||
| Filteri | |||
| @@ -168,7 +174,7 @@ const CandidatesPage = ({ history }) => { | |||
| </IconButton> | |||
| ) : ( | |||
| <IconButton | |||
| className="c-btn c-btn--primary-outlined candidate-btn-mobile" | |||
| className="c-btn c-btn--primary-outlined candidate-btn-mobile candidate-btn-filters2" | |||
| onClick={handleToggleFiltersDrawer} | |||
| > | |||
| <img | |||
| @@ -1,6 +1,7 @@ | |||
| const reportWebVitals = onPerfEntry => { | |||
| /* istanbul ignore file */ | |||
| const reportWebVitals = (onPerfEntry) => { | |||
| if (onPerfEntry && onPerfEntry instanceof Function) { | |||
| import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { | |||
| import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { | |||
| getCLS(onPerfEntry); | |||
| getFID(onPerfEntry); | |||
| getFCP(onPerfEntry); | |||
| @@ -6,6 +6,7 @@ import loadingMiddleware from './middleware/loadingMiddleware'; | |||
| import requestStatusMiddleware from './middleware/requestStatusMiddleware'; | |||
| import internalServerErrorMiddleware from './middleware/internalServerErrorMiddleware'; | |||
| /* istanbul ignore file */ | |||
| const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; | |||
| const sagaMiddleware = createSagaMiddleware(); | |||
| export default createStore( | |||
| @@ -1,6 +1,5 @@ | |||
| import { format } from 'date-fns'; | |||
| import { enUS } from 'date-fns/locale'; | |||
| import i18next from 'i18next'; | |||
| export function formatDate(date, fmt = 'dd.MM.y', locale = enUS) { | |||
| const dt = new Date(date); | |||
| @@ -17,33 +16,6 @@ export function formatDateTime(date) { | |||
| return format(dt, 'hh:mm dd.MM.y'); | |||
| } | |||
| export function getDateDay(date) { | |||
| const dt = new Date(date); | |||
| return format(dt, 'dd'); | |||
| } | |||
| export function getDateMonth(date) { | |||
| const dt = new Date(date); | |||
| return format(dt, 'MM'); | |||
| } | |||
| export function getDateYear(date) { | |||
| const dt = new Date(date); | |||
| return format(dt, 'y'); | |||
| } | |||
| export function formatDateTimeLocale(date) { | |||
| const dt = new Date(date); | |||
| return format(dt, 'MM/dd/y hh:mm aa'); | |||
| } | |||
| // TODO add locale | |||
| export function formatDateRange(dates) { | |||
| const start = formatDate(dates.start); | |||
| const end = formatDate(dates.end); | |||
| return i18next.t('common.date.range', { start, end }); | |||
| } | |||
| export function formatDateSrb(date) { | |||
| const dt = new Date(date); | |||
| return format(dt, 'dd.MM.'); | |||
| @@ -1 +0,0 @@ | |||
| export const parseEnumType = (typeArray, index) => typeArray[index - 1]; | |||
| @@ -1,3 +1,4 @@ | |||
| /* istanbul ignore file */ | |||
| const random = (arr) => { | |||
| return arr[Math.floor(Math.random() * arr.length)]; | |||
| }; | |||