| REACT_APP_BASE_API_URL=https://portalgatewayapi.bullioninternational.info/ |
| { | |||||
| "env": { | |||||
| "browser": true, | |||||
| "es2021": true | |||||
| }, | |||||
| "extends": [ | |||||
| "plugin:react/recommended", | |||||
| "standard-with-typescript" | |||||
| ], | |||||
| "overrides": [ | |||||
| ], | |||||
| "parserOptions": { | |||||
| "ecmaVersion": "latest", | |||||
| "sourceType": "module", | |||||
| "project": [ | |||||
| "./tsconfig.json" | |||||
| ] | |||||
| }, | |||||
| "plugins": [ | |||||
| "react" | |||||
| ], | |||||
| "rules": { | |||||
| "semi": "off", | |||||
| "@typescript-eslint/semi": ["error"], | |||||
| "no-use-before-define": "off", | |||||
| "@typescript-eslint/no-use-before-define": ["error"] | |||||
| } | |||||
| } |
| "@emotion/react": "^11.10.5", | "@emotion/react": "^11.10.5", | ||||
| "@emotion/styled": "^11.10.5", | "@emotion/styled": "^11.10.5", | ||||
| "@faker-js/faker": "^7.6.0", | "@faker-js/faker": "^7.6.0", | ||||
| "@mui/icons-material": "^5.10.9", | |||||
| "@mui/material": "^5.10.12", | "@mui/material": "^5.10.12", | ||||
| "@mui/x-data-grid": "^5.17.10", | |||||
| "@reduxjs/toolkit": "^1.9.0", | |||||
| "@testing-library/jest-dom": "^5.14.1", | "@testing-library/jest-dom": "^5.14.1", | ||||
| "@testing-library/react": "^13.0.0", | "@testing-library/react": "^13.0.0", | ||||
| "@testing-library/user-event": "^13.2.1", | "@testing-library/user-event": "^13.2.1", | ||||
| "@types/jest": "^27.0.1", | "@types/jest": "^27.0.1", | ||||
| "@types/jsonwebtoken": "^8.5.9", | |||||
| "@types/node": "^16.7.13", | "@types/node": "^16.7.13", | ||||
| "@types/react": "^18.0.0", | "@types/react": "^18.0.0", | ||||
| "@types/react-dom": "^18.0.0", | "@types/react-dom": "^18.0.0", | ||||
| "@types/react-redux": "^7.1.24", | |||||
| "@types/react-router-dom": "^5.3.3", | |||||
| "@types/redux-saga": "^0.10.5", | |||||
| "axios": "^1.1.3", | "axios": "^1.1.3", | ||||
| "date-fns": "^2.29.3", | "date-fns": "^2.29.3", | ||||
| "formik": "^2.2.9", | |||||
| "i18next": "^22.0.4", | "i18next": "^22.0.4", | ||||
| "json-server": "^0.17.1", | |||||
| "jsonwebtoken": "^8.5.1", | |||||
| "lodash": "^4.17.21", | |||||
| "lodash.isempty": "^4.4.0", | |||||
| "numeral": "^2.0.6", | |||||
| "owasp-password-strength-test": "^1.3.0", | |||||
| "qs": "^6.11.0", | "qs": "^6.11.0", | ||||
| "react": "^18.2.0", | "react": "^18.2.0", | ||||
| "react-currency-input-field": "^3.6.9", | |||||
| "react-dom": "^18.2.0", | "react-dom": "^18.2.0", | ||||
| "react-helmet-async": "^1.3.0", | "react-helmet-async": "^1.3.0", | ||||
| "react-i18next": "^12.0.0", | "react-i18next": "^12.0.0", | ||||
| "react-scripts": "5.0.1", | |||||
| "scss": "^0.2.4", | |||||
| "typescript": "^4.4.2", | |||||
| "web-vitals": "^2.1.0" | |||||
| "react-jwt": "^1.1.7", | |||||
| "react-number-format": "^5.1.1", | |||||
| "react-redux": "^8.0.5", | |||||
| "react-router": "^6.4.3", | |||||
| "react-router-dom": "^6.4.3", | |||||
| "react-scripts": "^5.0.1", | |||||
| "react-select": "^5.6.0", | |||||
| "redux": "^4.2.0", | |||||
| "redux-saga": "^1.2.1", | |||||
| "typescript": "*", | |||||
| "web-vitals": "^2.1.0", | |||||
| "yup": "^0.32.11" | |||||
| }, | }, | ||||
| "scripts": { | "scripts": { | ||||
| "start": "react-scripts start", | "start": "react-scripts start", | ||||
| "build": "react-scripts build", | "build": "react-scripts build", | ||||
| "test": "react-scripts test", | "test": "react-scripts test", | ||||
| "eject": "react-scripts eject" | |||||
| }, | |||||
| "eslintConfig": { | |||||
| "extends": [ | |||||
| "react-app", | |||||
| "react-app/jest" | |||||
| ] | |||||
| "eject": "react-scripts eject", | |||||
| "json-serve": "json-server --watch src/db/db.json --port=4000" | |||||
| }, | }, | ||||
| "browserslist": { | "browserslist": { | ||||
| "production": [ | "production": [ | ||||
| "last 1 firefox version", | "last 1 firefox version", | ||||
| "last 1 safari version" | "last 1 safari version" | ||||
| ] | ] | ||||
| }, | |||||
| "devDependencies": { | |||||
| "@types/lodash.isempty": "^4.4.7", | |||||
| "@types/numeral": "^2.0.2", | |||||
| "@types/owasp-password-strength-test": "^1.3.0", | |||||
| "@types/sass": "^1.43.1", | |||||
| "@types/sass-loader": "^8.0.3", | |||||
| "@types/webpack": "^5.28.0", | |||||
| "@typescript-eslint/eslint-plugin": "^5.0.0", | |||||
| "@typescript-eslint/parser": "^5.43.0", | |||||
| "eslint": "^8.0.1", | |||||
| "eslint-config-airbnb": "^19.0.4", | |||||
| "eslint-config-prettier": "^8.5.0", | |||||
| "eslint-config-standard-with-typescript": "^23.0.0", | |||||
| "eslint-plugin-import": "^2.25.2", | |||||
| "eslint-plugin-jsx-a11y": "^6.6.1", | |||||
| "eslint-plugin-n": "^15.0.0", | |||||
| "eslint-plugin-promise": "^6.0.0", | |||||
| "eslint-plugin-react": "^7.31.10", | |||||
| "eslint-plugin-react-hooks": "^4.6.0", | |||||
| "prettier": "^2.7.1", | |||||
| "sass": "^1.56.0" | |||||
| } | } | ||||
| } | } |
| import React from 'react'; | import React from 'react'; | ||||
| import logo from './logo.svg'; | |||||
| import './App.css'; | |||||
| import { Helmet } from 'react-helmet-async'; | |||||
| import i18next from 'i18next'; | |||||
| import { BrowserRouter } from 'react-router-dom'; | |||||
| import AppRoutes from './AppRoutes'; | |||||
| function App() { | function App() { | ||||
| return ( | return ( | ||||
| <div className="App"> | |||||
| <header className="App-header"> | |||||
| <img src={logo} className="App-logo" alt="logo" /> | |||||
| <p> | |||||
| Edit <code>src/App.tsx</code> and save to reload. | |||||
| </p> | |||||
| <a | |||||
| className="App-link" | |||||
| href="https://reactjs.org" | |||||
| target="_blank" | |||||
| rel="noopener noreferrer" | |||||
| > | |||||
| Learn React | |||||
| </a> | |||||
| </header> | |||||
| </div> | |||||
| <> | |||||
| <BrowserRouter> | |||||
| <Helmet> | |||||
| <title> | |||||
| {i18next.t('app.title')} | |||||
| </title> | |||||
| </Helmet> | |||||
| <main className='l-page'> | |||||
| <AppRoutes /> | |||||
| </main> | |||||
| </BrowserRouter> | |||||
| </> | |||||
| ); | ); | ||||
| } | } | ||||
| import React from "react"; | |||||
| import { Route, Routes } from "react-router-dom"; | |||||
| import { | |||||
| LOGIN_PAGE, | |||||
| HOME_PAGE, | |||||
| FORGOT_PASSWORD_PAGE, | |||||
| NOT_FOUND_PAGE, | |||||
| ERROR_PAGE, | |||||
| } from "./constants/pages"; | |||||
| // import LoginPage from './pages/LoginPage/LoginPage'; | |||||
| import LoginPage from "./pages/LoginPage/LoginPageMUI"; | |||||
| // import HomePage from './pages/HomePage/HomePage'; | |||||
| import HomePage from "./pages/HomePage/HomePageMUI"; | |||||
| import NotFoundPage from "./pages/ErrorPages/NotFoundPage"; | |||||
| import ErrorPage from "./pages/ErrorPages/ErrorPage"; | |||||
| // import ForgotPasswordPage from './pages/ForgotPasswordPage/ForgotPasswordPage'; | |||||
| import ForgotPasswordPage from "./pages/ForgotPasswordPage/ForgotPasswordPage"; | |||||
| import PrivateRoute from "./components/Router/PrivateRoute"; | |||||
| const AppRoutes = () => ( | |||||
| <Routes> | |||||
| <Route path="*" element={<NotFoundPage />} /> | |||||
| <Route path={LOGIN_PAGE} element={<LoginPage />} /> | |||||
| <Route path={NOT_FOUND_PAGE} element={<NotFoundPage />} /> | |||||
| <Route path={ERROR_PAGE} element={<ErrorPage />} /> | |||||
| <Route path={FORGOT_PASSWORD_PAGE} element={<ForgotPasswordPage />} /> | |||||
| <Route path={HOME_PAGE} element={<PrivateRoute />}> | |||||
| <Route path={HOME_PAGE} element={<HomePage />} /> | |||||
| </Route> | |||||
| </Routes> | |||||
| ); | |||||
| export default AppRoutes; |
| import React, { ReactNode } from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| interface AuthProps { | |||||
| children: ReactNode; | |||||
| } | |||||
| const Auth: React.FC<AuthProps> = ({ children }) => { | |||||
| const { t } = useTranslation(); | |||||
| return ( | |||||
| <div className="c-auth"> | |||||
| <h1 className="c-auth__title">{t(`login.welcome`)}</h1> | |||||
| {children} | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default Auth; |
| import React, { ReactNode } from 'react'; | |||||
| import SectionLoader from '../Loader/SectionLoader'; | |||||
| interface AuthCardProps { | |||||
| children: ReactNode; | |||||
| title?: string; | |||||
| subtitle?: string; | |||||
| isLoading?: boolean; | |||||
| } | |||||
| const AuthCard: React.FC<AuthCardProps> = ({ children, title, subtitle, isLoading }) => { | |||||
| return ( | |||||
| <div className="c-auth-card"> | |||||
| <SectionLoader isLoading={isLoading ? isLoading : false}> | |||||
| <h1 className="c-auth-card__title">{title}</h1> | |||||
| <h2 className="c-auth-card__subtitle">{subtitle}</h2> | |||||
| {children} | |||||
| </SectionLoader> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default AuthCard; |
| import React, { useRef } from 'react'; | |||||
| type Size = 'sm' | 'md' | 'lg' | 'xl'; | |||||
| type Type = 'button' | 'submit' | 'reset'; | |||||
| type TextTransform = 'uppercase' | 'capitalize'; | |||||
| type MinWidth = 'auto' | 'none'; | |||||
| interface ButtonProps { | |||||
| variant?: string; | |||||
| size?: Size; | |||||
| children?: React.ReactNode; | |||||
| authButton?: boolean; | |||||
| type?: Type; | |||||
| onClick?: () => void; | |||||
| textTransform?: TextTransform; | |||||
| className?: string; | |||||
| disabled?: boolean; | |||||
| hidden?: boolean; | |||||
| minWidth?: MinWidth; | |||||
| } | |||||
| const Button: React.FC<ButtonProps> = ({ | |||||
| variant, | |||||
| size, | |||||
| children, | |||||
| authButton, | |||||
| type, | |||||
| onClick, | |||||
| textTransform, | |||||
| className, | |||||
| disabled, | |||||
| hidden, | |||||
| minWidth, | |||||
| ...restProps | |||||
| }) => { | |||||
| const buttonRef = useRef<HTMLButtonElement>(null); | |||||
| function styles() { | |||||
| let style = 'c-btn'; | |||||
| if (variant) { | |||||
| style += ` c-btn--${variant}`; | |||||
| } | |||||
| if (size) { | |||||
| style += ` c-btn--${size}`; | |||||
| } | |||||
| if (textTransform) { | |||||
| style += ` c-btn--${textTransform}`; | |||||
| } | |||||
| if (authButton) { | |||||
| style += ` c-btn--auth`; | |||||
| } | |||||
| if (minWidth) { | |||||
| style += ` c-btn--${minWidth}`; | |||||
| } | |||||
| if (hidden) { | |||||
| style += ` c-btn--hidden`; | |||||
| } | |||||
| if (className) { | |||||
| style += ` ${className}`; | |||||
| } | |||||
| return style; | |||||
| } | |||||
| function handleClick() { | |||||
| if (buttonRef.current != null) { | |||||
| buttonRef.current.blur(); | |||||
| } | |||||
| if (typeof onClick === 'function') { | |||||
| onClick(); | |||||
| } | |||||
| } | |||||
| return ( | |||||
| <button | |||||
| ref={buttonRef} | |||||
| className={styles()} | |||||
| onClick={handleClick} | |||||
| type={type} | |||||
| disabled={disabled} | |||||
| {...restProps} | |||||
| > | |||||
| {children} | |||||
| </button> | |||||
| ); | |||||
| }; | |||||
| Button.defaultProps = { | |||||
| type: 'button', | |||||
| }; | |||||
| export default Button; |
| import React, { ReactNode, useRef } from 'react'; | |||||
| interface IconButtonProps { | |||||
| children: ReactNode; | |||||
| onClick: () => void; | |||||
| className: string; | |||||
| } | |||||
| const IconButton: React.FC<IconButtonProps> = ({ children, onClick, className }) => { | |||||
| const buttonRef = useRef<HTMLButtonElement>(null); | |||||
| function handleClick() { | |||||
| if (buttonRef.current != null) { | |||||
| buttonRef.current.blur(); | |||||
| } | |||||
| if (typeof onClick === 'function') { | |||||
| onClick(); | |||||
| } | |||||
| } | |||||
| return ( | |||||
| <button | |||||
| type="button" | |||||
| ref={buttonRef} | |||||
| onClick={handleClick} | |||||
| className={`c-icon-button ${className && className}`} | |||||
| > | |||||
| {children} | |||||
| </button> | |||||
| ); | |||||
| }; | |||||
| export default IconButton; |
| import React, { useEffect, useState, useRef, ReactNode } from 'react'; | |||||
| import { ErrorMessage } from 'formik'; | |||||
| import { ReactComponent as Search } from '../../assets/images/svg/search.svg'; | |||||
| import { ReactComponent as EyeOn } from '../../assets/images/svg/eye-on.svg'; | |||||
| import { ReactComponent as EyeOff } from '../../assets/images/svg/eye-off.svg'; | |||||
| import { ReactComponent as CapsLock } from '../../assets/images/svg/caps-lock.svg'; | |||||
| import IconButton from '../IconButton/IconButton'; | |||||
| interface Form { | |||||
| errors: Array<string>; | |||||
| setFieldError: (fieldName: string, errorMessage: string) => void; | |||||
| touched: any; | |||||
| } | |||||
| interface BaseInputFieldProps { | |||||
| type: string; | |||||
| label: string; | |||||
| field: any; | |||||
| placeholder: string; | |||||
| clearPlaceholderOnFocus?: boolean; | |||||
| isSearch?: boolean; | |||||
| className?: string; | |||||
| disabled: boolean; | |||||
| centerText?: boolean; | |||||
| link?: ReactNode; | |||||
| errorMessage?: string; | |||||
| autoFocus?: boolean; | |||||
| isCapsLockOn?: boolean; | |||||
| form: Form; | |||||
| onKeyDown?: any; | |||||
| onChange?: (e: any) => void; | |||||
| } | |||||
| const BaseInputField: React.FC<BaseInputFieldProps> = ({ | |||||
| type, | |||||
| label, | |||||
| field, | |||||
| form, | |||||
| placeholder, | |||||
| clearPlaceholderOnFocus = true, | |||||
| isSearch, | |||||
| className, | |||||
| disabled, | |||||
| centerText, | |||||
| link, | |||||
| errorMessage, | |||||
| autoFocus, | |||||
| isCapsLockOn, | |||||
| ...props | |||||
| }) => { | |||||
| const [inputPlaceholder, setPlaceholder] = useState(placeholder); | |||||
| const inputField = useRef<HTMLInputElement>(null); | |||||
| useEffect(() => { | |||||
| if (autoFocus) { | |||||
| if (inputField.current !== null) { | |||||
| inputField.current.focus(); | |||||
| } | |||||
| } | |||||
| }, [autoFocus, inputField]); | |||||
| useEffect(() => { | |||||
| if (errorMessage) { | |||||
| form.setFieldError(field.name, errorMessage); | |||||
| } | |||||
| }, [errorMessage]); // eslint-disable-line | |||||
| useEffect(() => { | |||||
| setPlaceholder(placeholder); | |||||
| }, [placeholder]); | |||||
| const [inputType, setInputType] = useState('password'); | |||||
| const passwordInput = type === 'password' ? ' c-input--password' : ''; | |||||
| const showPassword = () => { | |||||
| if (inputType === 'password') { | |||||
| setInputType('text'); | |||||
| } else { | |||||
| setInputType('password'); | |||||
| } | |||||
| }; | |||||
| // Nester Formik Field Names get bugged because of Undefined values, so i had to fix it like this | |||||
| // If you ask why 0 and 1? I dont see a need for forms to be nested more then 2 levels? | |||||
| const fieldName: any = field.name.split('.'); | |||||
| const formError = | |||||
| fieldName[0] && fieldName[1] | |||||
| ? form.errors[fieldName[0]] && form.errors[fieldName[0]][fieldName[1]] | |||||
| : form.errors[fieldName[0]]; | |||||
| const formTouched = | |||||
| fieldName[0] && fieldName[1] | |||||
| ? form.touched[fieldName[0]] && form.touched[fieldName[0]][fieldName[1]] | |||||
| : form.touched[fieldName[0]]; | |||||
| function styles() { | |||||
| let style = 'c-input'; | |||||
| if (formError && formTouched) { | |||||
| style += ` c-input--error`; | |||||
| } | |||||
| if (type === 'password') { | |||||
| style += ` c-input--password`; | |||||
| } | |||||
| if (isSearch) { | |||||
| style += ` c-input--search`; | |||||
| } | |||||
| if (centerText) { | |||||
| style += ` c-input--center-text`; | |||||
| } | |||||
| if (type === 'number') { | |||||
| style += ` c-input--demi-bold`; | |||||
| } | |||||
| if (className) { | |||||
| style += ` ${className}`; | |||||
| } | |||||
| return style; | |||||
| } | |||||
| const additionalActions = () => { | |||||
| if (!clearPlaceholderOnFocus) { | |||||
| return null; | |||||
| } | |||||
| return { | |||||
| onFocus: () => { | |||||
| setPlaceholder(''); | |||||
| }, | |||||
| onBlur: (e: any) => { | |||||
| setPlaceholder(placeholder); | |||||
| field.onBlur(e); | |||||
| }, | |||||
| }; | |||||
| }; | |||||
| return ( | |||||
| <div className={styles()}> | |||||
| {!!label && ( | |||||
| <label className="c-input__label" htmlFor={field.name}> | |||||
| {label} | |||||
| </label> | |||||
| )} | |||||
| {link && <div className="c-input__link">{link}</div>} | |||||
| <div className="c-input__field-wrap"> | |||||
| <input | |||||
| ref={inputField} | |||||
| type={type === 'password' ? inputType : type} | |||||
| placeholder={inputPlaceholder} | |||||
| disabled={disabled} | |||||
| {...field} | |||||
| {...props} | |||||
| {...additionalActions()} | |||||
| className="c-input__field" | |||||
| /> | |||||
| {!!isSearch && <Search className="c-input__icon" />} | |||||
| {!!passwordInput && ( | |||||
| <> | |||||
| {isCapsLockOn && <CapsLock className="c-input__caps-lock" />} | |||||
| <IconButton | |||||
| onClick={() => { | |||||
| showPassword(); | |||||
| }} | |||||
| className="c-input__icon" | |||||
| > | |||||
| {inputType === 'password' ? <EyeOff /> : <EyeOn />} | |||||
| </IconButton> | |||||
| </> | |||||
| )} | |||||
| </div> | |||||
| <ErrorMessage name={field.name}> | |||||
| {(errorMessage) => ( | |||||
| <span className="c-input__error">{errorMessage}</span> | |||||
| )} | |||||
| </ErrorMessage> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default BaseInputField; |
| 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'; | |||||
| interface CheckboxProps { | |||||
| className: string; | |||||
| children: React.ReactNode; | |||||
| name: string; | |||||
| checked: boolean; | |||||
| field: { | |||||
| onChange: () => void; | |||||
| }; | |||||
| onChange: () => void; | |||||
| } | |||||
| const Checkbox: React.FC<CheckboxProps> = ({ 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> | |||||
| ); | |||||
| export default Checkbox; |
| import React, { useEffect, useRef } from 'react'; | |||||
| import { ErrorMessage, useField } from 'formik'; | |||||
| import CurrencyInput from 'react-currency-input-field'; | |||||
| import { | |||||
| PLUS_SYMBOL, | |||||
| MINUS_SYMBOL, | |||||
| NUMPAD_MINUS_SYMBOL, | |||||
| NUMPAD_PLUS_SYMBOL, | |||||
| K_KEYCODE, | |||||
| } from '../../constants/keyCodeConstants'; | |||||
| import { formatMoneyNumeral } from '../../util/helpers/numeralHelpers'; | |||||
| interface CurrencyFieldProps { | |||||
| autoFocus: boolean; | |||||
| notCentered: boolean; | |||||
| notBold: boolean; | |||||
| label: string; | |||||
| onChange: (value: number | undefined) => void; | |||||
| value: string | number; | |||||
| } | |||||
| const CurrencyField: React.FC<CurrencyFieldProps> = ({ | |||||
| autoFocus, | |||||
| notCentered, | |||||
| notBold, | |||||
| label, | |||||
| onChange, | |||||
| value, | |||||
| ...props | |||||
| }) => { | |||||
| /* @ts-ignore */ | |||||
| const [field, meta] = useField(props); | |||||
| const inputField = useRef<HTMLInputElement>(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) { | |||||
| if (inputField.current !== null) { | |||||
| inputField.current.focus(); | |||||
| } | |||||
| } | |||||
| }, [autoFocus, inputField]); | |||||
| const onKeydownHandler = (event: React.KeyboardEvent<HTMLInputElement>) => { | |||||
| 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(Number(value)); | |||||
| }} | |||||
| onKeyDown={onKeydownHandler} | |||||
| ref={inputField} | |||||
| defaultValue={0} | |||||
| value={value} | |||||
| /> | |||||
| ) : ( | |||||
| <CurrencyInput | |||||
| {...props} | |||||
| prefix={prefixSymbol()} | |||||
| onValueChange={(value) => { | |||||
| onChange(Number(value)); | |||||
| }} | |||||
| onKeyDown={onKeydownHandler} | |||||
| ref={inputField} | |||||
| /> | |||||
| )} | |||||
| <ErrorMessage name={field.name}> | |||||
| {(errorMessage) => ( | |||||
| <span className="c-currency-field__error">{errorMessage}</span> | |||||
| )} | |||||
| </ErrorMessage> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default CurrencyField; |
| import React from 'react'; | |||||
| import BaseInputField from './BaseInputField'; | |||||
| interface EmailFieldProps{ | |||||
| field: any; | |||||
| form: any; | |||||
| label: string; | |||||
| placeholder: string; | |||||
| disabled: boolean; | |||||
| } | |||||
| const EmailField: React.FC<EmailFieldProps> = ({ | |||||
| field, | |||||
| form, | |||||
| label, | |||||
| placeholder, | |||||
| disabled, | |||||
| ...props | |||||
| }) => ( | |||||
| <BaseInputField | |||||
| type="email" | |||||
| label={label} | |||||
| placeholder={placeholder} | |||||
| disabled={disabled} | |||||
| form={form} | |||||
| field={field} | |||||
| {...props} | |||||
| /> | |||||
| ); | |||||
| export default EmailField; |
| import React from 'react'; | |||||
| 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'; | |||||
| interface NumberFieldProps { | |||||
| label: string; | |||||
| placeholder: string; | |||||
| disabled: boolean; | |||||
| preventAllExceptNumbers: boolean; | |||||
| field: any; | |||||
| form: any; | |||||
| } | |||||
| const NumberField: React.FC<NumberFieldProps> = ({ | |||||
| field, | |||||
| form, | |||||
| label, | |||||
| placeholder, | |||||
| disabled, | |||||
| preventAllExceptNumbers, | |||||
| ...props | |||||
| }) => { | |||||
| const onKeydownHandler = (event: React.KeyboardEvent) => { | |||||
| 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={onKeydownHandler} | |||||
| /> | |||||
| ); | |||||
| }; | |||||
| export default NumberField; |
| import React, { ReactElement, useState } from 'react'; | |||||
| import BaseInputField from './BaseInputField'; | |||||
| import PasswordStrength from './PasswordStrength'; | |||||
| interface Field { | |||||
| name: string; | |||||
| onFocus: () => void; | |||||
| onBlur: () => void; | |||||
| onChange: (e: any) => void; | |||||
| } | |||||
| interface Form { | |||||
| errors: Array<string>; | |||||
| setFieldError: (fieldName: string, errorMessage: string) => void; | |||||
| touched: any; | |||||
| } | |||||
| interface PasswordFieldProps { | |||||
| field: Field; | |||||
| form: Form; | |||||
| label: string; | |||||
| placeholder: string; | |||||
| disabled: boolean; | |||||
| shouldTestPasswordStrength: boolean; | |||||
| autoFocus: boolean; | |||||
| } | |||||
| const PasswordField: React.FC<PasswordFieldProps> = ({ | |||||
| field, | |||||
| form, | |||||
| label, | |||||
| placeholder, | |||||
| disabled, | |||||
| shouldTestPasswordStrength, | |||||
| autoFocus, | |||||
| ...props | |||||
| }) => { | |||||
| const [passwordValue, setPasswordValue] = useState(''); | |||||
| const [isCapsLockOn, setIsCapsLockOn] = useState(false); | |||||
| const onChange = (e: any) => { | |||||
| if (shouldTestPasswordStrength) { | |||||
| const { value } = e.target; | |||||
| setPasswordValue(value); | |||||
| } | |||||
| field.onChange(e); | |||||
| }; | |||||
| const onKeyDown = (keyEvent: KeyboardEvent) => { | |||||
| if (keyEvent.getModifierState('CapsLock')) { | |||||
| setIsCapsLockOn(true); | |||||
| } else { | |||||
| setIsCapsLockOn(false); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <div className="c-password"> | |||||
| <BaseInputField | |||||
| type="password" | |||||
| label={label} | |||||
| placeholder={placeholder} | |||||
| disabled={disabled} | |||||
| form={form} | |||||
| field={field} | |||||
| {...props} | |||||
| onChange={onChange} | |||||
| autoFocus={autoFocus} | |||||
| onKeyDown={onKeyDown} | |||||
| isCapsLockOn={isCapsLockOn} | |||||
| /> | |||||
| {shouldTestPasswordStrength && ( | |||||
| <PasswordStrength | |||||
| passwordValue={passwordValue} | |||||
| shouldTestPasswordStrength | |||||
| /> | |||||
| )} | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default PasswordField; |
| import React, { useEffect, useRef, useState } from 'react'; | |||||
| import owasp from 'owasp-password-strength-test'; | |||||
| import i18next from 'i18next'; | |||||
| owasp.config({ | |||||
| minOptionalTestsToPass: 3, | |||||
| }); | |||||
| const passwordStrengthOptions = [ | |||||
| { | |||||
| strength: 'weak', | |||||
| color: '#FF5028', | |||||
| }, | |||||
| { | |||||
| strength: 'average', | |||||
| color: '#FDB942', | |||||
| }, | |||||
| { | |||||
| strength: 'good', | |||||
| color: '#06BEE7', | |||||
| }, | |||||
| { | |||||
| strength: 'strong', | |||||
| color: '#00876A', | |||||
| }, | |||||
| ]; | |||||
| /** | |||||
| * User must pass a required test and at least 3 optional. | |||||
| * @param result - owasp result | |||||
| * @returns {number} - index of password strength 0-3 | |||||
| */ | |||||
| function getPasswordStrengthIndex(result) { | |||||
| // requirement for strong password is required test passed and at least 3 optional tests | |||||
| if (result.strong) { | |||||
| return 3; | |||||
| } | |||||
| if (!result.strong && result.optionalTestsPassed >= 3) { | |||||
| return 2; | |||||
| } | |||||
| if (result.optionalTestsPassed <= 0) { | |||||
| return 0; | |||||
| } | |||||
| return result.optionalTestsPassed - 1; | |||||
| } | |||||
| const PasswordStrength = ({ | |||||
| shouldTestPasswordStrength, | |||||
| passwordValue, | |||||
| passwordStrengthTestsRequired, | |||||
| }) => { | |||||
| const strengthContainer = useRef(null); | |||||
| const [passwordStrength, setPasswordStrength] = useState({ | |||||
| width: 0, | |||||
| color: 'red', | |||||
| }); | |||||
| const [error, setError] = useState(''); | |||||
| useEffect(() => { | |||||
| if (shouldTestPasswordStrength && passwordValue) { | |||||
| const bBox = strengthContainer.current.getBoundingClientRect(); | |||||
| const result = owasp.test(passwordValue); | |||||
| console.log(typeof result); | |||||
| const passwordStrengthIndex = getPasswordStrengthIndex(result); | |||||
| const passwordOption = passwordStrengthOptions[passwordStrengthIndex]; | |||||
| const width = !passwordValue | |||||
| ? 0 | |||||
| : (bBox.width * (passwordStrengthIndex + 1)) / | |||||
| passwordStrengthTestsRequired; | |||||
| setPasswordStrength({ width, color: passwordOption.color }); | |||||
| const strength = i18next.t(`password.${passwordOption.strength}`); | |||||
| setError(i18next.t('login.passwordStrength', { strength })); | |||||
| } | |||||
| }, [ | |||||
| passwordValue, | |||||
| shouldTestPasswordStrength, | |||||
| passwordStrengthTestsRequired, | |||||
| ]); | |||||
| if (!shouldTestPasswordStrength || !passwordValue) { | |||||
| return null; | |||||
| } | |||||
| const renderError = () => { | |||||
| if (!error) { | |||||
| return null; | |||||
| } | |||||
| return ( | |||||
| <div | |||||
| className="c-input--error" | |||||
| style={{ | |||||
| color: passwordStrength.color, | |||||
| }} | |||||
| > | |||||
| {error} | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| return ( | |||||
| <div ref={strengthContainer} className="c-password-strength__container"> | |||||
| <div className="c-password-strength__line--wrapper"> | |||||
| <div | |||||
| className="c-password-strength__line" | |||||
| style={{ | |||||
| backgroundColor: passwordStrength.color, | |||||
| width: passwordStrength.width, | |||||
| }} | |||||
| /> | |||||
| </div> | |||||
| {renderError()} | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| PasswordStrength.defaultProps = { | |||||
| passwordStrengthTestsRequired: 4, | |||||
| }; | |||||
| export default PasswordStrength; |
| 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 { | |||||
| BACKSPACE_KEYCODE, | |||||
| TAB_KEYCODE, | |||||
| RIGHT_ARROW_KEYCODE, | |||||
| LEFT_ARROW_KEYCODE, | |||||
| } from '../../constants/keyCodeConstants'; | |||||
| import BaseInputField from './BaseInputField'; | |||||
| interface Field { | |||||
| name: string; | |||||
| onFocus: () => void; | |||||
| onBlur: () => void; | |||||
| } | |||||
| interface Form { | |||||
| errors: Array<string>; | |||||
| setFieldError: (fieldName: string, errorMessage: string) => void; | |||||
| touched: any; | |||||
| } | |||||
| interface TextFieldProps { | |||||
| field: Field; | |||||
| form: Form; | |||||
| label: string; | |||||
| placeholder: string; | |||||
| disabled: boolean; | |||||
| centerText: boolean; | |||||
| autoFocus: boolean; | |||||
| preventAllExceptNumbers: boolean; | |||||
| } | |||||
| const TextField: React.FC<TextFieldProps> = ({ | |||||
| field, | |||||
| form, | |||||
| label, | |||||
| placeholder, | |||||
| disabled, | |||||
| centerText, | |||||
| autoFocus, | |||||
| preventAllExceptNumbers, | |||||
| ...props | |||||
| }) => { | |||||
| const onKeydownHandler = (event: React.KeyboardEvent) => { | |||||
| if (preventAllExceptNumbers) { | |||||
| if ( | |||||
| event.keyCode === BACKSPACE_KEYCODE || | |||||
| event.keyCode === TAB_KEYCODE || | |||||
| event.keyCode === RIGHT_ARROW_KEYCODE || | |||||
| event.keyCode === LEFT_ARROW_KEYCODE | |||||
| ) { | |||||
| return; | |||||
| } | |||||
| if ( | |||||
| (event.keyCode < 58 && event.keyCode > 47) || | |||||
| (event.keyCode < 106 && event.keyCode > 95) | |||||
| ) { | |||||
| return; | |||||
| } | |||||
| event.preventDefault(); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <BaseInputField | |||||
| autoFocus={autoFocus} | |||||
| type="text" | |||||
| label={label} | |||||
| placeholder={placeholder} | |||||
| disabled={disabled} | |||||
| form={form} | |||||
| field={field} | |||||
| centerText={centerText} | |||||
| {...props} | |||||
| onKeyDown={onKeydownHandler} | |||||
| /> | |||||
| ); | |||||
| }; | |||||
| export default TextField; |
| import React, { ReactNode } from 'react'; | |||||
| interface BlockSectionLoaderProps { | |||||
| children: ReactNode; | |||||
| isLoading: boolean; | |||||
| fullHeight: boolean; | |||||
| noShadow: boolean; | |||||
| } | |||||
| const BlockSectionLoader: React.FC<BlockSectionLoaderProps> = ({ 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> | |||||
| ); | |||||
| export default BlockSectionLoader; |
| const FullPageLoader: React.FC = () => { | |||||
| return ( | |||||
| <div className="c-loader c-loader--page"> | |||||
| <div className="c-loader__icon" /> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default FullPageLoader; |
| import React, { ReactNode } from 'react'; | |||||
| interface SectionLoaderProps { | |||||
| children: ReactNode; | |||||
| isLoading: boolean; | |||||
| } | |||||
| const SectionLoader: React.FC<SectionLoaderProps> = ({ children, isLoading }) => ( | |||||
| <div className="c-loader__wrapper"> | |||||
| {children} | |||||
| {isLoading && ( | |||||
| <div className="c-loader"> | |||||
| <div className="c-loader__icon" /> | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| ); | |||||
| export default SectionLoader; |
| import React from 'react'; | |||||
| import { Backdrop, CircularProgress } from '@mui/material'; | |||||
| import { alpha } from '@mui/system'; | |||||
| interface BackdropComponentProps { | |||||
| position: 'absolute' | 'fixed'; | |||||
| isLoading: boolean; | |||||
| } | |||||
| const BackdropComponent: React.FC<BackdropComponentProps> = ({ position = 'fixed', isLoading }) => ( | |||||
| <Backdrop | |||||
| sx={{ | |||||
| // 'fixed' takes whole page, 'absolute' takes whole space of the parent element which needs to have 'relative' position | |||||
| position, | |||||
| backgroundColor: ({ palette }) => | |||||
| alpha(palette.background.default, palette.action.disabledOpacity), | |||||
| zIndex: ({ zIndex }) => zIndex.drawer + 1, | |||||
| }} | |||||
| open={isLoading} | |||||
| > | |||||
| <CircularProgress /> | |||||
| </Backdrop> | |||||
| ); | |||||
| export default BackdropComponent; |
| import React from 'react'; | |||||
| import { | |||||
| Dialog, | |||||
| DialogContent, | |||||
| DialogTitle, | |||||
| DialogActions, | |||||
| Button, | |||||
| useMediaQuery, | |||||
| useTheme, | |||||
| } from '@mui/material'; | |||||
| interface DialogComponentProps { | |||||
| title: string; | |||||
| content: any; | |||||
| onClose: () => void; | |||||
| open: boolean; | |||||
| maxWidth: 'xs' | 'sm' | 'md' | 'lg'| 'xl'; | |||||
| fullWidth: boolean; | |||||
| responsive: boolean; | |||||
| } | |||||
| const DialogComponent: React.FC<DialogComponentProps> = ({ | |||||
| 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> | |||||
| ); | |||||
| }; | |||||
| export default DialogComponent; |
| import React from 'react'; | |||||
| import { Drawer } from '@mui/material'; | |||||
| interface DrawerComponentProps { | |||||
| open: boolean; | |||||
| toggleOpen: () => void; | |||||
| content: any; | |||||
| anchor?: 'top' | 'right' | 'left' | 'bottom'; | |||||
| } | |||||
| const DrawerComponent: React.FC<DrawerComponentProps> = ({ open, toggleOpen, content, anchor = 'right' }) => ( | |||||
| <Drawer | |||||
| sx={{ | |||||
| minWidth: 250, | |||||
| '& .MuiDrawer-paper': { | |||||
| minWidth: 250, | |||||
| }, | |||||
| }} | |||||
| anchor={anchor} | |||||
| open={open} | |||||
| onClose={toggleOpen} | |||||
| > | |||||
| {content ? content : null} | |||||
| </Drawer> | |||||
| ); | |||||
| export default DrawerComponent; |
| import React from 'react'; | |||||
| import { Typography } from '@mui/material'; | |||||
| interface ErrorMessageComponentProps { | |||||
| error: string; | |||||
| } | |||||
| const ErrorMessageComponent: React.FC<ErrorMessageComponentProps> = ({ error }) => ( | |||||
| <Typography variant="body1" color="error" my={2}> | |||||
| {error} | |||||
| </Typography> | |||||
| ); | |||||
| export default ErrorMessageComponent; |
| 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: React.FC = () => { | |||||
| 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: React.FC = () => { | |||||
| 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: React.MouseEvent<any>) => { | |||||
| 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 { loadRandomData, updatePage, updateItemsPerPage, updateFilter, updateSort } from '../../../store/features/randomData/randomDataSlice'; | |||||
| import React, { useEffect, useState } from 'react'; | |||||
| import { | |||||
| Paper, | |||||
| Box, | |||||
| Grid, | |||||
| Typography, | |||||
| Divider, | |||||
| TablePagination, | |||||
| TextField, | |||||
| FormControl, | |||||
| InputLabel, | |||||
| Select, | |||||
| MenuItem, | |||||
| SelectChangeEvent, | |||||
| } 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'; | |||||
| 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'; | |||||
| console.log(items) | |||||
| // Use debounce to prevent too many rerenders | |||||
| const debouncedFilterText = useDebounce(filterText, 500); | |||||
| useEffect(() => { | |||||
| dispatch(loadRandomData(30)); | |||||
| dispatch(updateSort(sort)); | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| batch(() => { | |||||
| dispatch(updateFilter(filterText)); | |||||
| currentPage > 0 && dispatch(updatePage(0)); | |||||
| }); | |||||
| }, [debouncedFilterText]); | |||||
| const handleFilterTextChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | |||||
| const filterText = event.target.value; | |||||
| setFilterText(filterText); | |||||
| }; | |||||
| const handleSortChange = (event: SelectChangeEvent<string>) => { | |||||
| const sort = event.target.value; | |||||
| dispatch(updateSort(sort)); | |||||
| }; | |||||
| const handlePageChange = (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, newPage: number) => { | |||||
| dispatch(updatePage(newPage)); | |||||
| }; | |||||
| const handleItemsPerPageChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | |||||
| 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, | |||||
| SelectChangeEvent, | |||||
| } 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: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | |||||
| const filterText = event.target.value; | |||||
| setFilterText(filterText); | |||||
| }; | |||||
| const handleSortChange = (event: SelectChangeEvent<any>) => { | |||||
| const sort = event.target.value; | |||||
| setSort(sort); | |||||
| }; | |||||
| const handlePageChange = (event: React.MouseEvent<HTMLButtonElement, MouseEvent> | null, newPage: number) => { | |||||
| setPage(newPage); | |||||
| }; | |||||
| const handleItemsPerPageChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | |||||
| 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 && | |||||
| /* @ts-ignore */ | |||||
| 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: React.FC = () => { | |||||
| const [anchorEl, setAnchorEl] = useState(null); | |||||
| const open = Boolean(anchorEl); | |||||
| const handleClick = (event: React.MouseEvent<any>) => { | |||||
| 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, { useState, useMemo, useContext } from 'react'; | |||||
| import { | |||||
| AppBar, | |||||
| Badge, | |||||
| Box, | |||||
| IconButton, | |||||
| Toolbar, | |||||
| Typography, | |||||
| List, | |||||
| ListItem, | |||||
| ListItemButton, | |||||
| ListItemIcon, | |||||
| ListItemText, | |||||
| useMediaQuery, | |||||
| } from '@mui/material'; | |||||
| import { useTheme } from '@mui/system'; | |||||
| import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined'; | |||||
| import ShoppingBasketIcon from '@mui/icons-material/ShoppingBasket'; | |||||
| import Brightness4Icon from '@mui/icons-material/Brightness4'; | |||||
| import Brightness7Icon from '@mui/icons-material/Brightness7'; | |||||
| import MenuList from './MenuListComponent'; | |||||
| import Drawer from './DrawerComponent'; | |||||
| import { ColorModeContext } from '../../context/ColorModeContext'; | |||||
| const NavbarComponent = () => { | |||||
| const [openDrawer, setOpenDrawer] = useState(false); | |||||
| const theme = useTheme(); | |||||
| const matches = useMediaQuery(theme.breakpoints.down('sm')); | |||||
| const toggleColorMode = useContext(ColorModeContext); | |||||
| const handleToggleDrawer = () => { | |||||
| setOpenDrawer(!openDrawer); | |||||
| }; | |||||
| const drawerContent = useMemo( | |||||
| () => ( | |||||
| <List> | |||||
| <ListItemButton divider onClick={handleToggleDrawer}> | |||||
| <ListItemIcon> | |||||
| <ListItemText>Link 1</ListItemText> | |||||
| </ListItemIcon> | |||||
| </ListItemButton> | |||||
| <ListItem divider onClick={handleToggleDrawer}> | |||||
| <ListItemIcon> | |||||
| <ListItemText>Link 2</ListItemText> | |||||
| </ListItemIcon> | |||||
| </ListItem> | |||||
| <ListItem divider onClick={handleToggleDrawer}> | |||||
| <ListItemText>Link 3</ListItemText> | |||||
| </ListItem> | |||||
| <ListItem divider> | |||||
| {/*@ts-ignore*/} | |||||
| <IconButton onClick={toggleColorMode}> | |||||
| <ListItemText>Toggle {theme.palette.mode} mode</ListItemText> | |||||
| {theme.palette.mode === 'dark' ? ( | |||||
| <Brightness7Icon /> | |||||
| ) : ( | |||||
| <Brightness4Icon /> | |||||
| )} | |||||
| </IconButton> | |||||
| </ListItem> | |||||
| </List> | |||||
| ), | |||||
| [handleToggleDrawer] | |||||
| ); | |||||
| return ( | |||||
| <AppBar | |||||
| elevation={2} | |||||
| sx={{ backgroundColor: 'background.default', position: 'relative' }} | |||||
| > | |||||
| <Toolbar> | |||||
| <Box | |||||
| component="div" | |||||
| sx={{ | |||||
| display: 'flex', | |||||
| justifyContent: 'space-between', | |||||
| alignItems: 'center', | |||||
| width: '100%', | |||||
| }} | |||||
| > | |||||
| {matches ? ( | |||||
| <Drawer | |||||
| open={openDrawer} | |||||
| toggleOpen={handleToggleDrawer} | |||||
| content={drawerContent} | |||||
| /> | |||||
| ) : ( | |||||
| <Box sx={{ display: 'flex' }}> | |||||
| <Typography | |||||
| variant="h6" | |||||
| sx={{ | |||||
| marginRight: 3, | |||||
| cursor: 'pointer', | |||||
| color: 'text.primary', | |||||
| }} | |||||
| > | |||||
| Link 1 | |||||
| </Typography> | |||||
| <Typography | |||||
| variant="body1" | |||||
| sx={{ | |||||
| marginRight: 3, | |||||
| cursor: 'pointer', | |||||
| color: 'text.primary', | |||||
| }} | |||||
| > | |||||
| Link 2 | |||||
| </Typography> | |||||
| <Typography | |||||
| variant="subtitle1" | |||||
| sx={{ | |||||
| marginRight: 3, | |||||
| cursor: 'pointer', | |||||
| color: 'text.primary', | |||||
| }} | |||||
| > | |||||
| Link 3 | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| <Box> | |||||
| <MenuList /> | |||||
| </Box> | |||||
| <Box | |||||
| sx={{ | |||||
| display: 'flex', | |||||
| justifyContent: 'center', | |||||
| alignItems: 'center', | |||||
| }} | |||||
| > | |||||
| {matches ? ( | |||||
| <Box> | |||||
| <IconButton onClick={handleToggleDrawer}> | |||||
| <MenuOutlinedIcon /> | |||||
| </IconButton> | |||||
| </Box> | |||||
| ) : ( | |||||
| <Box> | |||||
| <IconButton> | |||||
| <Badge badgeContent={3} color="primary"> | |||||
| <ShoppingBasketIcon color="action" /> | |||||
| </Badge> | |||||
| </IconButton> | |||||
| {/*@ts-ignore*/} | |||||
| <IconButton sx={{ ml: 1 }} onClick={toggleColorMode}> | |||||
| {theme.palette.mode === 'dark' ? ( | |||||
| <Brightness7Icon /> | |||||
| ) : ( | |||||
| <Brightness4Icon /> | |||||
| )} | |||||
| </IconButton> | |||||
| </Box> | |||||
| )} | |||||
| </Box> | |||||
| </Box> | |||||
| </Toolbar> | |||||
| </AppBar> | |||||
| ); | |||||
| }; | |||||
| export default NavbarComponent; |
| import React from 'react'; | |||||
| import { Box, Popover } from '@mui/material'; | |||||
| interface PopoverComponentProps { | |||||
| open: boolean; | |||||
| anchorEl: any; | |||||
| onClose: () => void; | |||||
| content: any; | |||||
| } | |||||
| const PopoverComponent: React.FC<PopoverComponentProps> = ({ 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> | |||||
| ); | |||||
| }; | |||||
| export default PopoverComponent; |
| import React, { useEffect } from 'react'; | |||||
| import { Navigate, Outlet } from 'react-router'; | |||||
| import { useDispatch, useSelector } from 'react-redux'; | |||||
| import { authenticateUser } from '../../store/features/login/loginSlice'; | |||||
| import { selectIsUserAuthenticated } from '../../store/selectors/userSelectors'; | |||||
| import { LOGIN_PAGE } from '../../constants/pages'; | |||||
| const PrivateRoute = () => { | |||||
| const dispatch = useDispatch(); | |||||
| const isUserAuthenticated = useSelector(selectIsUserAuthenticated); | |||||
| useEffect(() => { | |||||
| if (!isUserAuthenticated) { | |||||
| dispatch(authenticateUser()); | |||||
| } | |||||
| }, [isUserAuthenticated]); // eslint-disable-line | |||||
| return isUserAuthenticated ? ( | |||||
| <Outlet /> | |||||
| ) : ( | |||||
| <Navigate to={LOGIN_PAGE} /> | |||||
| ); | |||||
| }; | |||||
| export default PrivateRoute; |
| import React, { ReactNode } from 'react'; | |||||
| interface SectionProps { | |||||
| children: ReactNode; | |||||
| className?: string; | |||||
| } | |||||
| const Section: React.FC<SectionProps> = ({ children, className }) => ( | |||||
| <section className={`l-section ${className || ''}`}>{children}</section> | |||||
| ); | |||||
| export default Section; |
| import { createContext, FC, ReactNode } from "react"; | |||||
| import { ThemeProvider } from "@mui/material/styles"; | |||||
| import useToggleColorMode from "../hooks/useToggleColorMode"; | |||||
| export const ColorModeContext = createContext({ | |||||
| toggleColorMode: () => {}, | |||||
| }); | |||||
| const ColorModeProvider = ({ children }) => { | |||||
| const [toggleColorMode, theme] = useToggleColorMode(); | |||||
| return ( | |||||
| <ColorModeContext.Provider value={toggleColorMode}> | |||||
| <ThemeProvider theme={theme}> {children} </ThemeProvider> | |||||
| </ColorModeContext.Provider> | |||||
| ); | |||||
| }; | |||||
| export default ColorModeProvider; |
| import React, { createContext } from 'react'; | |||||
| import { ThemeProvider } from '@mui/material/styles'; | |||||
| import useToggleColorMode from '../hooks/useToggleColorMode'; | |||||
| /* @ts-ignore */ | |||||
| export const ColorModeContext = createContext(); | |||||
| interface Props { | |||||
| children: React.ReactNode; | |||||
| } | |||||
| const ColorModeProvider: React.FC<Props> = ({ children }) => { | |||||
| const [toggleColorMode, theme] = useToggleColorMode(); | |||||
| return ( | |||||
| <ColorModeContext.Provider value={toggleColorMode}> | |||||
| {/* @ts-ignore */} | |||||
| <ThemeProvider theme={theme}>{children}</ThemeProvider> | |||||
| </ColorModeContext.Provider> | |||||
| ); | |||||
| }; | |||||
| export default ColorModeProvider; |
| 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 { faker } from '@faker-js/faker' | import { faker } from '@faker-js/faker' | ||||
| interface FakeData { | |||||
| export interface FakeData { | |||||
| id: number; | id: number; | ||||
| name: string; | name: string; | ||||
| color: string; | color: string; | ||||
| items.push({ | items.push({ | ||||
| id: id, | id: id, | ||||
| name: `${faker.commerce.productAdjective()} ${faker.commerce.productMaterial()} ${faker.commerce.product()}`, | name: `${faker.commerce.productAdjective()} ${faker.commerce.productMaterial()} ${faker.commerce.product()}`, | ||||
| color: faker.commerce.color(), | |||||
| color: faker.color.human(), | |||||
| price: `$${faker.commerce.price()}`, | price: `$${faker.commerce.price()}`, | ||||
| company: faker.company.companyName(), | |||||
| company: faker.company.name(), | |||||
| }); | }); | ||||
| } | } | ||||
| return { items }; | |||||
| return {items} | |||||
| }; | }; |
| import { AxiosResponse } from 'axios'; | |||||
| import { useState, useCallback, useEffect } from 'react'; | |||||
| import { unstable_batchedUpdates } from 'react-dom'; | |||||
| import {FakeData} from '../db/db'; | |||||
| type ApiCall = (page: number, itemsPerPage: number, sortColumn: string, sortDirection: string, filter: string) => AxiosResponse; | |||||
| const usePagingHook = (page: number, itemsPerPage: number, sort: string, filter: string, apiCallback: ApiCall) => { | |||||
| const [items, setItems] = useState<FakeData[]>([]); | |||||
| 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 | |||||
| ); | |||||
| console.log('response',response); | |||||
| if (response.status === 200) { | |||||
| // Prevents multiple rerenders | |||||
| unstable_batchedUpdates(() => { | |||||
| setItems(response.data); | |||||
| setTotalCount(parseInt(response.data.length)); | |||||
| setTotalPages( | |||||
| Math.ceil(response.data.length / 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 { PaletteMode } from '@mui/material'; | import { PaletteMode } from '@mui/material'; | ||||
| const useToggleColorMode = () => { | const useToggleColorMode = () => { | ||||
| const currentColorMode = authScopeStringGetHelper('colorMode') || 'light'; | |||||
| const [mode, setMode] = useState<PaletteMode>(currentColorMode); | |||||
| const currentColorMode = authScopeStringGetHelper('colorMode') || 'light'; | |||||
| const [mode, setMode] = useState(currentColorMode); | |||||
| const toggleColorMode = () => { | const toggleColorMode = () => { | ||||
| const nextMode = mode === 'light' ? 'dark' : 'light'; | const nextMode = mode === 'light' ? 'dark' : 'light'; | ||||
| () => | () => | ||||
| createTheme({ | createTheme({ | ||||
| palette: { | palette: { | ||||
| mode | |||||
| mode: mode as PaletteMode | |||||
| } | } | ||||
| }), | }), | ||||
| [mode] | [mode] |
| goBack: 'Go back to homepage', | goBack: 'Go back to homepage', | ||||
| logout: 'Logout', | logout: 'Logout', | ||||
| }, | }, | ||||
| apiErrors: { | |||||
| ClientIpAddressIsNullOrEmpty: "Client Ip address is null or empty", | |||||
| UsernameDoesNotExist: "Username does not exist" | |||||
| } | |||||
| apiErrors:{ | |||||
| ClientIpAddressIsNullOrEmpty:"Client Ip address is null or empty", | |||||
| UsernameDoesNotExist: "Username does not exist", | |||||
| WrongCredentials: "Wrong credentials", | |||||
| SomethingWentWrong: "Something went wrong", | |||||
| WrongPasswordAccountIsLocked: "Wrong credentials, account is locked", | |||||
| AccountIsLocked: "Account is locked" | |||||
| } | |||||
| }; | }; |
| import { HelmetProvider } from 'react-helmet-async'; | import { HelmetProvider } from 'react-helmet-async'; | ||||
| import './i18n'; | import './i18n'; | ||||
| import ColorModeProvider from './context/ColorModeContext'; | import ColorModeProvider from './context/ColorModeContext'; | ||||
| import { Provider } from 'react-redux'; | |||||
| import {store} from './store'; | |||||
| const root = ReactDOM.createRoot( | const root = ReactDOM.createRoot( | ||||
| document.getElementById('root') as HTMLElement | document.getElementById('root') as HTMLElement | ||||
| root.render( | root.render( | ||||
| <HelmetProvider> | <HelmetProvider> | ||||
| <React.StrictMode> | <React.StrictMode> | ||||
| <Provider store={store}> | |||||
| <ColorModeProvider> | <ColorModeProvider> | ||||
| <App /> | <App /> | ||||
| </ColorModeProvider> | </ColorModeProvider> | ||||
| </Provider> | |||||
| </React.StrictMode> | </React.StrictMode> | ||||
| </HelmetProvider> | </HelmetProvider> | ||||
| ); | ); |
| import { useTranslation } from 'react-i18next'; | |||||
| const ErrorPage = () => { | |||||
| const { t } = useTranslation(); | |||||
| return ( | |||||
| <div className="c-error-page"> | |||||
| <div className="c-error-page__content"> | |||||
| <h1 className="c-error-page__title">500</h1> | |||||
| <p className="c-error-page__text">{t('errorPage.text')}</p> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default ErrorPage; |
| import { useTranslation } from 'react-i18next'; | |||||
| import Button from '../../components/Button/Button'; | |||||
| import Section from '../../components/Section/Section'; | |||||
| import { useNavigate } from "react-router-dom"; | |||||
| import { HOME_PAGE } from '../../constants/pages'; | |||||
| const NotFoundPage = () => { | |||||
| const { t } = useTranslation(); | |||||
| const navigate = useNavigate(); | |||||
| return ( | |||||
| <div className="c-error-page"> | |||||
| <Section className="c-error-page__content-container"> | |||||
| <div className="c-error-page__content"> | |||||
| <h1 className="c-error-page__title">404</h1> | |||||
| <p className="c-error-page__text">{t('notFound.text')}</p> | |||||
| <Button | |||||
| className="c-error-page__button" | |||||
| variant="primary-outlined" | |||||
| onClick={() => navigate(HOME_PAGE)} | |||||
| > | |||||
| {t('notFound.goBack')} | |||||
| </Button> | |||||
| </div> | |||||
| </Section> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default NotFoundPage; |
| import React from 'react'; | |||||
| import { Formik, Form, Field } from 'formik'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import * as Yup from 'yup'; | |||||
| import i18next from 'i18next'; | |||||
| import Auth from '../../components/Auth/Auth'; | |||||
| import AuthCard from '../../components/AuthCards/AuthCard'; | |||||
| import Section from '../../components/Section/Section'; | |||||
| import Button from '../../components/Button/Button'; | |||||
| import TextField from '../../components/InputFields/TextField'; | |||||
| const forgotPasswordValidationSchema = Yup.object().shape({ | |||||
| email: Yup.string().required('Email is a required field!'), | |||||
| }); | |||||
| const ForgotPasswordPage = () => { | |||||
| const { t } = useTranslation(); | |||||
| const handleSubmit = (values: any) => { | |||||
| console.log("Values",values) | |||||
| }; | |||||
| return ( | |||||
| <Auth> | |||||
| <AuthCard | |||||
| title={t('forgotPassword.title')} | |||||
| > | |||||
| <Section> | |||||
| <div className="c-reset-security"> | |||||
| <div className="c-reset-security__form"> | |||||
| <Formik | |||||
| onSubmit={handleSubmit} | |||||
| initialValues={{ email: '' }} | |||||
| validationSchema={forgotPasswordValidationSchema} | |||||
| > | |||||
| <Form> | |||||
| <Field | |||||
| label={t('login.forgotPasswordEmail')} | |||||
| name="email" | |||||
| component={TextField} | |||||
| /> | |||||
| <Button | |||||
| className="c-reset-security__button" | |||||
| authButton | |||||
| variant="primary" | |||||
| type="submit" | |||||
| > | |||||
| {t('forgotPassword.label')} | |||||
| </Button> | |||||
| </Form> | |||||
| </Formik> | |||||
| </div> | |||||
| </div> | |||||
| </Section> | |||||
| </AuthCard> | |||||
| </Auth> | |||||
| ); | |||||
| }; | |||||
| export default ForgotPasswordPage; |
| import React from 'react'; | |||||
| const HomePage = () => { | |||||
| return ( | |||||
| <div className="c-error-page"> | |||||
| <div className="c-error-page__content"> | |||||
| <h1 className="c-error-page__title">Home page</h1> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default HomePage; |
| import React from 'react'; | |||||
| import { Box, Grid } from '@mui/material'; | |||||
| import Modals from '../../components/MUI/Examples/ModalsExample'; | |||||
| import DataGrid from '../../components/MUI/Examples/DataGridExample'; | |||||
| import PagingSortingFiltering from '../../components/MUI/Examples/PagingSortingFilteringExample'; | |||||
| import PagingSortingFilteringServerSide from '../../components/MUI/Examples/PagingSortingFilteringExampleServerSide'; | |||||
| import RandomDataProvider from '../../context/RandomDataContext'; | |||||
| import NavbarComponent from '../../components/MUI/NavbarComponent'; | |||||
| const HomePage = () => { | |||||
| return ( | |||||
| <> | |||||
| <NavbarComponent /> | |||||
| <Box sx={{ mt: 4, mx: 4 }}> | |||||
| <Grid container spacing={2} justifyContent="center"> | |||||
| <Grid item xs={12} md={3}> | |||||
| <Modals /> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={6}> | |||||
| <DataGrid /> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={9}> | |||||
| <PagingSortingFiltering /> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={9}> | |||||
| {/* Move to higher components? */} | |||||
| <RandomDataProvider> | |||||
| <PagingSortingFilteringServerSide /> | |||||
| </RandomDataProvider> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default HomePage; |
| import React, { useState } from "react"; | |||||
| import { useFormik } from "formik"; | |||||
| import { useDispatch, useSelector } from "react-redux"; | |||||
| import { NavLink } from "react-router-dom"; | |||||
| import * as Yup from "yup"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { FORGOT_PASSWORD_PAGE, HOME_PAGE } from "../../constants/pages"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Container, | |||||
| Grid, | |||||
| IconButton, | |||||
| InputAdornment, | |||||
| Link, | |||||
| TextField, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { Visibility, VisibilityOff } from "@mui/icons-material"; | |||||
| import Backdrop from "../../components/MUI/BackdropComponent"; | |||||
| import ErrorMessage from "../../components/MUI/ErrorMessageComponent"; | |||||
| import { clearLoginErrors, fetchUserData } from "../../store/features/login/loginSlice"; | |||||
| import {useNavigate} from 'react-router-dom'; | |||||
| import { selectLoginError, selectLoginLoading } from "../../store/selectors/loginSelectors"; | |||||
| const LoginPage = () => { | |||||
| const dispatch = useDispatch(); | |||||
| const { t } = useTranslation(); | |||||
| const error = useSelector(selectLoginError); | |||||
| const history = useNavigate(); | |||||
| const [showPassword, setShowPassword] = useState(false); | |||||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||||
| // When user refreshes page | |||||
| // useEffect(() => { | |||||
| // function redirectClient() { | |||||
| // if (!tokens.RefreshToken && !tokens.JwtToken) { | |||||
| // return; | |||||
| // } | |||||
| // } | |||||
| // redirectClient(); | |||||
| // }, [history, tokens]); | |||||
| const isLoading = useSelector(selectLoginLoading); | |||||
| const handleApiResponseSuccess = () => { | |||||
| history(HOME_PAGE); | |||||
| }; | |||||
| type SubmitValues = { | |||||
| username: string; | |||||
| password: string; | |||||
| } | |||||
| const handleSubmit = (values: SubmitValues) => { | |||||
| const { username: Username, password: Password } = values; | |||||
| dispatch(clearLoginErrors()); | |||||
| dispatch(fetchUserData({ Username, Password, handleApiResponseSuccess })); | |||||
| }; | |||||
| const formik = useFormik({ | |||||
| initialValues: { | |||||
| username: "", | |||||
| password: "", | |||||
| }, | |||||
| validationSchema: Yup.object().shape({ | |||||
| username: Yup.string().required('Username is required'), | |||||
| password: Yup.string().required('Passwor is required'), | |||||
| }), | |||||
| onSubmit: handleSubmit, | |||||
| validateOnBlur: true, | |||||
| enableReinitialize: true, | |||||
| }); | |||||
| return ( | |||||
| <Container component="main" maxWidth="md"> | |||||
| <Box | |||||
| sx={{ | |||||
| marginTop: 32, | |||||
| display: "flex", | |||||
| flexDirection: "column", | |||||
| alignItems: "center", | |||||
| }} | |||||
| > | |||||
| <Typography component="h1" variant="h5"> | |||||
| {t("login.logInTitle")} | |||||
| </Typography> | |||||
| {error && <ErrorMessage error={error} />} | |||||
| <Box | |||||
| component="form" | |||||
| onSubmit={formik.handleSubmit} | |||||
| sx={{ position: "relative", mt: 1, p: 1 }} | |||||
| > | |||||
| <Backdrop position="absolute" isLoading={isLoading} /> | |||||
| <TextField | |||||
| name="username" | |||||
| label={t("common.labelUsername")} | |||||
| margin="normal" | |||||
| value={formik.values.username} | |||||
| onChange={formik.handleChange} | |||||
| error={formik.touched.username && Boolean(formik.errors.username)} | |||||
| helperText={formik.touched.username && formik.errors.username} | |||||
| autoFocus | |||||
| fullWidth | |||||
| /> | |||||
| <TextField | |||||
| name="password" | |||||
| label={t("common.labelPassword")} | |||||
| margin="normal" | |||||
| type={showPassword ? "text" : "password"} | |||||
| value={formik.values.password} | |||||
| onChange={formik.handleChange} | |||||
| error={formik.touched.password && Boolean(formik.errors.password)} | |||||
| helperText={formik.touched.password && formik.errors.password} | |||||
| fullWidth | |||||
| InputProps={{ | |||||
| endAdornment: ( | |||||
| <InputAdornment position="end"> | |||||
| <IconButton | |||||
| onClick={handleClickShowPassword} | |||||
| onMouseDown={handleMouseDownPassword} | |||||
| > | |||||
| {showPassword ? <Visibility /> : <VisibilityOff />} | |||||
| </IconButton> | |||||
| </InputAdornment> | |||||
| ), | |||||
| }} | |||||
| /> | |||||
| <Button | |||||
| type="submit" | |||||
| variant="contained" | |||||
| sx={{ mt: 3, mb: 2 }} | |||||
| fullWidth | |||||
| > | |||||
| {t("login.logIn")} | |||||
| </Button> | |||||
| <Grid container> | |||||
| <Grid | |||||
| item | |||||
| xs={12} | |||||
| md={6} | |||||
| sx={{ textAlign: { xs: "center", md: "left" } }} | |||||
| > | |||||
| <Link | |||||
| to={FORGOT_PASSWORD_PAGE} | |||||
| component={NavLink} | |||||
| variant="body2" | |||||
| underline="hover" | |||||
| > | |||||
| {t("login.forgotYourPassword")} | |||||
| </Link> | |||||
| </Grid> | |||||
| <Grid | |||||
| item | |||||
| xs={12} | |||||
| md={6} | |||||
| sx={{ textAlign: { xs: "center", md: "right" } }} | |||||
| > | |||||
| <Link | |||||
| to="#" | |||||
| component={NavLink} | |||||
| variant="body2" | |||||
| underline="hover" | |||||
| > | |||||
| {t("login.dontHaveAccount")} | |||||
| </Link> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| </Box> | |||||
| </Container> | |||||
| ); | |||||
| }; | |||||
| export default LoginPage; |
| import axios from 'axios'; | import axios from 'axios'; | ||||
| import { FakeData } from '../db/db'; | |||||
| const JSON_SERVER_ENDPOINT = 'http://localhost:4000'; | const JSON_SERVER_ENDPOINT = 'http://localhost:4000'; | ||||
| }); | }); | ||||
| export const getRequest = (url: string, params = null, options = {}) => | export const getRequest = (url: string, params = null, options = {}) => | ||||
| request.get(url, { params, ...options }); | |||||
| request.get<FakeData[]>(url, { params, ...options }); |
| emailorusername, | emailorusername, | ||||
| }); | }); | ||||
| export const attemptLogin = (payload: string) => | |||||
| export const attemptLogin = (payload: {}) => | |||||
| postRequest(apiEndpoints.authentications.login, payload); | postRequest(apiEndpoints.authentications.login, payload); | ||||
| export const updateSecurityAnswer = (payload: any) => | export const updateSecurityAnswer = (payload: any) => |
| import { createSlice } from '@reduxjs/toolkit'; | |||||
| import type { PayloadAction } from '@reduxjs/toolkit'; | |||||
| export interface AppState { | |||||
| } | |||||
| const initialState: AppState = { | |||||
| } | |||||
| export const appSlice = createSlice({ | |||||
| name: 'app', | |||||
| initialState, | |||||
| reducers: { | |||||
| appLoading: () => {}, | |||||
| setAppReady: () => {} | |||||
| } | |||||
| }); | |||||
| export const { setAppReady, appLoading } = appSlice.actions; | |||||
| export default appSlice.reducer; |
| import { createSlice } from '@reduxjs/toolkit'; | |||||
| import type { PayloadAction } from '@reduxjs/toolkit'; | |||||
| import { boolean } from 'yup/lib/locale'; | |||||
| export interface LoginState { | |||||
| email: string; | |||||
| token: { | |||||
| RefreshToken: string; | |||||
| JwtToken: string; | |||||
| }; | |||||
| logging: boolean; | |||||
| errorMessage: string; | |||||
| } | |||||
| const initialState: LoginState = { | |||||
| email: '', | |||||
| token: { | |||||
| RefreshToken: '', | |||||
| JwtToken: '' | |||||
| }, | |||||
| logging: false, | |||||
| errorMessage: '' | |||||
| } | |||||
| interface SetUserPayload { | |||||
| JwtToken: string, | |||||
| RefreshToken: string; | |||||
| } | |||||
| export interface LoginPayload { | |||||
| Username: string; | |||||
| Password: string; | |||||
| handleApiResponseSuccess: () => void; | |||||
| } | |||||
| export const loginSlice = createSlice({ | |||||
| name: 'login', | |||||
| initialState, | |||||
| reducers: { | |||||
| fetchUserData: (state, action: PayloadAction<LoginPayload>) => { | |||||
| state.logging = true; | |||||
| }, | |||||
| setToken: (state, action: PayloadAction<SetUserPayload>) => { | |||||
| state.token = action.payload; | |||||
| state.logging = false; | |||||
| }, | |||||
| setUserJwtToken: (state, action: PayloadAction<string>) => { | |||||
| state.token.JwtToken = action.payload; | |||||
| state.logging = false; | |||||
| }, | |||||
| logoutUser: () => {}, | |||||
| authenticateUser: () => {}, | |||||
| setError: (state, action: PayloadAction<string>) => { | |||||
| state.errorMessage = action.payload; | |||||
| state.logging = false; | |||||
| }, | |||||
| generateToken: () => {}, | |||||
| refreshUserToken: () => {}, | |||||
| resetLoginState: () => { return initialState }, | |||||
| clearLoginErrors: (state) => { | |||||
| state.errorMessage = ''; | |||||
| }, | |||||
| } | |||||
| }); | |||||
| export const loginActions = loginSlice.actions; | |||||
| export const {fetchUserData, setToken,authenticateUser, generateToken, setUserJwtToken, refreshUserToken, logoutUser, setError, resetLoginState, clearLoginErrors } = loginSlice.actions; | |||||
| export default loginSlice.reducer; |
| import { createSlice } from '@reduxjs/toolkit'; | |||||
| import type { PayloadAction } from '@reduxjs/toolkit'; | |||||
| import generate from '../../../util/helpers/randomData'; | |||||
| export interface Item { | |||||
| name: string; | |||||
| color: string; | |||||
| size: string; | |||||
| price: string; | |||||
| designer: string; | |||||
| type: string; | |||||
| salesPrice: string; | |||||
| } | |||||
| export interface RandomDataState { | |||||
| items: Item[]; | |||||
| filteredItems: Item[]; | |||||
| count: number; | |||||
| page: number; | |||||
| itemsPerPage: number; | |||||
| filter: string, | |||||
| sort: string; | |||||
| } | |||||
| const initialState: RandomDataState = { | |||||
| items: [], | |||||
| filteredItems: [], | |||||
| count: 0, | |||||
| page: 0, | |||||
| itemsPerPage: 12, | |||||
| filter: '', | |||||
| sort: 'name-asc' | |||||
| } | |||||
| export const randomDataSlice = createSlice({ | |||||
| name: 'randomData', | |||||
| initialState, | |||||
| reducers: { | |||||
| loadRandomData: (state, action: PayloadAction<number>) => { | |||||
| const count = action.payload; | |||||
| const items = generate(count); | |||||
| state.items = items; | |||||
| state.filteredItems = items; | |||||
| state.count = items.length; | |||||
| }, | |||||
| updatePage: (state, action: PayloadAction<number>) => { | |||||
| const page = action.payload; | |||||
| state.page = page; | |||||
| }, | |||||
| updateItemsPerPage: (state, action: PayloadAction<number>) => { | |||||
| const itemsPerPage = action.payload; | |||||
| state.itemsPerPage = itemsPerPage; | |||||
| }, | |||||
| updateFilter: (state, action: PayloadAction<string>) => { | |||||
| const filter = action.payload; | |||||
| const filteredItems = filter | |||||
| ? state.items.filter((item) => item.name.toLowerCase().includes(filter.toLowerCase())) : state.items; | |||||
| state.filter = filter; | |||||
| state.filteredItems = filteredItems; | |||||
| state.count = filteredItems.length; | |||||
| }, | |||||
| updateSort: (state, action: PayloadAction<string>) => { | |||||
| const sort = action.payload; | |||||
| const [field, direction] = sort.split('-'); | |||||
| const sortDirection = direction === 'asc' ? 1 : -1; | |||||
| const dataItems = state.filteredItems.length ? state.filteredItems : state.items; | |||||
| const sorted = dataItems.sort((a: any, b: any) => { | |||||
| if (a[field] > b[field]) { | |||||
| return sortDirection; | |||||
| } | |||||
| if (b[field] > a[field]) { | |||||
| return sortDirection * -1; | |||||
| } | |||||
| return 0; | |||||
| }); | |||||
| const filteredItems = state.filteredItems.length ? sorted : state.filteredItems; | |||||
| state.sort = sort; | |||||
| state.filteredItems = filteredItems; | |||||
| }, | |||||
| } | |||||
| }); | |||||
| export const { loadRandomData, updatePage, updateItemsPerPage, updateFilter, updateSort } = randomDataSlice.actions; | |||||
| export default randomDataSlice.reducer; |
| import { createSlice } from '@reduxjs/toolkit'; | |||||
| import type { PayloadAction } from '@reduxjs/toolkit'; | |||||
| export interface User { | |||||
| UserUid: string; | |||||
| CustomerUid: string; | |||||
| Email: string; | |||||
| Username: string; | |||||
| StateType: string; | |||||
| SecurityQuestionType: string; | |||||
| FirstName: string; | |||||
| LastName: string; | |||||
| MiddleName: string; | |||||
| isRequired2FA: string; | |||||
| } | |||||
| export interface UserState { | |||||
| user: User | {}; | |||||
| errorMessage: string; | |||||
| } | |||||
| const initialState: UserState = { | |||||
| user: {}, | |||||
| errorMessage: '' | |||||
| } | |||||
| export const userSlice = createSlice({ | |||||
| name: 'user', | |||||
| initialState, | |||||
| reducers: { | |||||
| setUser: (state, action: PayloadAction<User>) => { | |||||
| state.user = action.payload; | |||||
| }, | |||||
| setUserError: (state, action: PayloadAction<string>) => { | |||||
| state.errorMessage = action.payload; | |||||
| } | |||||
| } | |||||
| }); | |||||
| export const { setUser, setUserError } = userSlice.actions; | |||||
| export default userSlice.reducer; |
| import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; | |||||
| import loginReducer from './features/login/loginSlice'; | |||||
| import randomDataReducer from './features/randomData/randomDataSlice'; | |||||
| import userReducer from './features/user/userSlice'; | |||||
| import createSagaMiddleware from 'redux-saga'; | |||||
| const saga = createSagaMiddleware(); | |||||
| import rootSaga from './saga'; | |||||
| import internalServerErrorMiddleware from './middleware/internalServerErrorMiddleware'; | |||||
| import requestStatusMiddleware from './middleware/requestStatusMiddleware'; | |||||
| export const store = configureStore({ | |||||
| reducer: { | |||||
| login: loginReducer, | |||||
| randomData: randomDataReducer, | |||||
| user: userReducer, | |||||
| }, | |||||
| middleware: [...getDefaultMiddleware({thunk: false}), saga, requestStatusMiddleware, internalServerErrorMiddleware] | |||||
| }) | |||||
| saga.run(rootSaga); | |||||
| // Infer the `RootState` and `AppDispatch` types from the store itself | |||||
| export type RootState = ReturnType<typeof store.getState> | |||||
| // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} | |||||
| export type AppDispatch = typeof store.dispatch |
| import { ERROR_PAGE } from '../../constants/pages'; | |||||
| import { attachPostRequestListener } from '../../request'; | |||||
| // import history from '../utils/history'; | |||||
| export default () => (next: any) => (action: any) => { | |||||
| attachPostRequestListener((error) => { | |||||
| if (!error.response) { | |||||
| return Promise.reject(error); | |||||
| } | |||||
| if (error.response.status === 500) { | |||||
| // return history.push(ERROR_PAGE); | |||||
| console.log('ERROR'); | |||||
| } | |||||
| return Promise.reject(error); | |||||
| }); | |||||
| next(action); | |||||
| }; |
| import { attachPostRequestListener } from '../../request'; | |||||
| import apiEndpoints from '../../request/apiEndpoints'; | |||||
| import { logoutUser } from '../features/login/loginSlice'; | |||||
| export default ({ dispatch }: {dispatch: any}) => (next: any) => (action: any) => { | |||||
| attachPostRequestListener((error) => { | |||||
| if (!error.response) { | |||||
| return Promise.reject(error); | |||||
| } | |||||
| if ( | |||||
| error.response.config.url !== apiEndpoints.authentications.login && | |||||
| error.response.config.url !== | |||||
| apiEndpoints.authentications.confirmSecurityQuestion && | |||||
| error.response.status === 401 | |||||
| ) { | |||||
| return dispatch(logoutUser()); | |||||
| } | |||||
| return Promise.reject(error); | |||||
| }); | |||||
| next(action); | |||||
| }; |
| import { all } from 'redux-saga/effects'; | |||||
| import loginSaga from './loginSaga'; | |||||
| export default function* rootSaga() { | |||||
| yield all([ | |||||
| loginSaga(), | |||||
| ]); | |||||
| } |
| import { all, call, put, takeLatest } from '@redux-saga/core/effects'; | |||||
| import { decodeToken } from 'react-jwt'; | |||||
| import { | |||||
| attemptLogin, generateTokenRequest, logoutUserRequest, refreshTokenRequest | |||||
| } from '../../request/loginRequest'; | |||||
| import { loginActions, LoginPayload, resetLoginState, setError, setToken, setUserJwtToken } from '../features/login/loginSlice'; | |||||
| import { | |||||
| authScopeClearHelper, | |||||
| authScopeRemoveHelper, | |||||
| authScopeSetHelper, authScopeStringGetHelper, | |||||
| } from '../../util/helpers/authScopeHelpers'; | |||||
| import { | |||||
| JWT_REFRESH_TOKEN, | |||||
| JWT_TOKEN, | |||||
| REFRESH_TOKEN_CONST, | |||||
| } from '../../constants/localStorage'; | |||||
| import { | |||||
| addHeaderToken, removeHeaderToken, | |||||
| } from '../../request'; | |||||
| import { setUser, User } from '../features/user/userSlice'; | |||||
| import { rejectErrorCodeHelper } from '../../util/helpers/rejectErrorCodeHelper'; | |||||
| //import { LOGIN_PAGE } from '../../constants/pages'; | |||||
| interface Payload { | |||||
| payload: LoginPayload; | |||||
| } | |||||
| function* fetchUser({ payload}: Payload) { | |||||
| try { | |||||
| const { data } = yield call(attemptLogin, payload); | |||||
| if (data.JwtToken) { | |||||
| const user = decodeToken(data.JwtToken); | |||||
| yield call(authScopeSetHelper, JWT_TOKEN, data.JwtToken); | |||||
| yield call(authScopeSetHelper, JWT_REFRESH_TOKEN, data.JwtRefreshToken); | |||||
| yield call(authScopeSetHelper, REFRESH_TOKEN_CONST, data.RefreshToken); | |||||
| yield call(addHeaderToken, data.JwtToken); | |||||
| yield put(setUser(user as User)); | |||||
| } | |||||
| yield put(setToken(data)); | |||||
| if (payload.handleApiResponseSuccess) { | |||||
| yield call(payload.handleApiResponseSuccess); | |||||
| } | |||||
| } catch (e: any) { | |||||
| if (e.response && e.response.data) { | |||||
| if (payload.handleApiResponseSuccess) { | |||||
| yield call(payload.handleApiResponseSuccess); | |||||
| } | |||||
| const errorMessage: string = yield call(rejectErrorCodeHelper, e); | |||||
| console.log('error message', errorMessage); | |||||
| yield put(setError(errorMessage)); | |||||
| } | |||||
| } | |||||
| } | |||||
| function* authenticateUser() { | |||||
| try { | |||||
| const JwtToken: string = yield call(authScopeStringGetHelper, JWT_TOKEN); | |||||
| if (!JwtToken) { | |||||
| //yield call(history, LOGIN_PAGE); | |||||
| console.log('No JWT Token'); | |||||
| } | |||||
| yield put(setUserJwtToken(JwtToken)); | |||||
| } catch (error: any) { | |||||
| const errorMessage: string = yield call(rejectErrorCodeHelper, error); | |||||
| yield put(setError(errorMessage)); | |||||
| yield call(authScopeRemoveHelper, JWT_TOKEN); | |||||
| yield call(authScopeRemoveHelper, JWT_REFRESH_TOKEN); | |||||
| yield call(authScopeRemoveHelper, REFRESH_TOKEN_CONST); | |||||
| } | |||||
| } | |||||
| function* logoutUser () { | |||||
| try { | |||||
| const JwtToken: string = yield call(authScopeStringGetHelper, JWT_TOKEN); | |||||
| const user = decodeToken(JwtToken) as User; | |||||
| if (user) { | |||||
| yield call(logoutUserRequest, user.UserUid); | |||||
| } | |||||
| } catch (error) { | |||||
| console.log(error); // eslint-disable-line | |||||
| } finally { | |||||
| yield call(authScopeClearHelper); | |||||
| yield call(removeHeaderToken); | |||||
| yield put(resetLoginState()); | |||||
| //yield call(history, LOGIN_PAGE); | |||||
| } | |||||
| } | |||||
| export function* refreshToken() { | |||||
| try { | |||||
| const JwtToken: string = yield call(authScopeStringGetHelper, JWT_TOKEN); | |||||
| const JwtRefreshToken: string = yield call( | |||||
| authScopeStringGetHelper, | |||||
| JWT_REFRESH_TOKEN, | |||||
| ); | |||||
| if (JwtToken && JwtRefreshToken) { | |||||
| const { data } = yield call(refreshTokenRequest, { | |||||
| JwtRefreshToken, | |||||
| JwtToken, | |||||
| }); | |||||
| yield call(authScopeSetHelper, JWT_TOKEN, data.JwtToken); | |||||
| yield call(authScopeSetHelper, JWT_REFRESH_TOKEN, data.JwtRefreshToken); | |||||
| const user = decodeToken(data.JwtToken); | |||||
| addHeaderToken(data.JwtToken); | |||||
| yield put(setUser(user as User)); | |||||
| yield put(setUserJwtToken(data.JwtToken)); | |||||
| } | |||||
| } catch (error) { | |||||
| yield call(logoutUser); | |||||
| console.log(error); // eslint-disable-line | |||||
| } | |||||
| } | |||||
| export function* generateToken({ payload } : any) { | |||||
| try { | |||||
| const { data } = yield call(generateTokenRequest, payload.data); | |||||
| const { JwtToken, JwtRefreshToken } = data; | |||||
| if (JwtToken && JwtRefreshToken) { | |||||
| yield call(authScopeSetHelper, JWT_TOKEN, data.JwtToken); | |||||
| yield call(authScopeSetHelper, JWT_REFRESH_TOKEN, data.JwtRefreshToken); | |||||
| const user = decodeToken(data.JwtToken); | |||||
| addHeaderToken(data.JwtToken); | |||||
| if (user) { | |||||
| yield put(setUser(user as User)); | |||||
| } | |||||
| yield put(setUserJwtToken(data.JwtToken)); | |||||
| if (payload.onSuccess) { | |||||
| yield call(payload.onSuccess); | |||||
| } | |||||
| } | |||||
| } catch (error) { | |||||
| yield call(logoutUser); | |||||
| console.log(error); // eslint-disable-line | |||||
| } | |||||
| } | |||||
| export default function* loginSaga() { | |||||
| yield all([ | |||||
| takeLatest(loginActions.fetchUserData, fetchUser), | |||||
| takeLatest(loginActions.authenticateUser, authenticateUser), | |||||
| takeLatest(loginActions.logoutUser, logoutUser), | |||||
| takeLatest(loginActions.refreshUserToken, refreshToken), | |||||
| takeLatest(loginActions.generateToken, generateToken) | |||||
| ]); | |||||
| } |
| import { createSelector } from 'reselect'; | |||||
| import { JWT_TOKEN } from '../../constants/localStorage'; | |||||
| import type { RootState } from '../../store/index'; | |||||
| import { authScopeStringGetHelper } from '../../util/helpers/authScopeHelpers'; | |||||
| const loginSelector = (state: RootState) => state.login; | |||||
| export const selectLoginLoading = createSelector(loginSelector, (state) => state.logging); | |||||
| export const selectLoginEmail = createSelector(loginSelector, (state) => state.email); | |||||
| export const selectTokens = createSelector(loginSelector, (state) => state.token); | |||||
| export const selectJWTToken = createSelector(loginSelector, (state) => state.token.JwtToken || authScopeStringGetHelper(JWT_TOKEN)); | |||||
| export const selectLoginError = createSelector(loginSelector, (state) => state.errorMessage); |
| import { createSelector } from 'reselect'; | |||||
| import type { RootState } from '../../store/index'; | |||||
| const randomDataSelector = (state: RootState) => state.randomData; | |||||
| export const itemsSelector = createSelector(randomDataSelector, (state) => | |||||
| (state.filter) ? state.filteredItems : state.items | |||||
| ); | |||||
| export const pageSelector = createSelector( | |||||
| randomDataSelector, | |||||
| (state) => state.page | |||||
| ); | |||||
| export const itemsPerPageSelector = createSelector( | |||||
| randomDataSelector, | |||||
| (state) => state.itemsPerPage | |||||
| ); | |||||
| export const countSelector = createSelector( | |||||
| randomDataSelector, | |||||
| (state) => state.count | |||||
| ); | |||||
| export const filterSelector = createSelector( | |||||
| randomDataSelector, | |||||
| (state) => state.filter | |||||
| ); | |||||
| export const sortSelector = createSelector( | |||||
| randomDataSelector, | |||||
| (state) => state.sort | |||||
| ); |
| import { createSelector } from 'reselect'; | |||||
| import isEmpty from 'lodash.isempty'; | |||||
| import { authScopeStringGetHelper } from '../../util/helpers/authScopeHelpers'; | |||||
| import { JWT_TOKEN } from '../../constants/localStorage'; | |||||
| import type { RootState } from '../../store/index'; | |||||
| export const userSelector = (state: RootState) => state.user; | |||||
| export const selectAuthUser = createSelector( | |||||
| userSelector, | |||||
| (state) => state.user, | |||||
| ); | |||||
| export const selectIsUserAuthenticated = createSelector( | |||||
| selectAuthUser, | |||||
| (state) => !isEmpty(state) || authScopeStringGetHelper(JWT_TOKEN), | |||||
| ); | |||||
| export const getForgotPasswordRequest = createSelector( | |||||
| userSelector, | |||||
| (state) => state.user, | |||||
| ); | |||||
| export const selectForgotPasswordError = createSelector( | |||||
| userSelector, | |||||
| (state) => state.errorMessage, | |||||
| ); | |||||
| export const getResetPasswordRequest = createSelector( | |||||
| userSelector, | |||||
| (state) => state.user, | |||||
| ); |
| import { createBrowserHistory } from 'history'; | |||||
| export default createBrowserHistory(); |
| import Numeral from 'numeral'; | |||||
| export function formatMoneyNumeral(value: number, formatString: string = '$0,0.00') { | |||||
| const isNegativeValue = value < 0; | |||||
| return `${isNegativeValue ? `(` : ''}${Numeral(value).format(formatString)}${isNegativeValue ? `)` : ''}` | |||||
| } |
| import i18next from 'i18next'; | import i18next from 'i18next'; | ||||
| import { AxiosError } from 'axios'; | import { AxiosError } from 'axios'; | ||||
| interface ErrorResponse { | |||||
| export interface ErrorResponse { | |||||
| Errors: { Code: string }[] | Errors: { Code: string }[] | ||||
| } | } | ||||
| "forceConsistentCasingInFileNames": true, | "forceConsistentCasingInFileNames": true, | ||||
| "noFallthroughCasesInSwitch": true, | "noFallthroughCasesInSwitch": true, | ||||
| "module": "esnext", | "module": "esnext", | ||||
| "moduleResolution": "node", | |||||
| "moduleResolution": "Node", | |||||
| "resolveJsonModule": true, | "resolveJsonModule": true, | ||||
| "isolatedModules": true, | "isolatedModules": true, | ||||
| "noEmit": true, | "noEmit": true, |
| module.exports = { | |||||
| module: { | |||||
| rules: [ | |||||
| { | |||||
| test: /\.s[ac]ss$/i, | |||||
| use: [ | |||||
| // Creates `style` nodes from JS strings | |||||
| "style-loader", | |||||
| // Translates CSS into CommonJS | |||||
| "css-loader", | |||||
| // Compiles Sass to CSS | |||||
| "sass-loader", | |||||
| ], | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| }; |