Lazar Kostic 2 yıl önce
işleme
cf75fbeef3
75 değiştirilmiş dosya ile 35244 ekleme ve 0 silme
  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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

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

+ 32455
- 0
package-lock.json
Dosya farkı çok büyük olduğundan ihmal edildi
Dosyayı Görüntüle


+ 69
- 0
package.json Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle


+ 43
- 0
public/index.html Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle


BIN
public/logo512.png Dosyayı Görüntüle


+ 25
- 0
public/manifest.json Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

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

+ 38
- 0
src/App.css Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle


+ 4
- 0
src/constants/localStorage.js Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

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

+ 4
- 0
src/initialValues/loginInitialValues.js Dosyayı Görüntüle

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

+ 5
- 0
src/initialValues/registerInitialValues.js Dosyayı Görüntüle

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

+ 1
- 0
src/logo.svg Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

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

const primary = {
colors: primaryThemeColors,
};

export default primary;

+ 7
- 0
src/themes/primaryTheme/primaryThemeColors.js Dosyayı Görüntüle

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

+ 0
- 0
src/themes/primaryTheme/primaryThemeFonts.js Dosyayı Görüntüle


+ 19
- 0
src/util/authScopeHelpers.js Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

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

+ 65
- 0
src/util/randomData.js Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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 Dosyayı Görüntüle

@@ -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…
İptal
Kaydet