소스 검색

Added pages and redux toolkit

pull/1/head
Lazar Kostic 3 년 전
부모
커밋
656dc0b0a6
70개의 변경된 파일7535개의 추가작업 그리고 28042개의 파일을 삭제
  1. 1
    0
      .env
  2. 28
    0
      .eslintrc.json
  3. 0
    27899
      package-lock.json
  4. 51
    11
      package.json
  5. 16
    18
      src/App.tsx
  6. 35
    0
      src/AppRoutes.tsx
  7. 19
    0
      src/components/Auth/Auth.tsx
  8. 23
    0
      src/components/AuthCards/AuthCard.tsx
  9. 102
    0
      src/components/Button/Button.tsx
  10. 34
    0
      src/components/IconButton/IconButton.tsx
  11. 188
    0
      src/components/InputFields/BaseInputField.tsx
  12. 40
    0
      src/components/InputFields/Checkbox.tsx
  13. 121
    0
      src/components/InputFields/CurrencyField.tsx
  14. 32
    0
      src/components/InputFields/EmailField.tsx
  15. 72
    0
      src/components/InputFields/NumberField.tsx
  16. 84
    0
      src/components/InputFields/PasswordField.tsx
  17. 125
    0
      src/components/InputFields/PasswordStrength.js
  18. 45
    0
      src/components/InputFields/PercentageField.js
  19. 82
    0
      src/components/InputFields/TextField.tsx
  20. 24
    0
      src/components/Loader/BlockSectionLoader.tsx
  21. 9
    0
      src/components/Loader/FullPageLoader.tsx
  22. 19
    0
      src/components/Loader/SectionLoader.tsx
  23. 25
    0
      src/components/MUI/BackdropComponent.tsx
  24. 56
    0
      src/components/MUI/DialogComponent.tsx
  25. 27
    0
      src/components/MUI/DrawerComponent.tsx
  26. 14
    0
      src/components/MUI/ErrorMessageComponent.tsx
  27. 29
    0
      src/components/MUI/Examples/DataGridExample.tsx
  28. 64
    0
      src/components/MUI/Examples/ModalsExample.tsx
  29. 181
    0
      src/components/MUI/Examples/PagingSortingFilteringExample.tsx
  30. 161
    0
      src/components/MUI/Examples/PagingSortingFilteringExampleServerSide.tsx
  31. 26
    0
      src/components/MUI/MenuListComponent.tsx
  32. 162
    0
      src/components/MUI/NavbarComponent.tsx
  33. 34
    0
      src/components/MUI/PopoverComponent.tsx
  34. 25
    0
      src/components/Router/PrivateRoute.tsx
  35. 12
    0
      src/components/Section/Section.tsx
  36. 0
    18
      src/context/ColorModeContext.js
  37. 22
    0
      src/context/ColorModeContext.tsx
  38. 63
    0
      src/context/RandomDataContext.js
  39. 3505
    0
      src/db/db.json
  40. 4
    4
      src/db/db.ts
  41. 72
    0
      src/hooks/usePagingHook.ts
  42. 3
    3
      src/hooks/useToggleColorMode.ts
  43. 8
    4
      src/i18n/resources/en.ts
  44. 4
    0
      src/index.tsx
  45. 16
    0
      src/pages/ErrorPages/ErrorPage.tsx
  46. 31
    0
      src/pages/ErrorPages/NotFoundPage.tsx
  47. 63
    0
      src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
  48. 13
    0
      src/pages/HomePage/HomePage.tsx
  49. 37
    0
      src/pages/HomePage/HomePageMUI.tsx
  50. 178
    0
      src/pages/LoginPage/LoginPageMUI.tsx
  51. 2
    1
      src/request/jsonServerRequest.ts
  52. 1
    1
      src/request/loginRequest.ts
  53. 22
    0
      src/store/features/app/appSlice.ts
  54. 70
    0
      src/store/features/login/loginSlice.ts
  55. 92
    0
      src/store/features/randomData/randomDataSlice.ts
  56. 42
    0
      src/store/features/user/userSlice.ts
  57. 26
    0
      src/store/index.ts
  58. 18
    0
      src/store/middleware/internalServerErrorMiddleware.ts
  59. 22
    0
      src/store/middleware/requestStatusMiddleware.ts
  60. 9
    0
      src/store/saga/index.ts
  61. 152
    0
      src/store/saga/loginSaga.ts
  62. 16
    0
      src/store/selectors/loginSelectors.ts
  63. 33
    0
      src/store/selectors/randomDataSelectors.ts
  64. 32
    0
      src/store/selectors/userSelectors.ts
  65. 3
    0
      src/store/utils/history.ts
  66. 6
    0
      src/util/helpers/numeralHelpers.ts
  67. 1
    1
      src/util/helpers/rejectErrorCodeHelper.ts
  68. 1
    1
      tsconfig.json
  69. 17
    0
      webpack.config.js
  70. 985
    81
      yarn.lock

+ 1
- 0
.env 파일 보기

@@ -0,0 +1 @@
REACT_APP_BASE_API_URL=https://portalgatewayapi.bullioninternational.info/

+ 28
- 0
.eslintrc.json 파일 보기

@@ -0,0 +1,28 @@
{
"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"]
}
}

+ 0
- 27899
package-lock.json
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


+ 51
- 11
package.json 파일 보기

@@ -6,38 +6,56 @@
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@faker-js/faker": "^7.6.0",
"@mui/icons-material": "^5.10.9",
"@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/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"@types/jsonwebtoken": "^8.5.9",
"@types/node": "^16.7.13",
"@types/react": "^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",
"date-fns": "^2.29.3",
"formik": "^2.2.9",
"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",
"react": "^18.2.0",
"react-currency-input-field": "^3.6.9",
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.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": {
"start": "react-scripts start",
"build": "react-scripts build",
"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": {
"production": [
@@ -50,5 +68,27 @@
"last 1 firefox 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"
}
}

+ 16
- 18
src/App.tsx 파일 보기

@@ -1,25 +1,23 @@
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() {
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>
</>
);
}


+ 35
- 0
src/AppRoutes.tsx 파일 보기

@@ -0,0 +1,35 @@
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;

+ 19
- 0
src/components/Auth/Auth.tsx 파일 보기

@@ -0,0 +1,19 @@
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;

+ 23
- 0
src/components/AuthCards/AuthCard.tsx 파일 보기

@@ -0,0 +1,23 @@
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;

+ 102
- 0
src/components/Button/Button.tsx 파일 보기

@@ -0,0 +1,102 @@
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;

+ 34
- 0
src/components/IconButton/IconButton.tsx 파일 보기

@@ -0,0 +1,34 @@
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;

+ 188
- 0
src/components/InputFields/BaseInputField.tsx 파일 보기

@@ -0,0 +1,188 @@
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;

+ 40
- 0
src/components/InputFields/Checkbox.tsx 파일 보기

@@ -0,0 +1,40 @@
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;

+ 121
- 0
src/components/InputFields/CurrencyField.tsx 파일 보기

@@ -0,0 +1,121 @@
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;

+ 32
- 0
src/components/InputFields/EmailField.tsx 파일 보기

@@ -0,0 +1,32 @@
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;

+ 72
- 0
src/components/InputFields/NumberField.tsx 파일 보기

@@ -0,0 +1,72 @@
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;

+ 84
- 0
src/components/InputFields/PasswordField.tsx 파일 보기

@@ -0,0 +1,84 @@
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;

+ 125
- 0
src/components/InputFields/PasswordStrength.js 파일 보기

@@ -0,0 +1,125 @@
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;

+ 45
- 0
src/components/InputFields/PercentageField.js 파일 보기

@@ -0,0 +1,45 @@
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;

+ 82
- 0
src/components/InputFields/TextField.tsx 파일 보기

@@ -0,0 +1,82 @@
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;

+ 24
- 0
src/components/Loader/BlockSectionLoader.tsx 파일 보기

@@ -0,0 +1,24 @@
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;

+ 9
- 0
src/components/Loader/FullPageLoader.tsx 파일 보기

@@ -0,0 +1,9 @@
const FullPageLoader: React.FC = () => {
return (
<div className="c-loader c-loader--page">
<div className="c-loader__icon" />
</div>
);
};

export default FullPageLoader;

+ 19
- 0
src/components/Loader/SectionLoader.tsx 파일 보기

@@ -0,0 +1,19 @@
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;

+ 25
- 0
src/components/MUI/BackdropComponent.tsx 파일 보기

@@ -0,0 +1,25 @@
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;

+ 56
- 0
src/components/MUI/DialogComponent.tsx 파일 보기

@@ -0,0 +1,56 @@
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;

+ 27
- 0
src/components/MUI/DrawerComponent.tsx 파일 보기

@@ -0,0 +1,27 @@
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;

+ 14
- 0
src/components/MUI/ErrorMessageComponent.tsx 파일 보기

@@ -0,0 +1,14 @@
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;

+ 29
- 0
src/components/MUI/Examples/DataGridExample.tsx 파일 보기

@@ -0,0 +1,29 @@
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;

+ 64
- 0
src/components/MUI/Examples/ModalsExample.tsx 파일 보기

@@ -0,0 +1,64 @@
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;

+ 181
- 0
src/components/MUI/Examples/PagingSortingFilteringExample.tsx 파일 보기

@@ -0,0 +1,181 @@
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;

+ 161
- 0
src/components/MUI/Examples/PagingSortingFilteringExampleServerSide.tsx 파일 보기

@@ -0,0 +1,161 @@
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;

+ 26
- 0
src/components/MUI/MenuListComponent.tsx 파일 보기

@@ -0,0 +1,26 @@
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;

+ 162
- 0
src/components/MUI/NavbarComponent.tsx 파일 보기

@@ -0,0 +1,162 @@
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;

+ 34
- 0
src/components/MUI/PopoverComponent.tsx 파일 보기

@@ -0,0 +1,34 @@
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;

+ 25
- 0
src/components/Router/PrivateRoute.tsx 파일 보기

@@ -0,0 +1,25 @@
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;

+ 12
- 0
src/components/Section/Section.tsx 파일 보기

@@ -0,0 +1,12 @@
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;

+ 0
- 18
src/context/ColorModeContext.js 파일 보기

@@ -1,18 +0,0 @@
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;

+ 22
- 0
src/context/ColorModeContext.tsx 파일 보기

@@ -0,0 +1,22 @@
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;

+ 63
- 0
src/context/RandomDataContext.js 파일 보기

@@ -0,0 +1,63 @@
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;

+ 3505
- 0
src/db/db.json
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


db/db.ts → src/db/db.ts 파일 보기

@@ -1,6 +1,6 @@
import { faker } from '@faker-js/faker'

interface FakeData {
export interface FakeData {
id: number;
name: string;
color: string;
@@ -14,10 +14,10 @@ module.exports = () => {
items.push({
id: id,
name: `${faker.commerce.productAdjective()} ${faker.commerce.productMaterial()} ${faker.commerce.product()}`,
color: faker.commerce.color(),
color: faker.color.human(),
price: `$${faker.commerce.price()}`,
company: faker.company.companyName(),
company: faker.company.name(),
});
}
return { items };
return {items}
};

+ 72
- 0
src/hooks/usePagingHook.ts 파일 보기

@@ -0,0 +1,72 @@
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;

+ 3
- 3
src/hooks/useToggleColorMode.ts 파일 보기

@@ -7,8 +7,8 @@ import {
import { PaletteMode } from '@mui/material';

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 nextMode = mode === 'light' ? 'dark' : 'light';
@@ -20,7 +20,7 @@ const useToggleColorMode = () => {
() =>
createTheme({
palette: {
mode
mode: mode as PaletteMode
}
}),
[mode]

+ 8
- 4
src/i18n/resources/en.ts 파일 보기

@@ -86,8 +86,12 @@ export default {
goBack: 'Go back to homepage',
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"
}
};

+ 4
- 0
src/index.tsx 파일 보기

@@ -5,6 +5,8 @@ import App from './App';
import { HelmetProvider } from 'react-helmet-async';
import './i18n';
import ColorModeProvider from './context/ColorModeContext';
import { Provider } from 'react-redux';
import {store} from './store';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
@@ -12,9 +14,11 @@ const root = ReactDOM.createRoot(
root.render(
<HelmetProvider>
<React.StrictMode>
<Provider store={store}>
<ColorModeProvider>
<App />
</ColorModeProvider>
</Provider>
</React.StrictMode>
</HelmetProvider>
);

+ 16
- 0
src/pages/ErrorPages/ErrorPage.tsx 파일 보기

@@ -0,0 +1,16 @@
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;

+ 31
- 0
src/pages/ErrorPages/NotFoundPage.tsx 파일 보기

@@ -0,0 +1,31 @@
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;

+ 63
- 0
src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx 파일 보기

@@ -0,0 +1,63 @@
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;

+ 13
- 0
src/pages/HomePage/HomePage.tsx 파일 보기

@@ -0,0 +1,13 @@
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;

+ 37
- 0
src/pages/HomePage/HomePageMUI.tsx 파일 보기

@@ -0,0 +1,37 @@
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;

+ 178
- 0
src/pages/LoginPage/LoginPageMUI.tsx 파일 보기

@@ -0,0 +1,178 @@
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;

+ 2
- 1
src/request/jsonServerRequest.ts 파일 보기

@@ -1,4 +1,5 @@
import axios from 'axios';
import { FakeData } from '../db/db';

const JSON_SERVER_ENDPOINT = 'http://localhost:4000';

@@ -10,4 +11,4 @@ const request = axios.create({
});

export const getRequest = (url: string, params = null, options = {}) =>
request.get(url, { params, ...options });
request.get<FakeData[]>(url, { params, ...options });

+ 1
- 1
src/request/loginRequest.ts 파일 보기

@@ -6,7 +6,7 @@ export const getUsernames = (emailorusername: string) =>
emailorusername,
});

export const attemptLogin = (payload: string) =>
export const attemptLogin = (payload: {}) =>
postRequest(apiEndpoints.authentications.login, payload);

export const updateSecurityAnswer = (payload: any) =>

+ 22
- 0
src/store/features/app/appSlice.ts 파일 보기

@@ -0,0 +1,22 @@
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;

+ 70
- 0
src/store/features/login/loginSlice.ts 파일 보기

@@ -0,0 +1,70 @@
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;

+ 92
- 0
src/store/features/randomData/randomDataSlice.ts 파일 보기

@@ -0,0 +1,92 @@
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;

+ 42
- 0
src/store/features/user/userSlice.ts 파일 보기

@@ -0,0 +1,42 @@
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;

+ 26
- 0
src/store/index.ts 파일 보기

@@ -0,0 +1,26 @@
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

+ 18
- 0
src/store/middleware/internalServerErrorMiddleware.ts 파일 보기

@@ -0,0 +1,18 @@
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);
};

+ 22
- 0
src/store/middleware/requestStatusMiddleware.ts 파일 보기

@@ -0,0 +1,22 @@
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);
};

+ 9
- 0
src/store/saga/index.ts 파일 보기

@@ -0,0 +1,9 @@
import { all } from 'redux-saga/effects';

import loginSaga from './loginSaga';

export default function* rootSaga() {
yield all([
loginSaga(),
]);
}

+ 152
- 0
src/store/saga/loginSaga.ts 파일 보기

@@ -0,0 +1,152 @@
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)
]);
}

+ 16
- 0
src/store/selectors/loginSelectors.ts 파일 보기

@@ -0,0 +1,16 @@
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);

+ 33
- 0
src/store/selectors/randomDataSelectors.ts 파일 보기

@@ -0,0 +1,33 @@
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
);

+ 32
- 0
src/store/selectors/userSelectors.ts 파일 보기

@@ -0,0 +1,32 @@
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,
);

+ 3
- 0
src/store/utils/history.ts 파일 보기

@@ -0,0 +1,3 @@
import { createBrowserHistory } from 'history';

export default createBrowserHistory();

+ 6
- 0
src/util/helpers/numeralHelpers.ts 파일 보기

@@ -0,0 +1,6 @@
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 ? `)` : ''}`
}

+ 1
- 1
src/util/helpers/rejectErrorCodeHelper.ts 파일 보기

@@ -1,7 +1,7 @@
import i18next from 'i18next';
import { AxiosError } from 'axios';

interface ErrorResponse {
export interface ErrorResponse {
Errors: { Code: string }[]
}


+ 1
- 1
tsconfig.json 파일 보기

@@ -14,7 +14,7 @@
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,

+ 17
- 0
webpack.config.js 파일 보기

@@ -0,0 +1,17 @@
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",
],
},
],
},
};

+ 985
- 81
yarn.lock
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


Loading…
취소
저장