| import MainContainer from "./components/Section/MainContainer"; | import MainContainer from "./components/Section/MainContainer"; | ||||
| import AppRoutes from "./AppRoutes"; | import AppRoutes from "./AppRoutes"; | ||||
| /* istanbul ignore file */ | |||||
| function App() { | function App() { | ||||
| return ( | return ( | ||||
| <> | <> |
| import { mockState } from "../../mockState"; | import { mockState } from "../../mockState"; | ||||
| import { Router } from "react-router-dom"; | import { Router } from "react-router-dom"; | ||||
| import history from "../../store/utils/history"; | 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", () => { | describe("CandidatesPage render tests", () => { | ||||
| const cont = ( | const cont = ( | ||||
| let spyOnUseSelector; | let spyOnUseSelector; | ||||
| beforeEach(() => { | beforeEach(() => { | ||||
| window.matchMedia = createMatchMedia(601); | |||||
| spyOnUseSelector = jest.spyOn(redux, "useSelector"); | spyOnUseSelector = jest.spyOn(redux, "useSelector"); | ||||
| spyOnUseSelector | |||||
| .mockReturnValueOnce(mockState.technologies.technologies) | |||||
| spyOnUseSelector.mockReturnValueOnce(mockState.technologies.technologies); | |||||
| }); | }); | ||||
| afterEach(() => { | afterEach(() => { | ||||
| jest.restoreAllMocks(); | 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", () => { | it("Should render", () => { | ||||
| render(cont); | render(cont); | ||||
| expect(screen.getByTestId("candidates-page")).toBeDefined(); | 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); | 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", () => { | it("should be rendered button which is used for showing input responsible for searching by name", () => { | ||||
| expect(container.getElementsByClassName("candidate-btn")[1]).toBeDefined(); | 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); | 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", () => { | it("input for searching by name should not be shown when component is initialy rendered", () => { |
| import DayDetailsComponent from "../../components/Schedules/DayDetailsComponent"; | import DayDetailsComponent from "../../components/Schedules/DayDetailsComponent"; | ||||
| import ColorModeProvider from "../../context/ColorModeContext"; | import ColorModeProvider from "../../context/ColorModeContext"; | ||||
| const setCurrentlySelected = jest.fn(); | |||||
| const setCurrentlySelectedDay = jest.fn(); | |||||
| const props = { | const props = { | ||||
| selectedDate: "20.12.2023", | selectedDate: "20.12.2023", | ||||
| selectionProcesses: mockState.schedule.schedule, | selectionProcesses: mockState.schedule.schedule, | ||||
| open: jest.fn(), | open: jest.fn(), | ||||
| onClose: jest.fn(), | onClose: jest.fn(), | ||||
| setCurrentlySelected: jest.fn(), | |||||
| setCurrentlySelectedDay: jest.fn(), | |||||
| currentlySelectedDay: 1, | |||||
| setCurrentlySelected: setCurrentlySelected, | |||||
| setCurrentlySelectedDay: setCurrentlySelectedDay, | |||||
| currentlySelectedDay: 20, | |||||
| numberOfDaysInMonth: 31, | numberOfDaysInMonth: 31, | ||||
| history: { | history: { | ||||
| replace: jest.fn(), | replace: jest.fn(), | ||||
| expect(screen.getByTestId("day-component-dialog")).toBeDefined(); | 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); | render(cont); | ||||
| expect(screen.getAllByTestId("day-details-left-arrow")[0]).toBeDefined(); | 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); | render(cont); | ||||
| expect(screen.getAllByTestId("day-details-right-arrow")[0]).toBeDefined(); | expect(screen.getAllByTestId("day-details-right-arrow")[0]).toBeDefined(); | ||||
| }); | }); | ||||
| const arg = { pathname: "/candidates/1" }; | const arg = { pathname: "/candidates/1" }; | ||||
| expect(props.history.push).toHaveBeenCalledWith(arg); | 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); | |||||
| }); | |||||
| }); | }); |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| import React from 'react'; | |||||
| const FullPageLoader = () => { | |||||
| return ( | |||||
| <div className="c-loader c-loader--page"> | |||||
| <div className="c-loader__icon" /> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default FullPageLoader; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| {isLeftArrowDisabled === true ? ( | {isLeftArrowDisabled === true ? ( | ||||
| <div | <div | ||||
| className="day-details-arrow-container" | className="day-details-arrow-container" | ||||
| data-testid="day-details-left-arrow" | |||||
| > | > | ||||
| <img src={arrowLeftDisabled} /> | <img src={arrowLeftDisabled} /> | ||||
| </div> | </div> | ||||
| ) : ( | ) : ( | ||||
| <div | <div | ||||
| className="day-details-arrow-container" | className="day-details-arrow-container" | ||||
| data-testid="day-details-left-arrow" | |||||
| onClick={goBackOneDay} | onClick={goBackOneDay} | ||||
| > | > | ||||
| <img src={arrowLeft} /> | <img src={arrowLeft} /> |
| 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; | export const PAGE_SIZE_CANDIDATES = 9; |
| /* istanbul ignore file */ | |||||
| export const JWT_TOKEN = 'JwtToken'; | export const JWT_TOKEN = 'JwtToken'; | ||||
| export const JWT_REFRESH_TOKEN = 'JwtRefreshToken'; | export const JWT_REFRESH_TOKEN = 'JwtRefreshToken'; | ||||
| export const REFRESH_TOKEN_CONST = 'RefreshToken'; | export const REFRESH_TOKEN_CONST = 'RefreshToken'; |
| /* istanbul ignore file */ | |||||
| export const BASE_PAGE = '/'; | export const BASE_PAGE = '/'; | ||||
| export const FORGOT_PASSWORD_PAGE = '/forgot-password'; | export const FORGOT_PASSWORD_PAGE = '/forgot-password'; | ||||
| export const HOME_PAGE = '/home'; | export const HOME_PAGE = '/home'; |
| 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; |
| 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; |
| 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; |
| import enTranslations from './resources/en'; | import enTranslations from './resources/en'; | ||||
| import rsTranslations from './resources/rs'; | import rsTranslations from './resources/rs'; | ||||
| /* istanbul ignore file */ | |||||
| i18n.use(initReactI18next).init({ | i18n.use(initReactI18next).init({ | ||||
| lng: 'rs', | lng: 'rs', | ||||
| fallbackLng: 'en', | fallbackLng: 'en', |
| /* 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; | |||||
| } */ |
| import './i18n'; | import './i18n'; | ||||
| import ColorModeProvider from './context/ColorModeContext'; | import ColorModeProvider from './context/ColorModeContext'; | ||||
| /* istanbul ignore file */ | |||||
| ReactDOM.render( | ReactDOM.render( | ||||
| <HelmetProvider> | <HelmetProvider> | ||||
| <React.StrictMode> | <React.StrictMode> |
| /* istanbul ignore file */ | |||||
| export const mockState = { | export const mockState = { | ||||
| user: { | user: { | ||||
| user: { | user: { |
| <div className="r-b-rectangle"></div> | <div className="r-b-rectangle"></div> | ||||
| <div className="top-candidates-container"> | <div className="top-candidates-container"> | ||||
| {!matches ? ( | {!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 | Kandidati | ||||
| </p> | </p> | ||||
| )} | )} | ||||
| </IconButton> | </IconButton> | ||||
| ) : ( | ) : ( | ||||
| <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} | onClick={changeView} | ||||
| > | > | ||||
| Tablicni prikaz | Tablicni prikaz | ||||
| )} | )} | ||||
| {!matches ? ( | {!matches ? ( | ||||
| <IconButton | <IconButton | ||||
| className="c-btn c-btn--primary-outlined candidate-btn" | |||||
| className="c-btn c-btn--primary-outlined candidate-btn candidate-btn-filters1" | |||||
| onClick={handleToggleFiltersDrawer} | onClick={handleToggleFiltersDrawer} | ||||
| > | > | ||||
| Filteri | Filteri | ||||
| </IconButton> | </IconButton> | ||||
| ) : ( | ) : ( | ||||
| <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} | onClick={handleToggleFiltersDrawer} | ||||
| > | > | ||||
| <img | <img |
| const reportWebVitals = onPerfEntry => { | |||||
| /* istanbul ignore file */ | |||||
| const reportWebVitals = (onPerfEntry) => { | |||||
| if (onPerfEntry && onPerfEntry instanceof Function) { | 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); | getCLS(onPerfEntry); | ||||
| getFID(onPerfEntry); | getFID(onPerfEntry); | ||||
| getFCP(onPerfEntry); | getFCP(onPerfEntry); |
| import requestStatusMiddleware from './middleware/requestStatusMiddleware'; | import requestStatusMiddleware from './middleware/requestStatusMiddleware'; | ||||
| import internalServerErrorMiddleware from './middleware/internalServerErrorMiddleware'; | import internalServerErrorMiddleware from './middleware/internalServerErrorMiddleware'; | ||||
| /* istanbul ignore file */ | |||||
| const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; | ||||
| const sagaMiddleware = createSagaMiddleware(); | const sagaMiddleware = createSagaMiddleware(); | ||||
| export default createStore( | export default createStore( |
| import { format } from 'date-fns'; | import { format } from 'date-fns'; | ||||
| import { enUS } from 'date-fns/locale'; | import { enUS } from 'date-fns/locale'; | ||||
| import i18next from 'i18next'; | |||||
| export function formatDate(date, fmt = 'dd.MM.y', locale = enUS) { | export function formatDate(date, fmt = 'dd.MM.y', locale = enUS) { | ||||
| const dt = new Date(date); | const dt = new Date(date); | ||||
| return format(dt, 'hh:mm dd.MM.y'); | 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) { | export function formatDateSrb(date) { | ||||
| const dt = new Date(date); | const dt = new Date(date); | ||||
| return format(dt, 'dd.MM.'); | return format(dt, 'dd.MM.'); |
| export const parseEnumType = (typeArray, index) => typeArray[index - 1]; |
| /* istanbul ignore file */ | |||||
| const random = (arr) => { | const random = (arr) => { | ||||
| return arr[Math.floor(Math.random() * arr.length)]; | return arr[Math.floor(Math.random() * arr.length)]; | ||||
| }; | }; |