feat: initial commit and template import
This commit is contained in:
1
.env.development.example
Normal file
1
.env.development.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=https://api.example.com
|
||||||
1
.env.production.example
Normal file
1
.env.production.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=https://api.example.com
|
||||||
6
.eslintignore
Normal file
6
.eslintignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
node_modules/*
|
||||||
|
public
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
70
.eslintrc.cjs
Normal file
70
.eslintrc.cjs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'airbnb',
|
||||||
|
'prettier',
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:import/recommended',
|
||||||
|
'plugin:jsx-a11y/recommended',
|
||||||
|
'eslint-config-prettier',
|
||||||
|
],
|
||||||
|
plugins: ['react', 'react-hooks', 'prettier', 'jsx-a11y', 'import'],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
// Tells eslint-plugin-react to automatically detect the version of React to use.
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
// Tells eslint how to resolve imports
|
||||||
|
'import/resolver': {
|
||||||
|
node: {
|
||||||
|
paths: ['src'],
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'prettier/prettier': 'error',
|
||||||
|
'jsx-a11y/anchor-is-valid': 'off',
|
||||||
|
'react-hooks/exhaustive-deps': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
|
||||||
|
'no-unused-expressions': [
|
||||||
|
'error',
|
||||||
|
{ allowShortCircuit: true, allowTernary: true },
|
||||||
|
],
|
||||||
|
'import/no-extraneous-dependencies': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
devDependencies: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'linebreak-style': 'off',
|
||||||
|
'implicit-arrow-linebreak': 'off',
|
||||||
|
indent: 'off',
|
||||||
|
'object-curly-newline': 'off',
|
||||||
|
'operator-linebreak': 'off',
|
||||||
|
'no-confusing-arrow': 'off',
|
||||||
|
'function-paren-newline': 'off',
|
||||||
|
'no-mixed-operators': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
||||||
9
.prettierignore
Normal file
9
.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
/.cache
|
||||||
|
/.husky
|
||||||
|
.eslintignore
|
||||||
|
.gitignore
|
||||||
|
.git
|
||||||
|
LICENSE
|
||||||
|
build
|
||||||
|
dist
|
||||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
||||||
73
README.md
Normal file
73
README.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
## viterjs-template
|
||||||
|
|
||||||
|
JavaScript + React + Redux + Mui + Axios + ESLint + Prettier
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
#### Clone the repo
|
||||||
|
|
||||||
|
```
|
||||||
|
npx degit emre-cil/viterjs-template my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
cd my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Paths
|
||||||
|
|
||||||
|
Application using absolute paths
|
||||||
|
Example: '@/components/Counter/Counter';
|
||||||
|
|
||||||
|
if you don't want to use you can remove these lines from
|
||||||
|
|
||||||
|
> vite.config.js
|
||||||
|
|
||||||
|
```
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
> jsconfig.json
|
||||||
|
|
||||||
|
```
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
| Script | Description |
|
||||||
|
| ------------- | ---------------------------------- |
|
||||||
|
| pnpm dev | Runs the application. |
|
||||||
|
| pnpm build | Create builds for the application. |
|
||||||
|
| pnpm preview | Runs the Vite preview |
|
||||||
|
| pnpm lint | Display eslint errors |
|
||||||
|
| pnpm lint:fix | Fix the eslint errors |
|
||||||
|
| pnpm format | Runs prettier for all files |
|
||||||
|
| pnpm test | Run tests |
|
||||||
|
|
||||||
|
### Check List
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
16
index.html
Normal file
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Dosis:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
||||||
|
<title>Vite + React</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
jsconfig.json
Normal file
18
jsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "**/node_modules/*", "dist", "**/dist/*"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
10875
package-lock.json
generated
Normal file
10875
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
70
package.json
Normal file
70
package.json
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"name": "viterjs-template",
|
||||||
|
"version": "1.1.7",
|
||||||
|
"description": "React + Vite + Redux + MuI + axios + RRD + Prettier => Boilerplate",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "emrecil <info.emrecil@gmail.com>",
|
||||||
|
"keywords": [
|
||||||
|
"react",
|
||||||
|
"boilerplate",
|
||||||
|
"javaScript",
|
||||||
|
"starter",
|
||||||
|
"vite",
|
||||||
|
"redux",
|
||||||
|
"material-ui",
|
||||||
|
"axios",
|
||||||
|
"rrd",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/emre-cil/viterjs-template.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/emre-cil/viterjs-template/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/emre-cil/viterjs-template#readme",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --open",
|
||||||
|
"start": "vite --open",
|
||||||
|
"host": "vite --open --host",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
|
||||||
|
"lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
|
||||||
|
"format": "prettier \"src/**/*.{js,jsx,ts,tsx,css,scss}\" --write",
|
||||||
|
"format+lint": "prettier \"src/**/*.{js,jsx,ts,tsx,css,scss}\" --write && eslint src --ext .js,.jsx,.ts,.tsx --fix",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:cover": "vitest run --coverage"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.3",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@mui/icons-material": "^5.15.10",
|
||||||
|
"@mui/material": "^5.15.10",
|
||||||
|
"@reduxjs/toolkit": "^2.2.1",
|
||||||
|
"axios": "^1.6.7",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-redux": "^9.1.0",
|
||||||
|
"react-router-dom": "^6.22.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.23.9",
|
||||||
|
"@types/react": "^18.2.57",
|
||||||
|
"@types/react-dom": "^18.2.19",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"vite": "^5.1.4",
|
||||||
|
"vitest": "^1.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
15
src/App.jsx
Normal file
15
src/App.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { CssBaseline } from '@mui/material';
|
||||||
|
import Routing from './routes/Routing';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<CssBaseline />
|
||||||
|
<Routing />
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
36
src/app/api/apiSlice.js
Normal file
36
src/app/api/apiSlice.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
|
// import { setCredentials, logOut } from '../../features/user/userSlice';
|
||||||
|
|
||||||
|
const baseQuery = fetchBaseQuery({
|
||||||
|
baseUrl: import.meta.env.VITE_API_URL,
|
||||||
|
prepareHeaders: (headers, { getState }) => {
|
||||||
|
const { token } = getState().auth;
|
||||||
|
if (token) {
|
||||||
|
headers.set('Authorization', `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseQueryWithReauth = async (args, api, extraOptions) => {
|
||||||
|
const result = await baseQuery(args, api, extraOptions);
|
||||||
|
// ====================== TODO: Implement 401 unauthorized re-authentication ======================
|
||||||
|
// if (result?.error?.status === 401) {
|
||||||
|
// const refreshResult = await baseQuery('/refresh', api, extraOptions);
|
||||||
|
// if (refreshResult?.data) {
|
||||||
|
// const { user } = api.getState().auth;
|
||||||
|
// api.dispatch(setCredentials({ user, ...refreshResult.data }));
|
||||||
|
// result = await baseQuery(args, api, extraOptions);
|
||||||
|
// } else {
|
||||||
|
// api.dispatch(logOut());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiSlice = createApi({
|
||||||
|
baseQuery: baseQueryWithReauth,
|
||||||
|
endpoints: () => ({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default apiSlice;
|
||||||
17
src/app/store.js
Normal file
17
src/app/store.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import counterReducer from '@/features/counter/counterSlice';
|
||||||
|
import apiSlice from './api/apiSlice';
|
||||||
|
import userReducer from '@/features/user/userSlice';
|
||||||
|
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
[apiSlice.reducerPath]: apiSlice.reducer,
|
||||||
|
counter: counterReducer,
|
||||||
|
user: userReducer,
|
||||||
|
},
|
||||||
|
middleware: (getdefaultMiddleware) =>
|
||||||
|
getdefaultMiddleware().concat(apiSlice.middleware),
|
||||||
|
devTools: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default store;
|
||||||
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
86
src/components/Counter/Counter.jsx
Normal file
86
src/components/Counter/Counter.jsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import {
|
||||||
|
decrement,
|
||||||
|
increment,
|
||||||
|
incrementByAmount,
|
||||||
|
incrementAsync,
|
||||||
|
incrementIfOdd,
|
||||||
|
selectCount,
|
||||||
|
} from '@/features/counter/counterSlice';
|
||||||
|
import styles from './Counter.module.css';
|
||||||
|
|
||||||
|
function Counter() {
|
||||||
|
const count = useSelector(selectCount);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [incrementAmount, setIncrementAmount] = React.useState('2');
|
||||||
|
|
||||||
|
const incrementValue = Number(incrementAmount) || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={styles.wrapper}
|
||||||
|
sx={{
|
||||||
|
py: 10,
|
||||||
|
px: 2,
|
||||||
|
mt: 3,
|
||||||
|
boxShadow: 3,
|
||||||
|
borderRadius: 5,
|
||||||
|
backgroundColor: 'grey.100',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.button}
|
||||||
|
aria-label="Decrement value"
|
||||||
|
onClick={() => dispatch(decrement())}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<span className={styles.value}>{count}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.button}
|
||||||
|
aria-label="Increment value"
|
||||||
|
onClick={() => dispatch(increment())}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.row2}>
|
||||||
|
<input
|
||||||
|
className={styles.textbox}
|
||||||
|
type="number"
|
||||||
|
aria-label="Set increment amount"
|
||||||
|
value={incrementAmount}
|
||||||
|
onChange={(e) => setIncrementAmount(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.button}
|
||||||
|
onClick={() => dispatch(incrementByAmount(incrementValue))}
|
||||||
|
>
|
||||||
|
Add Amount
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.asyncButton}
|
||||||
|
onClick={() => dispatch(incrementAsync(incrementValue))}
|
||||||
|
>
|
||||||
|
Add Async
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.button}
|
||||||
|
onClick={() => dispatch(incrementIfOdd(incrementValue))}
|
||||||
|
>
|
||||||
|
Add If Odd
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Counter;
|
||||||
97
src/components/Counter/Counter.module.css
Normal file
97
src/components/Counter/Counter.module.css
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
.row,
|
||||||
|
.row2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.row2 {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.row2 > button {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.row > button,
|
||||||
|
.row2 > button {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.row:not(:last-child) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 78px;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: #f9ed69;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
font-size: 32px;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
outline: none;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
color: #fff;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow:
|
||||||
|
rgba(0, 0, 0, 0.12) 0px 1px 3px,
|
||||||
|
rgba(0, 0, 0, 0.24) 0px 1px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textbox {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #fff;
|
||||||
|
height: 3rem;
|
||||||
|
padding: 2px;
|
||||||
|
width: 64px;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 4px;
|
||||||
|
background-color: transparent;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
box-shadow:
|
||||||
|
rgba(0, 0, 0, 0.12) 0px 1px 3px,
|
||||||
|
rgba(0, 0, 0, 0.24) 0px 1px 2px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:active {
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asyncButton {
|
||||||
|
position: relative;
|
||||||
|
composes: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asyncButton:after {
|
||||||
|
content: '';
|
||||||
|
background-color: rgba(112, 76, 182, 0.15);
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition:
|
||||||
|
width 1s linear,
|
||||||
|
opacity 0.5s ease 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asyncButton:active:after {
|
||||||
|
width: 0%;
|
||||||
|
opacity: 1;
|
||||||
|
transition: 0s;
|
||||||
|
}
|
||||||
148
src/components/TemplateTester/TemplateTester.jsx
Normal file
148
src/components/TemplateTester/TemplateTester.jsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, Box, Typography, IconButton } from '@mui/material';
|
||||||
|
import Brightness4Icon from '@mui/icons-material/Brightness4';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { changeMode, selectMode } from '@/features/user/userSlice';
|
||||||
|
|
||||||
|
function TemplateTester() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const mode = useSelector(selectMode);
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
{
|
||||||
|
type: 'primary',
|
||||||
|
colors: ['.light', '.main', '.dark'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'secondary',
|
||||||
|
colors: ['.light', '.main', '.dark'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'error',
|
||||||
|
colors: ['.light', '.main', '.dark'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
colors: ['.light', '.main', '.dark'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'info',
|
||||||
|
colors: ['.light', '.main', '.dark'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
colors: ['.light', '.main', '.dark'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'grey',
|
||||||
|
colors: [
|
||||||
|
'.50',
|
||||||
|
'.100',
|
||||||
|
'.200',
|
||||||
|
'.300',
|
||||||
|
'.400',
|
||||||
|
'.500',
|
||||||
|
'.600',
|
||||||
|
'.700',
|
||||||
|
'.800',
|
||||||
|
'.900',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'background',
|
||||||
|
colors: ['.default', '.paper', '.opposite', '.light'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
colors: ['.primary', '.secondary', '.disabled'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const typographies = [
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'subtitle1',
|
||||||
|
'subtitle2',
|
||||||
|
'body1',
|
||||||
|
'body2',
|
||||||
|
'body3',
|
||||||
|
'body4',
|
||||||
|
'caption',
|
||||||
|
'button',
|
||||||
|
'overline',
|
||||||
|
];
|
||||||
|
const themeTypes = (type, func) => (
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
boxShadow: 3,
|
||||||
|
borderRadius: 5,
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: 'grey.100',
|
||||||
|
}}
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
|
<Typography variant="h3">{type}</Typography> {func}
|
||||||
|
<IconButton
|
||||||
|
onClick={() => dispatch(changeMode())}
|
||||||
|
sx={{ position: 'absolute', top: 10, right: 10 }}
|
||||||
|
>
|
||||||
|
<Brightness4Icon
|
||||||
|
sx={{
|
||||||
|
transition: 'transform 0.4s',
|
||||||
|
transform: mode === 'dark' ? 'rotateY(180deg)' : 'rotateY(0deg)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const colorCards = colors.map((cat) => (
|
||||||
|
<Stack key={cat.type} gap={1}>
|
||||||
|
<Typography variant="h5">{cat.type}</Typography>
|
||||||
|
<Stack direction="row" flexWrap="wrap" gap={2}>
|
||||||
|
{cat.colors.map((color) => (
|
||||||
|
<Stack key={color}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
boxShadow: 2,
|
||||||
|
width: { xs: 62, sm: 100, md: 125 },
|
||||||
|
height: { xs: 62, sm: 100, md: 125 },
|
||||||
|
backgroundColor: cat.type + color,
|
||||||
|
background: color === 'gradient' && cat.type + color,
|
||||||
|
borderRadius: 1,
|
||||||
|
p: 0.65,
|
||||||
|
'& p': {
|
||||||
|
fontSize: { xs: 10, sm: 14, md: 16 },
|
||||||
|
textShadow:
|
||||||
|
mode === 'dark' ? '0px 0px 10px #000' : '0px 0px 10px #fff',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography fontWeight="bold">{color}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
));
|
||||||
|
|
||||||
|
const typoCards = typographies.map((typo) => (
|
||||||
|
<Stack key={typo} gap={1}>
|
||||||
|
<Typography variant={typo}>{typo}</Typography>
|
||||||
|
</Stack>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap={5}>
|
||||||
|
{themeTypes('#Colors', colorCards)}
|
||||||
|
{themeTypes('#Typography', typoCards)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TemplateTester;
|
||||||
7
src/features/counter/counterAPI.js
Normal file
7
src/features/counter/counterAPI.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* eslint-disable no-promise-executor-return */
|
||||||
|
// A mock function to mimic making an async request for data
|
||||||
|
export default function fetchCount(amount = 1) {
|
||||||
|
return new Promise((resolve) =>
|
||||||
|
setTimeout(() => resolve({ data: amount }), 500),
|
||||||
|
);
|
||||||
|
}
|
||||||
74
src/features/counter/counterSlice.js
Normal file
74
src/features/counter/counterSlice.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||||
|
import fetchCount from './counterAPI';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
value: 0,
|
||||||
|
status: 'idle',
|
||||||
|
};
|
||||||
|
|
||||||
|
// The function below is called a thunk and allows us to perform async logic. It
|
||||||
|
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
|
||||||
|
// will call the thunk with the `dispatch` function as the first argument. Async
|
||||||
|
// code can then be executed and other actions can be dispatched. Thunks are
|
||||||
|
// typically used to make async requests.
|
||||||
|
export const incrementAsync = createAsyncThunk(
|
||||||
|
'counter/fetchCount',
|
||||||
|
async (amount) => {
|
||||||
|
const response = await fetchCount(amount);
|
||||||
|
// The value we return becomes the `fulfilled` action payload
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const counterSlice = createSlice({
|
||||||
|
name: 'counter',
|
||||||
|
initialState,
|
||||||
|
// The `reducers` field lets us define reducers and generate associated actions
|
||||||
|
reducers: {
|
||||||
|
increment: (state) => {
|
||||||
|
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
||||||
|
// doesn't actually mutate the state because it uses the Immer library,
|
||||||
|
// which detects changes to a "draft state" and produces a brand new
|
||||||
|
// immutable state based off those changes
|
||||||
|
state.value += 1;
|
||||||
|
},
|
||||||
|
decrement: (state) => {
|
||||||
|
state.value -= 1;
|
||||||
|
},
|
||||||
|
// Use the PayloadAction type to declare the contents of `action.payload`
|
||||||
|
incrementByAmount: (state, action) => {
|
||||||
|
state.value += action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// The `extraReducers` field lets the slice handle actions defined elsewhere,
|
||||||
|
// including actions generated by createAsyncThunk or in other slices.
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(incrementAsync.pending, (state) => {
|
||||||
|
state.status = 'loading';
|
||||||
|
})
|
||||||
|
.addCase(incrementAsync.fulfilled, (state, action) => {
|
||||||
|
state.status = 'idle';
|
||||||
|
state.value += action.payload;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
|
||||||
|
|
||||||
|
// The function below is called a selector and allows us to select a value from
|
||||||
|
// the state. Selectors can also be defined inline where they're used instead of
|
||||||
|
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
|
||||||
|
export const selectCount = (state) => state.counter.value;
|
||||||
|
|
||||||
|
// We can also write thunks by hand, which may contain both sync and async logic.
|
||||||
|
// Here's an example of conditionally dispatching actions based on current state.
|
||||||
|
export const incrementIfOdd = (amount) => (dispatch, getState) => {
|
||||||
|
const currentValue = selectCount(getState());
|
||||||
|
if (currentValue % 2 === 1) {
|
||||||
|
dispatch(incrementByAmount(amount));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default counterSlice.reducer;
|
||||||
39
src/features/user/userApiSlice.js
Normal file
39
src/features/user/userApiSlice.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import apiSlice from '@/app/api/apiSlice';
|
||||||
|
|
||||||
|
export const userApiSlice = apiSlice.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
login: builder.mutation({
|
||||||
|
query: (body) => ({
|
||||||
|
url: '/login',
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
credentials: 'include',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
register: builder.mutation({
|
||||||
|
query: (body) => ({
|
||||||
|
url: '/register',
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
logout: builder.mutation({
|
||||||
|
query: () => ({
|
||||||
|
url: '/logout',
|
||||||
|
method: 'POST',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
refresh: builder.mutation({
|
||||||
|
query: () => ({
|
||||||
|
url: '/refresh',
|
||||||
|
method: 'POST',
|
||||||
|
withCredentials: true,
|
||||||
|
credentials: 'include',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useLoginMutation, useLogoutMutation, useRegisterMutation } =
|
||||||
|
userApiSlice;
|
||||||
56
src/features/user/userSlice.js
Normal file
56
src/features/user/userSlice.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
token: null,
|
||||||
|
user: {
|
||||||
|
Id: import.meta.env.VITE_WEB_USER_ID,
|
||||||
|
FirstName: '',
|
||||||
|
LastName: '',
|
||||||
|
},
|
||||||
|
mode: localStorage.getItem('mode')
|
||||||
|
? localStorage.getItem('mode')
|
||||||
|
: window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userSlice = createSlice({
|
||||||
|
name: 'user',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setCredentials: (state, action) => {
|
||||||
|
state.token = action.payload.AccessToken;
|
||||||
|
state.user = action.payload.User;
|
||||||
|
},
|
||||||
|
logOut: (state) => {
|
||||||
|
state.user = { Id: import.meta.env.VITE_WEB_USER_ID };
|
||||||
|
state.token = null;
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
},
|
||||||
|
setToken: (state, action) => {
|
||||||
|
state.token = action.payload;
|
||||||
|
},
|
||||||
|
changeMode: (state) => {
|
||||||
|
if (state.mode === 'light') {
|
||||||
|
state.mode = 'dark';
|
||||||
|
localStorage.setItem('mode', 'dark');
|
||||||
|
} else {
|
||||||
|
state.mode = 'light';
|
||||||
|
localStorage.setItem('mode', 'light');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setCredentials, logOut, setToken, changeMode } =
|
||||||
|
userSlice.actions;
|
||||||
|
|
||||||
|
export const selectUser = (state) => state.user.user;
|
||||||
|
|
||||||
|
export const selectToken = (state) => state.user.token;
|
||||||
|
|
||||||
|
export const selectMode = (state) => state.user.mode;
|
||||||
|
|
||||||
|
export default userSlice.reducer;
|
||||||
13
src/hooks/useBoolean.js
Normal file
13
src/hooks/useBoolean.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function useBoolean(initialValue = false) {
|
||||||
|
const [value, setValue] = React.useState(initialValue);
|
||||||
|
|
||||||
|
const setTrue = React.useCallback(() => setValue(true), []);
|
||||||
|
const setFalse = React.useCallback(() => setValue(false), []);
|
||||||
|
const toggle = React.useCallback(() => setValue((x) => !x), []);
|
||||||
|
|
||||||
|
return [value, setTrue, setFalse, toggle];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useBoolean;
|
||||||
17
src/hooks/useDebounce.js
Normal file
17
src/hooks/useDebounce.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function useDebounce(value, delay) {
|
||||||
|
const [debouncedValue, setDebouncedValue] = React.useState(value);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDebounce;
|
||||||
1
src/main.css
Normal file
1
src/main.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* global style */
|
||||||
17
src/main.jsx
Normal file
17
src/main.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import React from 'react';
|
||||||
|
import store from './app/store';
|
||||||
|
import AppThemeProvider from './themes/AppThemeProvider';
|
||||||
|
import App from './App';
|
||||||
|
import './main.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Provider store={store}>
|
||||||
|
<AppThemeProvider>
|
||||||
|
<App />
|
||||||
|
</AppThemeProvider>
|
||||||
|
</Provider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
86
src/pages/Auth/Auth.styles.js
Normal file
86
src/pages/Auth/Auth.styles.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
export const FormSX = {
|
||||||
|
p: 3,
|
||||||
|
zIndex: 1,
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
boxShadow: 6,
|
||||||
|
userSelect: 'none',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
borderRadius: '10px',
|
||||||
|
transition: 'transform 0.3s',
|
||||||
|
width: { xs: '90%', sm: 400 },
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
|
||||||
|
img: {
|
||||||
|
p: '16px 32px',
|
||||||
|
},
|
||||||
|
|
||||||
|
button: {
|
||||||
|
height: '3rem',
|
||||||
|
color: 'white',
|
||||||
|
textShadow: '1px 1px 1px rgba(0, 0, 0, 0.6)',
|
||||||
|
},
|
||||||
|
|
||||||
|
a: {
|
||||||
|
fontWeight: '500',
|
||||||
|
lineHeight: '19px',
|
||||||
|
color: 'primary.main',
|
||||||
|
transition: 'color 0.3s',
|
||||||
|
textShadow: '1px 1px 1px rgba(0, 0, 0, 0.6)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'@keyframes wawes': {
|
||||||
|
from: {
|
||||||
|
transform: 'rotate(0deg)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: 'rotate(360deg)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'&::before, &::after': {
|
||||||
|
content: '""',
|
||||||
|
zIndex: -1,
|
||||||
|
width: '600px',
|
||||||
|
height: '800px',
|
||||||
|
position: 'absolute',
|
||||||
|
borderRadius: '40% 45% 35% 40%',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&::before': {
|
||||||
|
top: '-35%',
|
||||||
|
left: '75%',
|
||||||
|
animation: 'wawes 6s linear infinite',
|
||||||
|
background:
|
||||||
|
'linear-gradient(90deg, rgba(0, 101, 243, 0.2) 0%, rgba(0, 120, 255, 0.4) 100%)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&::after': {
|
||||||
|
top: '-30%',
|
||||||
|
left: '70%',
|
||||||
|
animation: 'wawes 8s linear infinite',
|
||||||
|
background:
|
||||||
|
'linear-gradient(90deg, rgba(0, 110, 255, 0.5) 0%, rgba(0, 120, 255, 0.3) 100%)',
|
||||||
|
},
|
||||||
|
|
||||||
|
'& span': {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
'& input': {
|
||||||
|
fontSize: '16px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
},
|
||||||
|
'& label.Mui-focused ': {
|
||||||
|
pt: 0.2,
|
||||||
|
},
|
||||||
|
'& .MuiFormLabel-root': {
|
||||||
|
fontSize: '16px',
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
37
src/pages/Auth/AuthOutlet.jsx
Normal file
37
src/pages/Auth/AuthOutlet.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Stack, useTheme, Typography } from '@mui/material';
|
||||||
|
import { FormSX } from './Auth.styles';
|
||||||
|
|
||||||
|
function AuthOutlet({ children, header }) {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<Stack
|
||||||
|
gap={3}
|
||||||
|
sx={{
|
||||||
|
...FormSX,
|
||||||
|
border: `1px solid ${theme.palette.grey.border}`,
|
||||||
|
background: theme.palette.grey[50],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{header ? (
|
||||||
|
<Typography textAlign="center" variant="h2">
|
||||||
|
{header}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
theme.palette.mode === 'dark'
|
||||||
|
? 'https://picsum.photos/100/50'
|
||||||
|
: 'https://picsum.photos/200/300'
|
||||||
|
}
|
||||||
|
alt="logo"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthOutlet;
|
||||||
105
src/pages/Auth/Login.jsx
Normal file
105
src/pages/Auth/Login.jsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
InputAdornment,
|
||||||
|
Button,
|
||||||
|
Link,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/material';
|
||||||
|
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||||
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import React from 'react';
|
||||||
|
import AuthOutlet from './AuthOutlet';
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
const email = React.useRef(null);
|
||||||
|
const password = React.useRef(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [showPassword, setShowPassword] = React.useState(false);
|
||||||
|
const handleClickShowPassword = () => setShowPassword(!showPassword);
|
||||||
|
const handleMouseDownPassword = () => setShowPassword(!showPassword);
|
||||||
|
|
||||||
|
const loginHandler = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const user = email.current.value.replace(/\s+/g, '');
|
||||||
|
const pwd = password.current.value.replace(/\s+/g, '');
|
||||||
|
if (user === '') {
|
||||||
|
// Please enter your email.
|
||||||
|
email.current.focus();
|
||||||
|
} else if (pwd === '') {
|
||||||
|
// 'Please enter your password.'
|
||||||
|
password.current.focus();
|
||||||
|
} else {
|
||||||
|
// do login stuff
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Focus email input when component mounted. */
|
||||||
|
React.useEffect(() => {
|
||||||
|
email.current.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthOutlet>
|
||||||
|
<TextField
|
||||||
|
inputRef={email}
|
||||||
|
type="email"
|
||||||
|
label="E-mail"
|
||||||
|
variant="outlined"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Stack gap={1}>
|
||||||
|
<TextField
|
||||||
|
inputRef={password}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
label="Password"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ '& .MuiInputBase-root ': { pr: '4px' } }}
|
||||||
|
autoComplete="new-password"
|
||||||
|
InputProps={{
|
||||||
|
// <-- This is where the toggle button sis added.
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
aria-label="toggle password visibility"
|
||||||
|
onClick={handleClickShowPassword}
|
||||||
|
onMouseDown={handleMouseDownPassword}
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityIcon /> : <VisibilityOffIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
variant="body2"
|
||||||
|
textAlign="right"
|
||||||
|
onClick={() => navigate('/forgot-password')}
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Button variant="contained" onClick={loginHandler}>
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Typography variant="body2" component="p">
|
||||||
|
Dont you have an account?
|
||||||
|
</Typography>
|
||||||
|
<Link
|
||||||
|
variant="body2"
|
||||||
|
sx={{ display: 'inline', ml: 1 }}
|
||||||
|
onClick={() => navigate('/register')}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
</AuthOutlet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
121
src/pages/Auth/Register.jsx
Normal file
121
src/pages/Auth/Register.jsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Stack, Typography, TextField, Button, Link } from '@mui/material';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import AuthOutlet from './AuthOutlet';
|
||||||
|
|
||||||
|
function Register() {
|
||||||
|
const ad = React.useRef(null);
|
||||||
|
const soyad = React.useRef(null);
|
||||||
|
const email = React.useRef(null);
|
||||||
|
const password = React.useRef(null);
|
||||||
|
const passwordConf = React.useRef(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const registerHandler = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const FirstName = ad.current.value.replace(/\s+/g, '');
|
||||||
|
const LastName = soyad.current.value.replace(/\s+/g, '');
|
||||||
|
const Email = email.current.value.replace(/\s+/g, '');
|
||||||
|
const Password = password.current.value.replace(/\s+/g, '');
|
||||||
|
const pwdConf = passwordConf.current.value.replace(/\s+/g, '');
|
||||||
|
if (FirstName === '') {
|
||||||
|
// 'Please enter name.'
|
||||||
|
ad.current.focus();
|
||||||
|
} else if (LastName === '') {
|
||||||
|
// 'Please enter surname.'
|
||||||
|
soyad.current.focus();
|
||||||
|
} else if (Email === '') {
|
||||||
|
// 'Please enter email.'
|
||||||
|
email.current.focus();
|
||||||
|
} else if (!/^[\w-.]+@([\w-]+\.)+[\w-]{1,20}$/.test(Email)) {
|
||||||
|
// 'Please enter real email.'
|
||||||
|
email.current.focus();
|
||||||
|
} else if (Password === '') {
|
||||||
|
// 'Please enter password.'
|
||||||
|
password.current.focus();
|
||||||
|
} else if (pwdConf === '') {
|
||||||
|
// 'Please enter password again.'
|
||||||
|
passwordConf.current.focus();
|
||||||
|
} else if (Password !== pwdConf) {
|
||||||
|
// 'Passwords do not match.'
|
||||||
|
password.current.focus();
|
||||||
|
} else if (Password.length < 6) {
|
||||||
|
// 'Password must be at least 6 characters.'
|
||||||
|
password.current.focus();
|
||||||
|
} else if (
|
||||||
|
!/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{6,}$/.test(Password)
|
||||||
|
) {
|
||||||
|
// 'Password must contain at least one uppercase letter, one lowercase letter and one number.',
|
||||||
|
password.current.focus();
|
||||||
|
} else {
|
||||||
|
// do register stuff
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Focus name input when component mounted. */
|
||||||
|
React.useEffect(() => {
|
||||||
|
ad.current.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthOutlet>
|
||||||
|
<Stack direction="row" gap={3} sx={{ alignItems: 'center' }}>
|
||||||
|
<TextField
|
||||||
|
inputRef={ad}
|
||||||
|
label="Name"
|
||||||
|
type="text"
|
||||||
|
variant="outlined"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
inputRef={soyad}
|
||||||
|
label="Surname"
|
||||||
|
type="text"
|
||||||
|
variant="outlined"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<TextField
|
||||||
|
inputRef={email}
|
||||||
|
type="email"
|
||||||
|
label="E-mail"
|
||||||
|
variant="outlined"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
inputRef={password}
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
label="Password"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
inputRef={passwordConf}
|
||||||
|
hidden
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
label="Password (again)"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="contained" onClick={registerHandler}>
|
||||||
|
Sign Up
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Typography variant="body2" component="p">
|
||||||
|
Alredy have an account?
|
||||||
|
</Typography>
|
||||||
|
<Link
|
||||||
|
variant="body2"
|
||||||
|
sx={{ display: 'inline', ml: 1 }}
|
||||||
|
onClick={() => navigate('/login')}
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
</AuthOutlet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Register;
|
||||||
23
src/pages/Home/Home.jsx
Normal file
23
src/pages/Home/Home.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Typography, Stack, Container } from '@mui/material';
|
||||||
|
import Counter from '@/components/Counter/Counter';
|
||||||
|
import TemplateTester from '@/components/TemplateTester/TemplateTester';
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
return (
|
||||||
|
<Container sx={{ py: 2, position: 'relative' }}>
|
||||||
|
<Stack gap={1} my={2}>
|
||||||
|
<Typography textAlign="center" variant="h2">
|
||||||
|
Viterjs-template
|
||||||
|
</Typography>
|
||||||
|
<Typography textAlign="center" variant="subtitle1">
|
||||||
|
React + Redux + MuI + Axios + ESlint + Prettier
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<TemplateTester />
|
||||||
|
<Counter />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
17
src/routes/Routing.jsx
Normal file
17
src/routes/Routing.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Routes, Route } from 'react-router-dom';
|
||||||
|
import Home from '@/pages/Home/Home';
|
||||||
|
import Login from '@/pages/Auth/Login';
|
||||||
|
import Register from '@/pages/Auth/Register';
|
||||||
|
|
||||||
|
function Routing() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="*" element={<Home />} />
|
||||||
|
<Route path="/login/*" element={<Login />} />
|
||||||
|
<Route path="/register/*" element={<Register />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Routing;
|
||||||
0
src/setupTests.js
Normal file
0
src/setupTests.js
Normal file
203
src/themes/AppThemeProvider.jsx
Normal file
203
src/themes/AppThemeProvider.jsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
ThemeProvider,
|
||||||
|
createTheme,
|
||||||
|
responsiveFontSizes,
|
||||||
|
} from '@mui/material/styles';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { selectMode } from '@/features/user/userSlice';
|
||||||
|
|
||||||
|
function AppThemeProvider({ children }) {
|
||||||
|
const mode = useSelector(selectMode);
|
||||||
|
const theme = responsiveFontSizes(
|
||||||
|
createTheme({
|
||||||
|
palette: {
|
||||||
|
mode,
|
||||||
|
primary: {
|
||||||
|
main: '#1c9c7c',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: '#9DF3C4',
|
||||||
|
},
|
||||||
|
Ink: {
|
||||||
|
Darkest: '#000000',
|
||||||
|
Darker: '#222222',
|
||||||
|
Dark: '#303437',
|
||||||
|
Base: '#404446',
|
||||||
|
Light: '#6C7072',
|
||||||
|
Lighter: '#72777A',
|
||||||
|
},
|
||||||
|
Sky: {
|
||||||
|
Dark: '#979C9E',
|
||||||
|
Base: '#CDCFD0',
|
||||||
|
Light: '#E3E5E5',
|
||||||
|
Lighter: '#F2F4F5',
|
||||||
|
Lightest: '#F7F9FA',
|
||||||
|
White: '#FFFFFF',
|
||||||
|
},
|
||||||
|
|
||||||
|
Red: {
|
||||||
|
Darkest: '#6B0206',
|
||||||
|
Base: '#E8282B',
|
||||||
|
Light: '#F94739',
|
||||||
|
Lighter: '#FF9898',
|
||||||
|
Lightest: '#FFE5E5',
|
||||||
|
},
|
||||||
|
|
||||||
|
Green: {
|
||||||
|
Darkest: '#0A4C0A',
|
||||||
|
Base: '#0F8B0F',
|
||||||
|
Light: '#1EB01E',
|
||||||
|
Lighter: '#7FF77F',
|
||||||
|
Lightest: '#E5FFE5',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: mode === 'dark' ? '#000000' : '#FCFBFA',
|
||||||
|
opposite: mode === 'dark' ? '#FCFBFA' : '#000000',
|
||||||
|
paper: mode === 'dark' ? '#131313' : '#FCFCFC',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: mode === 'dark' ? '#FFFFFF' : '#000000',
|
||||||
|
secondary: '#999999',
|
||||||
|
disabled: '#C3C1BD',
|
||||||
|
},
|
||||||
|
|
||||||
|
grey: {
|
||||||
|
50: mode === 'dark' ? 'hsl(0, 0%, 10%)' : 'hsl(0, 5%, 95%)',
|
||||||
|
100: mode === 'dark' ? 'hsl(0, 0%, 20%)' : 'hsl(0, 0%, 90%)',
|
||||||
|
200: mode === 'dark' ? 'hsl(0, 0%, 30%)' : 'hsl(0, 0%, 80%)',
|
||||||
|
300: mode === 'dark' ? 'hsl(0, 0%, 40%)' : 'hsl(0, 0%, 70%)',
|
||||||
|
400: mode === 'dark' ? 'hsl(0, 0%, 50%)' : 'hsl(0, 0%, 60%)',
|
||||||
|
500: mode === 'dark' ? 'hsl(0, 0%, 60%)' : 'hsl(0, 0%, 50%)',
|
||||||
|
600: mode === 'dark' ? 'hsl(0, 0%, 70%)' : 'hsl(0, 0%, 40%)',
|
||||||
|
700: mode === 'dark' ? 'hsl(0, 0%, 80%)' : 'hsl(0, 0%, 30%)',
|
||||||
|
800: mode === 'dark' ? 'hsl(0, 0%, 90%)' : 'hsl(0, 0%, 20%)',
|
||||||
|
900: mode === 'dark' ? 'hsl(0, 5%, 95%)' : 'hsl(0, 0%, 10%)',
|
||||||
|
},
|
||||||
|
gradient: {
|
||||||
|
bronze: 'linear-gradient(180deg, #9C6D3E 0%, #E8C8A9 100%)',
|
||||||
|
silver: 'linear-gradient(180deg, #808080 0%, #DFDFDF 100%)',
|
||||||
|
gold: 'linear-gradient(180deg, #A3873C 0%, #E3D294 100%)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
typography: {
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
|
||||||
|
h1: {
|
||||||
|
fontSize: '26px',
|
||||||
|
fontWeight: '600',
|
||||||
|
// lineHeight: '33px',
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '22px',
|
||||||
|
fontWeight: '600',
|
||||||
|
// lineHeight: '28px',
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: '600',
|
||||||
|
// lineHeight: '25px',
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: '600',
|
||||||
|
// lineHeight: '23px',
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: '500',
|
||||||
|
// lineHeight: '20px',
|
||||||
|
},
|
||||||
|
|
||||||
|
CTA1: {
|
||||||
|
fontSize: '28px',
|
||||||
|
fontWeight: '500',
|
||||||
|
// lineHeight: '35px',
|
||||||
|
},
|
||||||
|
CTA2: {
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: '500',
|
||||||
|
// lineHeight: '23px',
|
||||||
|
},
|
||||||
|
CTA3: {
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: '400',
|
||||||
|
// lineHeight: '20px',
|
||||||
|
},
|
||||||
|
Body1: {
|
||||||
|
fontFamily: 'Lato, sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '400',
|
||||||
|
// lineHeight: '18px',
|
||||||
|
},
|
||||||
|
Body2: {
|
||||||
|
fontFamily: 'Lato, sans-serif',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '400',
|
||||||
|
// lineHeight: '16px',
|
||||||
|
},
|
||||||
|
Body3: {
|
||||||
|
fontFamily: 'Lato, sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '400',
|
||||||
|
// lineHeight: '14px',
|
||||||
|
},
|
||||||
|
Body1Medium: {
|
||||||
|
fontFamily: 'Lato, sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '500',
|
||||||
|
// lineHeight: '17px',
|
||||||
|
},
|
||||||
|
Body1SemiBold: {
|
||||||
|
fontFamily: 'Lato, sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: '600',
|
||||||
|
// lineHeight: '17px',
|
||||||
|
},
|
||||||
|
body3: {
|
||||||
|
fontSize: '12px',
|
||||||
|
// lineHeight: '16px',
|
||||||
|
display: 'block',
|
||||||
|
},
|
||||||
|
body4: {
|
||||||
|
fontSize: '10px',
|
||||||
|
// lineHeight: '14px',
|
||||||
|
display: 'block',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiCssBaseline: {
|
||||||
|
styleOverrides: {
|
||||||
|
body: {
|
||||||
|
// ---CSS BODY--- \\
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiLink: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'none',
|
||||||
|
lineHeight: '16px',
|
||||||
|
transition: 'all 0.1s ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
opacity: 0.8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiIconButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
aspectRatio: '1/1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppThemeProvider;
|
||||||
25
vite.config.js
Normal file
25
vite.config.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
sourcemap: false,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
css: false,
|
||||||
|
include: ['src/**/__tests__/*'],
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: 'src/setupTests.ts',
|
||||||
|
clearMocks: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user