소스 검색

Init

main
Lazar Kostic 2 년 전
커밋
cf75fbeef3
75개의 변경된 파일35244개의 추가작업 그리고 0개의 파일을 삭제
  1. 31
    0
      .eslintrc
  2. 24
    0
      .eslintrc.json
  3. 25
    0
      .gitignore
  4. 36
    0
      Dockerfile
  5. 70
    0
      README.md
  6. 15
    0
      db/db.js
  7. 7
    0
      jsconfig.json
  8. 32455
    0
      package-lock.json
  9. 69
    0
      package.json
  10. BIN
      public/favicon.ico
  11. 43
    0
      public/index.html
  12. BIN
      public/logo192.png
  13. BIN
      public/logo512.png
  14. 25
    0
      public/manifest.json
  15. 3
    0
      public/robots.txt
  16. 38
    0
      src/App.css
  17. 53
    0
      src/App.js
  18. 8
    0
      src/App.test.js
  19. 22
    0
      src/AppRoutes.js
  20. 26
    0
      src/components/Backdrop/BackdropComponent.js
  21. 30
    0
      src/components/DataGrid/DataGridExample.js
  22. 57
    0
      src/components/Dialog/DialogComponent.js
  23. 28
    0
      src/components/Drawer/DrawerComponent.js
  24. 15
    0
      src/components/ErrorMessage/ErrorMessageComponent.js
  25. 44
    0
      src/components/MenuList/MenuListComponent.js
  26. 64
    0
      src/components/Modals/ModalsExample.js
  27. 171
    0
      src/components/Navbar/NavbarComponent.js
  28. 180
    0
      src/components/PagingSorting/PagingSortingFilteringExample.js
  29. 157
    0
      src/components/PagingSorting/PagingSortingFilteringExampleServerSide.js
  30. 35
    0
      src/components/Popover/PopoverComponent.js
  31. 17
    0
      src/components/RequireAuth/RequireAuth.js
  32. 0
    0
      src/components/Router/PrivateRoute.js
  33. 4
    0
      src/constants/localStorage.js
  34. 8
    0
      src/constants/pages.js
  35. 21
    0
      src/context/ColorModeContext.js
  36. 76
    0
      src/context/RandomDataContext.js
  37. 48
    0
      src/features/api/apiSlice.js
  38. 15
    0
      src/features/auth/authApiSlice.js
  39. 23
    0
      src/features/auth/authSlice.js
  40. 13
    0
      src/features/posts/postsApiSlice.js
  41. 13
    0
      src/features/provider/providerApiSlice.js
  42. 109
    0
      src/features/randomData/randomDataSlice.js
  43. 15
    0
      src/features/register/registerApiSlice.js
  44. 37
    0
      src/features/store.js
  45. 17
    0
      src/hooks/useDebounceHook.js
  46. 65
    0
      src/hooks/usePagingHook.js
  47. 44
    0
      src/hooks/useToggleColorMode.js
  48. 32
    0
      src/i18n/index.js
  49. 108
    0
      src/i18n/resources/en.js
  50. 108
    0
      src/i18n/resources/sr.js
  51. 13
    0
      src/index.css
  52. 38
    0
      src/index.js
  53. 4
    0
      src/initialValues/forgotPasswordInitialValues.js
  54. 4
    0
      src/initialValues/loginInitialValues.js
  55. 5
    0
      src/initialValues/registerInitialValues.js
  56. 1
    0
      src/logo.svg
  57. 40
    0
      src/pages/AuthCallbackPage/AuthCallbackPage.js
  58. 41
    0
      src/pages/HomePage/HomePage.js
  59. 180
    0
      src/pages/LoginPage/LoginPage.js
  60. 186
    0
      src/pages/RegisterPage/RegisterPage.js
  61. 13
    0
      src/reportWebVitals.js
  62. 5
    0
      src/setupTests.js
  63. 13
    0
      src/themes/index.js
  64. 7
    0
      src/themes/primaryTheme/primaryTheme.js
  65. 7
    0
      src/themes/primaryTheme/primaryThemeColors.js
  66. 0
    0
      src/themes/primaryTheme/primaryThemeFonts.js
  67. 19
    0
      src/util/authScopeHelpers.js
  68. 40
    0
      src/util/dateHelpers.js
  69. 1
    0
      src/util/enumMappers.js
  70. 65
    0
      src/util/randomData.js
  71. 11
    0
      src/util/stringHelpers.js
  72. 16
    0
      src/util/toastMessage.js
  73. 8
    0
      src/validations/forgotPasswordValidation.js
  74. 11
    0
      src/validations/loginValidation.js
  75. 12
    0
      src/validations/registerValidation.js

+ 31
- 0
.eslintrc 파일 보기

@@ -0,0 +1,31 @@
{
"extends": ["react-app", "airbnb", "prettier"],
"settings": {
"import/resolver": {
"node": {
"paths": ["src"]
}
}
},
"plugins": ["react", "react-hooks", "security"],
"rules": {
"react/jsx-filename-extension": "off",
"react/jsx-props-no-spreading": "off",
"react/button-has-type": "off",
"react/require-default-props": "off",
"import/no-extraneous-dependencies": "off",
"import/prefer-default-export": "off",
"consistent-return": "off",
"no-shadow": "off",
"no-use-before-define": "off",
"no-template-curly-in-string": "off",
"react-hooks/exhaustive-deps": "warn",
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
}

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

@@ -0,0 +1,24 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:react/recommended"],
"settings": {
"import/resolver": {
"node": {
"paths": ["src"]
}
}
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["react"],
"rules": {}
}

+ 25
- 0
.gitignore 파일 보기

@@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

.env

+ 36
- 0
Dockerfile 파일 보기

@@ -0,0 +1,36 @@
FROM node:16-alpine

WORKDIR /app

COPY package*.json ./
COPY src ./
COPY public ./

RUN yarn install

# Bundle app source
COPY . .

EXPOSE 3000
CMD ["yarn", "start"]

########################################################
################## BUILD VERSION #######################
# Use a Node 16 base image
#FROM node:16-alpine
# Set the working directory to /app inside the container
#WORKDIR /app
# Copy app files
#COPY . .
# ==== BUILD =====
# Install dependencies
#RUN yarn
# Build the app
#RUN yarn run build
# ==== RUN =======
# Set the env to "production"
#ENV NODE_ENV production
# Expose the port on which the app will be running (3000 is the default that `serve` uses)
#EXPOSE 3000
# Start the app
#CMD [ "npx", "serve", "build" ]

+ 70
- 0
README.md 파일 보기

@@ -0,0 +1,70 @@
# Getting Started with Create React App

This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

## Available Scripts

In the project directory, you can run:

### `npm start`

Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.

The page will reload when you make changes.\
You may also see any lint errors in the console.

### `npm test`

Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.

### `npm run build`

Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!

See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.

### `npm run eject`

**Note: this is a one-way operation. Once you `eject`, you can't go back!**

If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.

Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.

You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.

## Learn More

You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).

To learn React, check out the [React documentation](https://reactjs.org/).

### Code Splitting

This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)

### Analyzing the Bundle Size

This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)

### Making a Progressive Web App

This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)

### Advanced Configuration

This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)

### Deployment

This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)

### `npm run build` fails to minify

This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

+ 15
- 0
db/db.js 파일 보기

@@ -0,0 +1,15 @@
const faker = require('faker');

module.exports = () => {
const items = [];
for (let id = 1; id <= 500; id++) {
items.push({
id: id,
name: `${faker.commerce.productAdjective()} ${faker.commerce.productMaterial()} ${faker.commerce.product()}`,
color: faker.commerce.color(),
price: `$${faker.commerce.price()}`,
company: faker.company.companyName(),
});
}
return { items };
};

+ 7
- 0
jsconfig.json 파일 보기

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}

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


+ 69
- 0
package.json 파일 보기

@@ -0,0 +1,69 @@
{
"name": "template",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.11.16",
"@mui/material": "^5.12.1",
"@mui/x-data-grid": "^6.2.1",
"@reduxjs/toolkit": "^1.9.5",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"date-fns": "^2.29.3",
"faker": "^5.5.3",
"formik": "^2.2.9",
"i18next": "^22.4.15",
"json-server": "^0.17.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0",
"react-i18next": "^12.2.0",
"react-jwt": "^1.1.8",
"react-redux": "^8.0.5",
"react-router-dom": "^6.10.0",
"react-scripts": "5.0.1",
"react-toastify": "^9.1.2",
"redux-persist": "^6.0.0",
"web-vitals": "^2.1.4",
"yup": "^1.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"json-serve": "json-server ./db/db.js --port=4000"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"dotenv": "^16.0.3",
"eslint": "^8.38.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^2.8.7"
}
}

BIN
public/favicon.ico 파일 보기


+ 43
- 0
public/index.html 파일 보기

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.

Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png 파일 보기


BIN
public/logo512.png 파일 보기


+ 25
- 0
public/manifest.json 파일 보기

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

+ 3
- 0
public/robots.txt 파일 보기

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

+ 38
- 0
src/App.css 파일 보기

@@ -0,0 +1,38 @@
.App {
text-align: center;
}

.App-logo {
height: 40vmin;
pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}

.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}

.App-link {
color: #61dafb;
}

@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

+ 53
- 0
src/App.js 파일 보기

@@ -0,0 +1,53 @@
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Helmet } from "react-helmet-async";
import i18next from "i18next";
import AppRoutes from "./AppRoutes";
import { useTranslation } from "react-i18next";

import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

import { StyledEngineProvider } from "@mui/material";
import { authScopeStringGetHelper } from "util/authScopeHelpers";
import { LANGUAGE } from "constants/localStorage";
import { HOME_PAGE } from "constants/pages";
import { useSelector } from "react-redux";
import { selectCurrentToken } from "features/auth/authSlice";

const App = () => {
const { i18n } = useTranslation();
const navigate = useNavigate();

const auth = useSelector(selectCurrentToken);

useEffect(() => {
const lang = authScopeStringGetHelper(LANGUAGE);

if (lang) {
i18n.changeLanguage(lang);
}
}, []);

useEffect(() => {

if (auth !== null) {
navigate(HOME_PAGE, { replace: true });
}
}, []);

return (
<>
<Helmet>
<title>{i18next.t("app.title")}</title>
</Helmet>
<StyledEngineProvider injectFirst>
<ToastContainer bodyClassName="ToastBody" />
<AppRoutes />
</StyledEngineProvider>
</>
);
};

export default App;

+ 8
- 0
src/App.test.js 파일 보기

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

+ 22
- 0
src/AppRoutes.js 파일 보기

@@ -0,0 +1,22 @@
import React from "react";
import { Route, Routes } from "react-router-dom";

import LoginPage from "./pages/LoginPage/LoginPage";
import RegisterPage from "pages/RegisterPage/RegisterPage";
import HomePage from "pages/HomePage/HomePage";
import RequireAuth from "components/RequireAuth/RequireAuth";
import AuthCallback from "pages/AuthCallbackPage/AuthCallbackPage";

const AppRoutes = () => (
<Routes>
<Route path="/" element={<LoginPage />} />
<Route path="login" element={<LoginPage />} />
<Route exact path="register" element={<RegisterPage />} />
<Route path="/api/auth/:provider/callback" element={<AuthCallback />} />
<Route element={<RequireAuth />}>
<Route path="home" element={<HomePage />} />
</Route>
</Routes>
);

export default AppRoutes;

+ 26
- 0
src/components/Backdrop/BackdropComponent.js 파일 보기

@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Backdrop, CircularProgress } from '@mui/material';
import { alpha } from '@mui/system';

const BackdropComponent = ({ 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>
);

BackdropComponent.propTypes = {
position: PropTypes.oneOf(['fixed', 'absolute']),
isLoading: PropTypes.bool,
};

export default BackdropComponent;

+ 30
- 0
src/components/DataGrid/DataGridExample.js 파일 보기

@@ -0,0 +1,30 @@
import React from "react";
import { Paper, Typography } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid";
import { useTranslation } from "react-i18next";

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 = () => {
const { t } = useTranslation();
return (
<Paper sx={{ p: 2 }} elevation={5}>
<Typography variant="h4" gutterBottom align="center">
{t("common.dataGridExample")}
</Typography>
<DataGrid autoHeight rows={rows} columns={columns} />
</Paper>
);
};

export default DataGridExample;

+ 57
- 0
src/components/Dialog/DialogComponent.js 파일 보기

@@ -0,0 +1,57 @@
import React from "react";
import PropTypes from "prop-types";
import {
Dialog,
DialogContent,
DialogTitle,
DialogActions,
Button,
useMediaQuery,
useTheme,
} from "@mui/material";

const DialogComponent = ({
title,
content,
onClose,
open,
maxWidth,
fullWidth,
responsive,
}) => {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down("md"));

const handleClose = () => {
onClose();
};

return (
<Dialog
maxWidth={maxWidth}
fullWidth={fullWidth}
fullScreen={responsive && fullScreen}
onClose={handleClose}
open={open}
>
<DialogTitle>{title}</DialogTitle>
{content && <DialogContent>{content}</DialogContent>}
<DialogActions>
<Button onClick={handleClose}>OK</Button>
<Button onClick={handleClose}>Cancel</Button>
</DialogActions>
</Dialog>
);
};

DialogComponent.propTypes = {
title: PropTypes.string,
open: PropTypes.bool,
content: PropTypes.any,
onClose: PropTypes.func.isRequired,
maxWidth: PropTypes.oneOf(["xs", "sm", "md", "lg", "xl"]),
fullWidth: PropTypes.bool,
responsive: PropTypes.bool,
};

export default DialogComponent;

+ 28
- 0
src/components/Drawer/DrawerComponent.js 파일 보기

@@ -0,0 +1,28 @@
import React from "react";
import PropTypes from "prop-types";
import { Drawer } from "@mui/material";

const DrawerComponent = ({ open, toggleOpen, content, anchor = "right" }) => (
<Drawer
sx={{
minWidth: 250,
"& .MuiDrawer-paper": {
minWidth: 250,
},
}}
anchor={anchor}
open={open}
onClose={toggleOpen}
>
{content ? content : null}
</Drawer>
);

DrawerComponent.propTypes = {
open: PropTypes.bool,
toggleOpen: PropTypes.func,
content: PropTypes.any,
anchor: PropTypes.oneOf(["top", "right", "left", "bottom"]),
};

export default DrawerComponent;

+ 15
- 0
src/components/ErrorMessage/ErrorMessageComponent.js 파일 보기

@@ -0,0 +1,15 @@
import React from "react";
import PropTypes from "prop-types";
import { Typography } from "@mui/material";

const ErrorMessageComponent = ({ error }) => (
<Typography variant="body1" color="error" my={2}>
{error}
</Typography>
);

ErrorMessageComponent.propTypes = {
error: PropTypes.string.isRequired,
};

export default ErrorMessageComponent;

+ 44
- 0
src/components/MenuList/MenuListComponent.js 파일 보기

@@ -0,0 +1,44 @@
import React, { useState } from "react";
import { Button, Menu, MenuItem } from "@mui/material";
import { useTranslation } from "react-i18next";
import { authScopeSetHelper } from "util/authScopeHelpers";
import { LANGUAGE } from "constants/localStorage";

const MenuListComponent = () => {
const { t, i18n } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};

const handleLanguageChange = (language) => {
i18n.changeLanguage(language);
authScopeSetHelper(LANGUAGE, language)
setAnchorEl(null);
};

return (
<div>
<Button onClick={handleClick}>{t("common.language")}</Button>
<Menu
id="menu-list"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
<MenuItem onClick={() => handleLanguageChange("en")}>
{t("common.english")}
</MenuItem>
<MenuItem onClick={() => handleLanguageChange("sr")}>
{t("common.serbian")}
</MenuItem>
</Menu>
</div>
);
};

export default MenuListComponent;

+ 64
- 0
src/components/Modals/ModalsExample.js 파일 보기

@@ -0,0 +1,64 @@
import React, { useState } from "react";
import { Button, Divider, Paper, Typography } from "@mui/material";
import DialogComponent from "../Dialog/DialogComponent";
import DrawerComponent from "../Drawer/DrawerComponent";
import PopoverComponent from "../Popover/PopoverComponent";

const Modals = () => {
const [dialogOpen, setDialogOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const [popoverOpen, setPopoverOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);

return (
<Paper
sx={{
p: 2,
display: "flex",
flexDirection: "column",
}}
elevation={5}
>
<Typography variant="h4" gutterBottom align="center">
Modals Example
</Typography>
<Divider />
<Button onClick={() => setDialogOpen(true)}>Open Dialog</Button>
<Button onClick={() => setDrawerOpen(true)}>Open Drawer</Button>
<Button
onClick={(e) => {
setPopoverOpen(true);
setAnchorEl(e.currentTarget);
}}
>
Open Popover
</Button>
<DialogComponent
title="Dialog Title"
content={<Typography>Dialog Content</Typography>}
open={dialogOpen}
onClose={() => setDialogOpen(false)}
maxWidth="md"
fullWidth
responsive
/>
<DrawerComponent
anchor="left"
content={<Typography sx={{ p: 2 }}>Drawer Content</Typography>}
open={drawerOpen}
toggleOpen={() => setDrawerOpen(!drawerOpen)}
/>
<PopoverComponent
anchorEl={anchorEl}
open={popoverOpen}
onClose={() => {
setPopoverOpen(false);
setAnchorEl(null);
}}
content={<Typography sx={{ p: 2 }}>Popover Content</Typography>}
/>
</Paper>
);
};

export default Modals;

+ 171
- 0
src/components/Navbar/NavbarComponent.js 파일 보기

@@ -0,0 +1,171 @@
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 LogoutIcon from "@mui/icons-material/Logout";
import Brightness4Icon from "@mui/icons-material/Brightness4";
import Brightness7Icon from "@mui/icons-material/Brightness7";
import MenuList from "../MenuList/MenuListComponent";
import Drawer from "../Drawer/DrawerComponent";
import { ColorModeContext } from "context/ColorModeContext";
import { useDispatch } from "react-redux";
import { logOut } from "features/auth/authSlice";

const NavbarComponent = () => {
const dispatch = useDispatch();
const [openDrawer, setOpenDrawer] = useState(false);
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down("sm"));
const toggleColorMode = useContext(ColorModeContext);

const handleToggleDrawer = () => {
setOpenDrawer(!openDrawer);
};

const handleLogout = () => {
dispatch(logOut());
};

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>
<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>
<IconButton sx={{ ml: 1 }} onClick={toggleColorMode}>
{theme.palette.mode === "dark" ? (
<Brightness7Icon />
) : (
<Brightness4Icon />
)}
</IconButton>
<IconButton onClick={handleLogout}>
<LogoutIcon />
</IconButton>
</Box>
)}
</Box>
</Box>
</Toolbar>
</AppBar>
);
};

export default NavbarComponent;

+ 180
- 0
src/components/PagingSorting/PagingSortingFilteringExample.js 파일 보기

@@ -0,0 +1,180 @@
import React, { useEffect, useState } from "react";
import {
Paper,
Box,
Grid,
Typography,
Divider,
TablePagination,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
} from "@mui/material";
import { useDispatch, useSelector, batch } from "react-redux";
import useDebounce from "hooks/useDebounceHook";
import {
itemsSelector,
pageSelector,
itemsPerPageSelector,
countSelector,
sortSelector,
} from "features/randomData/randomDataSlice";
import {
loadRandomData,
updatePage,
updateItemsPerPage,
updateFilter,
updateSort,
} from "features/randomData/randomDataSlice";

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);
// 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) => {
const filterText = event.target.value;
setFilterText(filterText);
};

const handleSortChange = (event) => {
const sort = event.target.value;
dispatch(updateSort(sort));
};

const handlePageChange = (event, newPage) => {
dispatch(updatePage(newPage));
};

const handleItemsPerPageChange = (event) => {
const itemsPerPage = parseInt(event.target.value);
batch(() => {
dispatch(updateItemsPerPage(itemsPerPage));
dispatch(updatePage(0));
});
};

return (
<Paper
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "start",
py: 2,
minHeight: 500,
}}
elevation={5}
>
<Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center">
Pagination, Filtering and Sorting Example Client Side
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexWrap: "wrap",
mx: 2,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
width: "100%",
}}
>
{/* TODO Separate into SelectComponent */}
<FormControl sx={{ flexGrow: 1 }}>
<InputLabel id="sort-label">Sort</InputLabel>
<Select
label="Sort"
labelId="sort-label"
id="sort-select-helper"
value={sort}
onChange={handleSortChange}
>
<MenuItem value="name-asc">Name - A-Z</MenuItem>
<MenuItem value="name-desc">Name - Z-A</MenuItem>
<MenuItem value="price-asc">Price - Lowest to Highest</MenuItem>
<MenuItem value="price-desc">Price - Highest to Lowest</MenuItem>
</Select>
</FormControl>
<TextField
sx={{ flexGrow: 1 }}
variant="outlined"
label="Filter"
placeholder="Filter"
value={filterText}
onChange={handleFilterTextChange}
/>
</Box>
</Box>
<Grid container>
{items &&
items.length > 0 &&
items
.slice(
currentPage * itemsPerPage,
currentPage * itemsPerPage + itemsPerPage
)
.map((product, index) => (
// ! DON'T USE index for key, this is for example only
<Grid item sx={{ p: 2 }} xs={12} sm={6} md={4} lg={3} key={index}>
{/* TODO separate into component */}
<Paper sx={{ p: 3, height: "100%" }} elevation={3}>
<Typography sx={{ fontWeight: 600 }}>Name: </Typography>
<Typography display="inline"> {product.name}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Designer: </Typography>
<Typography display="inline"> {product.designer}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Type: </Typography>
<Typography display="inline"> {product.type}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Price: </Typography>
<Typography display="inline"> ${product.price}</Typography>
</Paper>
</Grid>
))}
</Grid>
<Box sx={{ width: "100%" }}>
<TablePagination
component="div"
count={totalCount}
page={currentPage}
onPageChange={handlePageChange}
rowsPerPage={itemsPerPage}
onRowsPerPageChange={handleItemsPerPageChange}
rowsPerPageOptions={[12, 24, 48, 96]}
labelRowsPerPage="Items per page"
showFirstButton
showLastButton
/>
</Box>
</Paper>
);
};

export default PagingSortingFilteringExample;

+ 157
- 0
src/components/PagingSorting/PagingSortingFilteringExampleServerSide.js 파일 보기

@@ -0,0 +1,157 @@
import React, { useEffect, useState } from "react";
import {
Paper,
Box,
Grid,
Typography,
Divider,
TablePagination,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
} from "@mui/material";
import Backdrop from "../Backdrop/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;

// Use debounce to prevent too many rerenders
const debouncedFilterText = useDebounce(filterText, 500);

useEffect(() => {
setFilter(filterText);
}, [debouncedFilterText]);

const handleFilterTextChange = (event) => {
const filterText = event.target.value;
setFilterText(filterText);
};

const handleSortChange = (event) => {
const sort = event.target.value;
setSort(sort);
};

const handlePageChange = (event, newPage) => {
setPage(newPage);
};

const handleItemsPerPageChange = (event) => {
const itemsPerPage = parseInt(event.target.value);
setItemsPerPage(itemsPerPage);
setPage(0);
};

return (
<Paper
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "start",
py: 2,
minHeight: 500,
position: "relative",
}}
elevation={5}
>
{loading && <Backdrop isLoading={loading} position="absolute" />}
<Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center">
Pagination, Filtering and Sorting Example Server Side
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexWrap: "wrap",
mx: 2,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
width: "100%",
}}
>
<FormControl sx={{ flexGrow: 1 }}>
<InputLabel id="sort-label">Sort</InputLabel>
<Select
label="Sort"
labelId="sort-label"
id="sort-select-helper"
value={sort || ""}
onChange={handleSortChange}
>
<MenuItem value="">None</MenuItem>
<MenuItem value="name-asc">Name - A-Z</MenuItem>
<MenuItem value="name-desc">Name - Z-A</MenuItem>
<MenuItem value="price-asc">Price - Lowest to Highest</MenuItem>
<MenuItem value="price-desc">Price - Highest to Lowest</MenuItem>
</Select>
</FormControl>
<TextField
sx={{ flexGrow: 1 }}
variant="outlined"
label="Filter"
placeholder="Filter"
value={filterText}
onChange={handleFilterTextChange}
/>
</Box>
<Grid container sx={{ position: "relative" }}>
{items &&
items.length > 0 &&
items.map((item) => (
<Grid
item
sx={{ p: 2 }}
xs={12}
sm={6}
md={4}
lg={3}
key={item.id}
>
{/* TODO separate into component */}
<Paper sx={{ p: 3, height: "100%" }} elevation={3}>
<Typography sx={{ fontWeight: 600 }}>Name: </Typography>
<Typography display="inline"> {item.name}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Company: </Typography>
<Typography display="inline"> {item.company}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Color: </Typography>
<Typography display="inline"> {item.color}</Typography>
<Divider />
<Typography sx={{ fontWeight: 600 }}>Price: </Typography>
<Typography display="inline"> {item.price}</Typography>
</Paper>
</Grid>
))}
</Grid>
<Box sx={{ width: "100%" }}>
<TablePagination
component="div"
count={totalCount}
page={currentPage}
onPageChange={handlePageChange}
rowsPerPage={itemsPerPage}
onRowsPerPageChange={handleItemsPerPageChange}
rowsPerPageOptions={[12, 24, 48, 96]}
labelRowsPerPage="Items per page"
showFirstButton
showLastButton
/>
</Box>
</Box>
</Paper>
);
};

export default PagingSortingFilteringExampleServerSide;

+ 35
- 0
src/components/Popover/PopoverComponent.js 파일 보기

@@ -0,0 +1,35 @@
import React from "react";
import PropTypes from "prop-types";
import { Box, Popover } from "@mui/material";

const PopoverComponent = ({ open, anchorEl, onClose, content }) => {
const handleClose = () => {
onClose();
};

return (
<Box component="div">
<Popover
sx={{ p: 5 }}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
>
{content}
</Popover>
</Box>
);
};

PopoverComponent.propTypes = {
anchorEl: PropTypes.object,
open: PropTypes.bool,
onClose: PropTypes.func.isRequired,
content: PropTypes.any,
};

export default PopoverComponent;

+ 17
- 0
src/components/RequireAuth/RequireAuth.js 파일 보기

@@ -0,0 +1,17 @@
import React from "react";
import { useLocation, Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectCurrentToken } from "features/auth/authSlice";

const RequireAuth = () => {
const token = useSelector(selectCurrentToken);
const location = useLocation();

return token ? (
<Outlet />
) : (
<Navigate to={"/login"} state={{ from: location }} replace />
);
};

export default RequireAuth;

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


+ 4
- 0
src/constants/localStorage.js 파일 보기

@@ -0,0 +1,4 @@
export const JWT_TOKEN = "JwtToken";
export const JWT_REFRESH_TOKEN = "JwtRefreshToken";
export const REFRESH_TOKEN_CONST = "RefreshToken";
export const LANGUAGE = "Language";

+ 8
- 0
src/constants/pages.js 파일 보기

@@ -0,0 +1,8 @@
export const BASE_PAGE = '/';
export const LOGIN_PAGE = '/login';
export const REGISTER_PAGE = '/register';
export const FORGOT_PASSWORD_PAGE = '/forgot-password';
export const HOME_PAGE = '/home';
export const ERROR_PAGE = '/error-page';
export const NOT_FOUND_PAGE = '/not-found';
export const AUTH_CALLBACK_PAGE = '/api/auth/:provider/callback'

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

@@ -0,0 +1,21 @@
import React, { createContext } from "react";
import PropTypes from "prop-types";
import { ThemeProvider } from "@mui/material/styles";
import useToggleColorMode from "hooks/useToggleColorMode";

export const ColorModeContext = createContext();

const ColorModeProvider = ({ children }) => {
const [toggleColorMode, theme] = useToggleColorMode();
return (
<ColorModeContext.Provider value={toggleColorMode}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</ColorModeContext.Provider>
);
};

ColorModeProvider.propTypes = {
children: PropTypes.node,
};

export default ColorModeProvider;

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

@@ -0,0 +1,76 @@
import React, { createContext, useContext, useState } from "react";
import PropTypes from "prop-types";
import usePagingHook from "hooks/usePagingHook";

const apiCall = async (page, itemsPerPage, sort, sortDirection, filter) => {
const res = await fetch(
"http://localhost:4000/items?" +
new URLSearchParams({
_page: page,
_limit: itemsPerPage,
// Conditionally add to params object if keys exist
...(sort && { _sort: sort }),
...(sortDirection && { _order: sortDirection }),
...(filter && { q: filter }),
}),
{
headers: {
"Content-Type": "application/json",
},
}
);
const totalCount = res.headers.get('x-total-count')
const data = await res.json()
return {totalCount, data}
};

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;

+ 48
- 0
src/features/api/apiSlice.js 파일 보기

@@ -0,0 +1,48 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { setCredetnials, logOut } from "features/auth/authSlice";
import { isExpired } from "react-jwt";

const baseQuery = fetchBaseQuery({
baseUrl: "https://strapi.dilig.net",
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token;
if (token) {
headers.set("Authorization", `Bearer ${token.jwt}`);
}
return headers;
},
});

const baseQueryWithReauth = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions);

if (result?.error?.status === 401) {
const token = api.getState().auth.token;
const expired = isExpired(token);
if (expired) {
const refreshResult = await baseQuery(
{
url: "/api/token/refresh",
method: "POST",
body: { refreshToken: token.refreshToken },
},
api,
extraOptions
);
if (refreshResult?.data) {
const user = api.getState().auth.user;
api.dispatch(setCredetnials({ ...refreshResult.data, user }));
result = await baseQuery(args, api, extraOptions);
} else {
api.dispatch(logOut());
}
}
}
return result;
};

export const apiSlice = createApi({
baseQuery: baseQueryWithReauth,
// eslint-disable-next-line no-unused-vars
endpoints: (builder) => ({}),
});

+ 15
- 0
src/features/auth/authApiSlice.js 파일 보기

@@ -0,0 +1,15 @@
import { apiSlice } from "features/api/apiSlice";

export const authApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
login: builder.mutation({
query: (credentials) => ({
url: "/api/auth/local",
method: "POST",
body: { ...credentials },
}),
}),
}),
});

export const { useLoginMutation } = authApiSlice;

+ 23
- 0
src/features/auth/authSlice.js 파일 보기

@@ -0,0 +1,23 @@
import { createSlice } from "@reduxjs/toolkit";

const authSlice = createSlice({
name: "auth",
initialState: { user: null, token: null },
reducers: {
setCredetnials: (state, action) => {
const { user, jwt, refreshToken } = action.payload;
state.user = user;
state.token = { jwt, refreshToken };
},
logOut: (state) => {
state.user = null;
state.token = null;
},
},
});

export const { setCredetnials, logOut } = authSlice.actions;
export default authSlice.reducer;

export const selectCurrentUser = (state) => state.auth.user;
export const selectCurrentToken = (state) => state.auth.token;

+ 13
- 0
src/features/posts/postsApiSlice.js 파일 보기

@@ -0,0 +1,13 @@
import { apiSlice } from "features/api/apiSlice";

export const postsApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
allPosts: builder.query({
query: () => ({
url: "api/posts",
}),
}),
}),
});

export const { useAllPostsQuery } = postsApiSlice;

+ 13
- 0
src/features/provider/providerApiSlice.js 파일 보기

@@ -0,0 +1,13 @@
import { apiSlice } from "features/api/apiSlice";

export const providerApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
providerLogin: builder.query({
query: (data) => ({
url: `api/auth/${data.provider}/callback${data.search}`,
}),
}),
}),
});

export const { useProviderLoginQuery } = providerApiSlice;

+ 109
- 0
src/features/randomData/randomDataSlice.js 파일 보기

@@ -0,0 +1,109 @@
import { createSlice } from "@reduxjs/toolkit";
import generate from "util/randomData";
import { createSelector } from "reselect";

const randomDataSlice = createSlice({
name: "randomData",
initialState: {
items: [],
filteredItems: [],
count: 0,
page: 0,
itemsPerPage: 12,
filter: "",
sort: "",
},
reducers: {
loadRandomData: (state, action) => {
const count = action.payload;
const generatedItems = generate(count);

state.items = generatedItems;
state.filteredItems = generatedItems;
state.count = generatedItems.length;
},
updatePage: (state, action) => {
state.page = action.payload;
},
updateItemsPerPage: (state, action) => {
state.itemsPerPage = action.payload;
},
updateFilter: (state, action) => {
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) => {
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, b) => {
if (a[field] > b[field]) {
return sortDirection;
}
if (b[field] > a[field]) {
return sortDirection * -1;
}
return 0;
});

state.sort = sort;
state.filteredItems = sorted;
},
},
});

export const {
loadRandomData,
updatePage,
updateItemsPerPage,
updateFilter,
updateSort,
} = randomDataSlice.actions;
export default randomDataSlice.reducer;

// Random data selectors

const randomDataSelector = (state) => state.randomData;
export const itemsSelector = createSelector(
randomDataSelector,
(state) => state.filteredItems
);

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
);

+ 15
- 0
src/features/register/registerApiSlice.js 파일 보기

@@ -0,0 +1,15 @@
import { apiSlice } from "features/api/apiSlice";

export const registerApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
register: builder.mutation({
query: (credentials) => ({
url: "api/auth/local/register",
method: "POST",
body: { ...credentials },
}),
}),
}),
});

export const { useRegisterMutation } = registerApiSlice;

+ 37
- 0
src/features/store.js 파일 보기

@@ -0,0 +1,37 @@
import { configureStore } from "@reduxjs/toolkit";
import { apiSlice } from "./api/apiSlice";
import authReducer from "../features/auth/authSlice";
import randomDataReducer from "../features/randomData/randomDataSlice";
import {
persistReducer,
persistStore,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from "redux-persist";
import storage from "redux-persist/lib/storage";

const authPersistConfig = {
key: "auth",
storage,
};

export const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
auth: persistReducer(authPersistConfig, authReducer),
randomData: randomDataReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(apiSlice.middleware),
devTools: true,
});

export const persistor = persistStore(store);

+ 17
- 0
src/hooks/useDebounceHook.js 파일 보기

@@ -0,0 +1,17 @@
import { useEffect, useState } from "react";

const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);

return () => {
clearTimeout(timer);
};
}, [value, delay]);

return debouncedValue;
};

export default useDebounce;

+ 65
- 0
src/hooks/usePagingHook.js 파일 보기

@@ -0,0 +1,65 @@
import { useState, useCallback, useEffect } from "react";
import { unstable_batchedUpdates } from "react-dom";

const usePagingHook = (page, itemsPerPage, sort, filter, apiCallback) => {
const [items, setItems] = useState([]);
const [totalPages, setTotalPages] = useState(0);
const [currentPage, setCurrentPage] = useState(0);
const [loading, setLoading] = useState(false);
const [totalCount, setTotalCount] = useState(0);

const reload = useCallback(async () => {
setLoading(true);
try {
const [sortColumn, sortDirection] = sort.split("-");
const {totalCount, data} = await apiCallback(
page,
itemsPerPage,
sortColumn,
sortDirection,
filter
);

// Prevents multiple rerenders
unstable_batchedUpdates(() => {
setItems(data);
setTotalCount(parseInt(totalCount));
setTotalPages(
Math.ceil(totalCount / 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;

+ 44
- 0
src/hooks/useToggleColorMode.js 파일 보기

@@ -0,0 +1,44 @@
import { useState, useMemo } from "react";
import { createTheme } from "@mui/material/styles";
import {
authScopeSetHelper,
authScopeStringGetHelper,
} from "util/authScopeHelpers";
import selectedTheme from "themes";

const useToggleColorMode = () => {
const currentColorMode = authScopeStringGetHelper("colorMode") || "light";
const [mode, setMode] = useState(currentColorMode);

const toggleColorMode = () => {
const nextMode = mode === "light" ? "dark" : "light";
setMode(nextMode);
authScopeSetHelper("colorMode", nextMode);
};

const theme = useMemo(
() =>
createTheme({
palette: {
mode,
primary: {
main:
mode === "light"
? selectedTheme.colors.primaryLight
: selectedTheme.colors.primaryDark,
},
secondary: {
main:
mode === "light"
? selectedTheme.colors.secondaryLight
: selectedTheme.colors.secondaryDark,
},
},
}),
[mode]
);

return [toggleColorMode, theme];
};

export default useToggleColorMode;

+ 32
- 0
src/i18n/index.js 파일 보기

@@ -0,0 +1,32 @@
import { format as formatDate } from "date-fns";
import i18n from "i18next";
import { initReactI18next } from "react-i18next";

import enTranslations from "./resources/en";
import srTranslations from "./resources/sr";

i18n.use(initReactI18next).init({
lng: "en",
fallbackLng: "en",
debug: false,
supportedLngs: ["en", "sr"],
resources: {
en: {
translation: enTranslations,
},
sr: {
translation: srTranslations,
},
},
interpolation: {
format: (value, format) => {
if (value instanceof Date) {
return formatDate(value, format);
}

return value;
},
},
});

export default i18n;

+ 108
- 0
src/i18n/resources/en.js 파일 보기

@@ -0,0 +1,108 @@
export default {
app: {
title: "React template",
},
refresh: {
title: "Are you active?",
cta: "You were registered as not active, please confirm that you are active in the next minute, if you don't you will be logged out.",
},
common: {
language: "Language",
english: "English",
serbian: "Serbian",
close: "Close",
dataGridExample: "Data Grid Example",
trademark: "TM",
search: "Search",
error: "Error",
continue: "Continue",
labelUsername: "Username",
labelEmail: "Email",
labelPassword: "Password",
next: "Next",
nextPage: "Next page",
previousPage: "Previous page",
back: "Back",
goBack: "Go Back",
ok: "Ok",
done: "Done",
confirm: "Confirm",
printDownload: "Print/Download",
cancel: "Cancel",
remove: "Remove",
invite: "Invite",
save: "Save",
complete: "Complete",
download: "Download",
yes: "Yes",
no: "No",
to: "to",
select: "Select...",
none: "None",
date: {
range: "{{start}} to {{end}}",
},
},
register: {
registerTitle: "Register",
usernameRequired: "Username is required.",
emailFormat: "Invalid email address format.",
emailRequired: "An email or username is required.",
passwordLength: "Your password contain between 8 and 50 characters.",
passwordRequired: "A Password is required.",
},
login: {
welcome: "React template",
dontHaveAccount: "Don't have an account? ",
emailFormat: "Invalid email address format.",
emailRequired: "An email or username is required.",
noUsers: "There are no users with that email.",
passwordStrength: "Your password is {{strength}}.",
passwordLength: "Your password contain between 8 and 50 characters.",
signUpRecommendation: "Sign up",
email: "Please enter your email address or username to log in:",
logInTitle: "Log In",
logIn: "Log In",
signUp: "Sign Up",
usernameRequired: "Username is required.",
passwordRequired: "A Password is required.",
forgotYourPassword: "Forgot your password?",
forgotPasswordEmail: "Email",
useDifferentEmail: "Use different email address or username",
},
password: {
weak: "weak",
average: "average",
good: "good",
strong: "strong",
},
forgotPassword: {
title: "Forgot Password",
label: "Send email",
emailRequired: "An email is required.",
emailFormat: "Invalid email address format.",
forgotPassword: {
title: "Forgot Password",
subtitle:
"Please answer the security question to gain access to your account:",
label: "Reset Password",
},
},
notFound: {
text: "We're sorry but we couldn't find the page you were looking for.",
goBack: "Go back to homepage",
},
errorPage: {
text: "We're sorry, an internal server error came up. Please be patient or try again later.",
goBack: "Go back to homepage",
logout: "Logout",
},
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",
},
};

+ 108
- 0
src/i18n/resources/sr.js 파일 보기

@@ -0,0 +1,108 @@
export default {
app: {
title: "React template",
},
refresh: {
title: "Are you active?",
cta: "You were registered as not active, please confirm that you are active in the next minute, if you don't you will be logged out.",
},
common: {
language: "Jezik",
english: "Engleski",
serbian: "Srpski",
dataGridExample: "Primer Data Grid-a",
close: "Close",
trademark: "TM",
search: "Pretraga",
error: "Greška",
continue: "Nastavite",
labelUsername: "Korisničko ime",
labelEmail: "E-mail",
labelPassword: "Šifra",
next: "Napred",
nextPage: "Sledeća stranica",
previousPage: "Predhodna stranica",
back: "Nazad",
goBack: "Idite nazad",
ok: "U redu",
done: "Gotovo",
confirm: "Potvrdite",
printDownload: "Print/Download",
cancel: "Cancel",
remove: "Remove",
invite: "Invite",
save: "Save",
complete: "Complete",
download: "Download",
yes: "Yes",
no: "No",
to: "to",
select: "Select...",
none: "None",
date: {
range: "{{start}} to {{end}}",
},
},
register: {
registerTitle: "Register",
usernameRequired: "Username is required.",
emailFormat: "Invalid email address format.",
emailRequired: "An email or username is required.",
passwordLength: "Your password contain between 8 and 50 characters.",
passwordRequired: "A Password is required.",
},
login: {
welcome: "React template",
dontHaveAccount: "Nemate nalog? ",
emailFormat: "Loš format email-a",
emailRequired: "Email/korisničko ime je obavezno",
noUsers: "Ne postoji korisnik",
passwordStrength: "Your password is {{strength}}.",
passwordLength: "Your password contain between 8 and 50 characters.",
signUpRecommendation: "Registrujte se",
email: "Please enter your email address or username to log in:",
logInTitle: "Prijava",
logIn: "Ulogujte se",
signUp: "Sign Up",
usernameRequired: "Username is required.",
passwordRequired: "A Password is required.",
forgotYourPassword: "Zaboravili ste šifru?",
forgotPasswordEmail: "Email",
useDifferentEmail: "Use different email address or username",
},
password: {
weak: "weak",
average: "average",
good: "good",
strong: "strong",
},
forgotPassword: {
title: "Forgot Password",
label: "Send email",
emailRequired: "An email is required.",
emailFormat: "Invalid email address format.",
forgotPassword: {
title: "Forgot Password",
subtitle:
"Please answer the security question to gain access to your account:",
label: "Reset Password",
},
},
notFound: {
text: "We're sorry but we couldn't find the page you were looking for.",
goBack: "Go back to homepage",
},
errorPage: {
text: "We're sorry, an internal server error came up. Please be patient or try again later.",
goBack: "Go back to homepage",
logout: "Logout",
},
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",
},
};

+ 13
- 0
src/index.css 파일 보기

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

+ 38
- 0
src/index.js 파일 보기

@@ -0,0 +1,38 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import "./i18n";
import { HelmetProvider } from "react-helmet-async";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { store, persistor } from "./features/store";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import ColorModeProvider from "context/ColorModeContext";
import { CssBaseline } from "@mui/material";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<HelmetProvider>
<React.StrictMode>
<Provider store={store}>
<ColorModeProvider>
<CssBaseline />
<PersistGate loading={null} persistor={persistor}>
<BrowserRouter>
<Routes>
<Route path="/*" element={<App />} />
</Routes>
</BrowserRouter>
</PersistGate>
</ColorModeProvider>
</Provider>
</React.StrictMode>
</HelmetProvider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

+ 4
- 0
src/initialValues/forgotPasswordInitialValues.js 파일 보기

@@ -0,0 +1,4 @@
export default {
email: "",
};

+ 4
- 0
src/initialValues/loginInitialValues.js 파일 보기

@@ -0,0 +1,4 @@
export default {
email: "",
password: "",
};

+ 5
- 0
src/initialValues/registerInitialValues.js 파일 보기

@@ -0,0 +1,5 @@
export default {
username: "",
email: "",
password: "",
};

+ 1
- 0
src/logo.svg 파일 보기

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

+ 40
- 0
src/pages/AuthCallbackPage/AuthCallbackPage.js 파일 보기

@@ -0,0 +1,40 @@
import React, { useEffect } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import PropTypes from "prop-types";
// import Backdrop from 'components/MUI/BackdropComponent';
import { useProviderLoginQuery } from "features/provider/providerApiSlice";
import { useDispatch } from "react-redux";
import { setCredetnials } from "features/auth/authSlice";
import BackdropComponent from "components/Backdrop/BackdropComponent";

function AuthCallback() {
const { provider } = useParams();
const location = useLocation();
const dispatch = useDispatch();
const navigate = useNavigate()

const { data, isLoading } = useProviderLoginQuery({ provider, search: location.search });

useEffect(() => {
if (data?.jwt && data?.refreshToken && data?.user) {
dispatch(setCredetnials(data));
navigate("/home", { replace: true });
}
}, [data]);

return <div>
<BackdropComponent position="absolute" isLoading={isLoading} />
</div>;
}

AuthCallback.propTypes = {
history: PropTypes.shape({
replace: PropTypes.func,
push: PropTypes.func,
location: PropTypes.shape({
pathname: PropTypes.string,
}),
}),
};

export default AuthCallback;

+ 41
- 0
src/pages/HomePage/HomePage.js 파일 보기

@@ -0,0 +1,41 @@
import React from "react";
import NavbarComponent from "components/Navbar/NavbarComponent";
import { Box, Grid } from "@mui/material";
import Modals from "components/Modals/ModalsExample";
import DataGridExample from "components/DataGrid/DataGridExample";
import RandomDataProvider from "context/RandomDataContext";
import PagingSortingFilteringExampleServerSide from "components/PagingSorting/PagingSortingFilteringExampleServerSide";
import PagingSortingFilteringExample from "components/PagingSorting/PagingSortingFilteringExample";
import { useAllPostsQuery } from "features/posts/postsApiSlice";
import BackdropComponent from "components/Backdrop/BackdropComponent";
const HomePage = () => {
const {data, isLoading} = useAllPostsQuery()
console.log('posts', data?.data)
return (
<>
<NavbarComponent />
<BackdropComponent position="absolute" isLoading={isLoading} />
<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}>
<DataGridExample />
</Grid>
<Grid item xs={12} md={9}>
<PagingSortingFilteringExample />
</Grid>
<Grid item xs={12} md={9}>
{/* Move to higher components? */}
<RandomDataProvider>
<PagingSortingFilteringExampleServerSide />
</RandomDataProvider>
</Grid>
</Grid>
</Box>
</>
);
};

export default HomePage;

+ 180
- 0
src/pages/LoginPage/LoginPage.js 파일 보기

@@ -0,0 +1,180 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { useFormik } from "formik";
import { NavLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { FORGOT_PASSWORD_PAGE, REGISTER_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 loginValidation from "validations/loginValidation";
import loginInitialValues from "initialValues/loginInitialValues";
import GoogleIcon from "@mui/icons-material/Google";
import { useLoginMutation } from "features/auth/authApiSlice";
import BackdropComponent from "components/Backdrop/BackdropComponent";
import { useDispatch } from "react-redux";
import { setCredetnials } from "features/auth/authSlice";
import { makeErrorToastMessage } from "util/toastMessage";
import { useNavigate } from "react-router-dom";

const LoginPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();

const [login, { isLoading }] = useLoginMutation();
const dispatch = useDispatch();

const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword(!showPassword);
const handleMouseDownPassword = () => setShowPassword(!showPassword);

const handleGoogle = () => {
window.location = "http://localhost:1337/api/connect/google";
};

const handleSubmit = async (values) => {
const { email: identifier, password } = values;
try {
const userData = await login({ identifier, password }).unwrap();
dispatch(setCredetnials(userData));
navigate("/home", { replace: true });
} catch (err) {
makeErrorToastMessage(err.data.error.message);
}
};

const formik = useFormik({
initialValues: loginInitialValues,
validationSchema: loginValidation,
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 }}
>
<BackdropComponent position="absolute" isLoading={isLoading} />
<TextField
name="email"
label={t("common.labelEmail")}
margin="normal"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
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>
<Button
onClick={handleGoogle}
startIcon={<GoogleIcon />}
fullWidth
variant="outlined"
>
Connect with Google
</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={REGISTER_PAGE}
component={NavLink}
variant="body2"
underline="hover"
>
{t("login.dontHaveAccount")}
</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
);
};

LoginPage.propTypes = {
history: PropTypes.shape({
replace: PropTypes.func,
push: PropTypes.func,
location: PropTypes.shape({
pathname: PropTypes.string,
}),
}),
};
export default LoginPage;

+ 186
- 0
src/pages/RegisterPage/RegisterPage.js 파일 보기

@@ -0,0 +1,186 @@
/* eslint-disable */
import React, { useState } from "react";
import PropTypes from "prop-types";
import { useFormik } from "formik";
import { NavLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { FORGOT_PASSWORD_PAGE, LOGIN_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 GoogleIcon from "@mui/icons-material/Google";
import registerInitialValues from "initialValues/registerInitialValues";
import registerValidation from "validations/registerValidation";
import { makeErrorToastMessage, makeToastMessage } from "util/toastMessage";
import { useRegisterMutation } from "features/register/registerApiSlice";
import { useNavigate } from "react-router-dom";
import BackdropComponent from "components/Backdrop/BackdropComponent";

const RegisterPage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword(!showPassword);
const handleMouseDownPassword = () => setShowPassword(!showPassword);

const [register, { isLoading }] = useRegisterMutation();

const handleGoogle = () => {
window.location = "http://localhost:1337/api/connect/google";
};

const handleSubmit = async (values) => {
const { username, email, password } = values;
try {
await register({ username, email, password }).unwrap();
makeToastMessage("User successfuly registered. Please login.");
navigate("/login");
} catch (err) {
makeErrorToastMessage(err.data.error.message);
}
};

const formik = useFormik({
initialValues: registerInitialValues,
validationSchema: registerValidation,
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("register.registerTitle")}
</Typography>
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: "relative", mt: 1, p: 1 }}
>
<BackdropComponent 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="email"
label={t("common.labelEmail")}
margin="normal"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
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("register.registerTitle")}
</Button>
<Button
onClick={handleGoogle}
startIcon={<GoogleIcon />}
fullWidth
variant="outlined"
>
Connect with Google
</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={LOGIN_PAGE}
component={NavLink}
variant="body2"
underline="hover"
>
{t("login.logIn")}
</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
);
};

RegisterPage.propTypes = {
history: PropTypes.shape({
replace: PropTypes.func,
push: PropTypes.func,
location: PropTypes.shape({
pathname: PropTypes.string,
}),
}),
};
export default RegisterPage;

+ 13
- 0
src/reportWebVitals.js 파일 보기

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};

export default reportWebVitals;

+ 5
- 0
src/setupTests.js 파일 보기

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

+ 13
- 0
src/themes/index.js 파일 보기

@@ -0,0 +1,13 @@
import primary from "./primaryTheme/primaryTheme";

let selectedThemeNumber = 0;

const getTheme = () => {
if (selectedThemeNumber === 0) {
return {...primary}
}
}

const selectedTheme = getTheme();

export default selectedTheme;

+ 7
- 0
src/themes/primaryTheme/primaryTheme.js 파일 보기

@@ -0,0 +1,7 @@
import { primaryThemeColors } from "./primaryThemeColors";

const primary = {
colors: primaryThemeColors,
};

export default primary;

+ 7
- 0
src/themes/primaryTheme/primaryThemeColors.js 파일 보기

@@ -0,0 +1,7 @@
export const primaryThemeColors = {
primaryLight: "#673ab7",
primaryDark: "#009688",
secondaryLight: "#212121",
secondaryDark: "#f5f5f5",
};

+ 0
- 0
src/themes/primaryTheme/primaryThemeFonts.js 파일 보기


+ 19
- 0
src/util/authScopeHelpers.js 파일 보기

@@ -0,0 +1,19 @@
export function authScopeGetHelper(key) {
return JSON.parse(localStorage.getItem(key));
}

export function authScopeStringGetHelper(key) {
return localStorage.getItem(key);
}

export function authScopeSetHelper(key, value) {
localStorage.setItem(key, value);
}

export function authScopeRemoveHelper(key) {
localStorage.removeItem(key);
}

export function authScopeClearHelper() {
localStorage.clear();
}

+ 40
- 0
src/util/dateHelpers.js 파일 보기

@@ -0,0 +1,40 @@
import { format } from "date-fns";
import { enUS } from "date-fns/locale";
import i18next from "i18next";

export function formatDate(date, fmt = "MM/dd/y", locale = enUS) {
const dt = new Date(date);
return format(dt, fmt, { locale });
}

export function formatDateTime(date) {
const dt = new Date(date);
return format(dt, "MM/dd/y hh:mm aa");
}

export function getDateDay(date) {
const dt = new Date(date);
return format(dt, "dd");
}

export function getDateMonth(date) {
const dt = new Date(date);
return format(dt, "MM");
}

export function getDateYear(date) {
const dt = new Date(date);
return format(dt, "y");
}

export function formatDateTimeLocale(date) {
const dt = new Date(date);
return format(dt, "MM/dd/y hh:mm aa");
}

// TODO add locale
export function formatDateRange(dates) {
const start = formatDate(dates.start);
const end = formatDate(dates.end);
return i18next.t("common.date.range", { start, end });
}

+ 1
- 0
src/util/enumMappers.js 파일 보기

@@ -0,0 +1 @@
export const parseEnumType = (typeArray, index) => typeArray[index - 1];

+ 65
- 0
src/util/randomData.js 파일 보기

@@ -0,0 +1,65 @@
const random = (arr) => {
return arr[Math.floor(Math.random() * arr.length)];
};

const size = () => {
return random(["Extra Small", "Small", "Medium", "Large", "Extra Large"]);
};

const color = () => {
return random(["Red", "Green", "Blue", "Orange", "Yellow"]);
};

const designer = () => {
return random([
"Ralph Lauren",
"Alexander Wang",
"Grayse",
"Marc NY Performance",
"Scrapbook",
"J Brand Ready to Wear",
"Vintage Havana",
"Neiman Marcus Cashmere Collection",
"Derek Lam 10 Crosby",
"Jordan",
]);
};

const type = () => {
return random([
"Cashmere",
"Cardigans",
"Crew and Scoop",
"V-Neck",
"Shoes",
"Cowl & Turtleneck",
]);
};

const price = () => {
return (Math.random() * 100).toFixed(2);
};

function generate(count) {
const data = [];
for (let i = 0; i < count; i++) {
const currentColor = color();
const currentSize = size();
const currentType = type();
const currentDesigner = designer();
const currentPrice = price();

data.push({
name: `${currentDesigner} ${currentType} ${currentColor} ${currentSize}`,
color: currentColor,
size: currentSize,
designer: currentDesigner,
type: currentType,
price: currentPrice,
salesPrice: currentPrice,
});
}
return data;
}

export default generate;

+ 11
- 0
src/util/stringHelpers.js 파일 보기

@@ -0,0 +1,11 @@
export function separateByUppercase(string) {
return string.split(/(?=[A-Z])/).join(" ");
}

export function separateByUnderscore(string) {
return string.replaceAll("_", " ");
}

export function joinArrayWithComma(arr) {
return arr.join(", ");
}

+ 16
- 0
src/util/toastMessage.js 파일 보기

@@ -0,0 +1,16 @@
import { toast } from "react-toastify";

const defaultOptions = {
position: "top-center",
autoClose: 3000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
pauseOnFocusLoss: false,
draggable: true,
};

export const makeToastMessage = (message, options = defaultOptions) =>
toast(message, options);
export const makeErrorToastMessage = (message, options = defaultOptions) =>
toast.error(message, options);

+ 8
- 0
src/validations/forgotPasswordValidation.js 파일 보기

@@ -0,0 +1,8 @@
import * as Yup from "yup";
import i18next from "i18next";

export default Yup.object().shape({
email: Yup.string()
.email(i18next.t("login.emailFormat"))
.required(i18next.t("login.emailRequired")),
});

+ 11
- 0
src/validations/loginValidation.js 파일 보기

@@ -0,0 +1,11 @@
import * as Yup from "yup";
import i18next from "i18next";

export default Yup.object().shape({
email: Yup.string()
.email(i18next.t("login.emailFormat"))
.required(i18next.t("login.emailRequired")),
password: Yup.string()
.required(i18next.t("login.passwordRequired"))
.min(8, i18next.t("login.passwordLength")),
});

+ 12
- 0
src/validations/registerValidation.js 파일 보기

@@ -0,0 +1,12 @@
import * as Yup from "yup";
import i18next from "i18next";

export default Yup.object().shape({
username: Yup.string().required("register.usernameRequired"),
email: Yup.string()
.email(i18next.t("register.emailFormat"))
.required(i18next.t("register.emailRequired")),
password: Yup.string()
.required(i18next.t("register.passwordRequired"))
.min(8, i18next.t("register.passwordLength")),
});

Loading…
취소
저장